1. 可变性
在 JavaScript 中有七种根本数据类型(string
、number
、boolean
、undefined
、symbol
、bigint
和 null
),这些都是不可变的。这意味着一旦调配了一个值,咱们就无奈批改它们,咱们能够做的是将它重新分配给一个不同的值(不同的内存指针)。另一方面,其余数据类型(如 Object 和 Function)是可变的,这意味着咱们能够批改同一内存指针中的值。
// Q1
let text = 'abcde'
text[1] = 'z'
console.log(text) // abcde
字符串是不可变的,因而一旦调配给一个值,就不能将其更改为不同的值,您能够做的是重新分配它。请记住,更改值和重新分配给另一个值是不同的。
// Q2
const arr = [1, 2, 3]
arr.length = 0
console.log(arr) // []
调配 arr.length
为 0 与重置或革除数组雷同,因而此时数组将变为空数组。
// Q3
const arr = [1, 2, 3, 4]
arr[100] = undefined
console.log(arr, arr.length) // [1, 2, 3, 4, empty × 96, undefined] 101
因为数组占用的是间断的内存地位,所以当咱们将索引 100 赋给一个值(包含 undefined
)时,JavaScript 会保留索引 0 到 索引 100 的内存,这意味着当初的数组长度为 101。
2. var 和 晋升
// Q4
var variable = 10;
(() => {
variable2 = 100
console.log(variable)
console.log(variable2)
variable = 20
var variable2 = 50
console.log(variable)
})();
console.log(variable)
var variable = 30
console.log(variable2)
// 10
// 100
// 20
// 20
// ReferenceError: variable2 is not defined
var
是函数作用域变量,而 let
和 const
是块级作用域变量,只有 var
能被晋升,这意味着变量申明总是被挪动到顶部。因为晋升,您甚至能够在应用 var
关键字申明变量之前调配、调用或应用该变量。
let
和const
不能被晋升,因为它启用了 TDZ(临时性死区),这意味着变量在申明之前是不可拜访的。
在下面的示例中,variable2
在函数外部申明,var
关键字使该变量仅在函数范畴内可用。所以当函数外的任何货色想要应用或者调用该变量时,referenceError
就会被抛出。
// Q5
test() // 不报错
function test() {cconsole.log('test')
}
test2() // 报错
var test2 = () => console.log('test2')
function
关键字申明的函数能够晋升函数语句,然而不能晋升箭头函数,即便它是应用 var
进行变量申明的。
3. 必然性全局变量
// Q6
function foo() {
let a = b = 0;
a++;
return a;
}
foo();
typeof b; // number
typeof a; // undefined
console.log(a) // error: ReferenceError: a is not defined
var
是函数作用域,let
是块级作用域变量。尽管看起来 a
和 b
都是应用 let
申明的(let a = b = 0
),但实际上变量 b
被申明为全局变量并调配给 Window
对象。换句话说,它相似于:
function foo() {
window.b = 0;
let a = b;
a++;
}
4. 闭包
// Q7
const length = 4;
const fns = [];
const fns2 = [];
for(var i = 0; i < length; i++) {fns.push(() => console.log(i));
}
for(let i = 0; i < length; i++) {fns2.push(() => console.log(i));
}
fns.forEach(fn => fn()); // 4 4 4 4
fns2.forEach(fn => fn()); // 0 1 2 3
闭包是对变量环境的一种爱护,即便变量曾经更改或者已被垃圾回收。在下面的问题中,区别在于变量申明,其中第一个循环应用的是 var
,第二个循环应用的是 let
。
var
是函数作用域变量,因而当它在 for
循环块内申明时,var
被视为全局变量而不是外部变量。另一方面,let
是块级作用域的变量,相似于 Java 和 C++ 等其余语言中的变量申明。
在这种状况下,闭包只产生在 let
变量中,推送到 fns2
数组的每个函数都会记住变量以后的值,无论变量未来是否更改。相同,fns
不记住变量的以后值,它应用全局变量的将来或最终值。
5. 对象
// Q8
var obj1 = {n: 1}
var obj2 = obj1
obj2.n = 2
console.log(obj1) // {n: 2}
// Q9
function foo(obj) {
obj.n = 3
obj.name = '测试'
}
foo(obj2)
console,log(obj1) // {n: 3, name: '测试'}
正如咱们所知,对象变量仅蕴含该对象的内存地位指针,所以这里 obj2
和 obj1
指向同一个对象。这意味着如果咱们更改 obj2
的任何值,obj1
也会受到影响,因为实质上它们是同一个对象。同样,当咱们在函数中将对象作为参数传递时,传递的参数只蕴含对象指针。因而,函数能够间接批改对象而不返回任何内容,这种技术称为 通过援用传递。
// Q10
var foo = {n: 1};
var bar = foo;
console.log(foo === bar); // true
foo.x = foo = {n: 2};
console.log(foo) // {n: 2}
console.log(bar) // {n: 1, x: { n: 2} }
console.log(foo === bar) // false
因为对象变量只蕴含该对象内存地位的指针,所以当咱们申明 var bar = foo
时,foo
和 bar
都指向同一个对象。
在下一个逻辑中,foo = {n: 2}
首先运行,其中 foo
被调配给不同对象,因而 foo
有一个指向不同对象的指针。同时,foo.x = foo
正在运行,这里的 foo
依然蕴含旧指针,所以逻辑相似于:
foo = {n: 2}
bar.x = foo
所以 bar.x = {n: 2}
,最初 foo
的值是 {n: 2}
,而 bar
是 {n: 1, x: { n: 2} }
。
6. this
// Q11
const obj = {
name: "test",
prop: {
name: "prop name",
print: function(){console.log(this.name)
},
},
print: function(){console.log(this.name)
}
print2: () => console.log(this.name, this)
}
obj.print() // test
obj.prop.print() // prop name
obj.print2() // undefined, window global object
下面的例子展现了 this
关键字在一个对象中是如何工作的,this
援用执行函数中的执行上下文对象。然而,this
范畴仅在一般函数申明中可用,在箭头函数中不可用。
下面的例子展现了显示绑定,例如在 object1.object2.object3.object4.print()
中,print
函数将应用最新的对象 object4
作为 this
上下文,如果 this
未绑定对象,它将回退到根对象,该对象是在调用 obj.print2()
时的 Window
全局对象。
另一方面,您还必须了解对象上下文之前曾经绑定的隐式绑定,因而下一个函数执行始终应用该对象作为 this
上下文。例如:当咱们应用 func.bind(<object>)
时,它将返回一个 <object>
用作新执行上下文的新函数。
7. 强制转换
// Q12
console.log(1 + "2" + "2"); // 122
console.log(1 + +"2" + "2"); // 32
console.log(1 + -"1" + "2"); // 02
console.log(+"1" + "1" + "2"); // 112
console.log("A" - "B" + "2"); // NaN2
console.log("A" - "B" + 2); // NaN
"10,11" == [[[[10]], 11]] // true (10,11 == 10,11)
"[object Object]" == {name: "test"} true
强制转换是最辣手的 JavaScript 问题之一。一般来说有两条准则,第一条是,如果 2 个操作数与 +
操作符连贯,则两个操作数将首先应用 toString
办法转变为字符串,而后连贯。同时,其余运算符(如 -
、*
或 /
)会将操作数更改为数字,如果它不能被强制转换为一个数字,则返回 NAN
。
如果操作数蕴含一个对象或数组,那就更辣手了。任何对象的 toString
办法返回的都是 "[object Object]"
,但在数组中,该 toString
办法将返回由逗号分隔的根底值。
留神:==
示意容许强制转换,而 ===
不容许。
8. 异步
// Q13
console.log(1);
new Promise(resolve => {console.log(2);
return setTimeout(() => {console.log(3);
resolve();}, 0)
})
setTimeout(function() {console.log(4) }, 1000);
setTimeout(function() {console.log(5) }, 0);
console.log(6);
// 1
// 2
// 6
// 3
// 5
// 4
在这里,你须要晓得事件循环、宏工作和微工作队列是如何工作的。您能够在此处查看这篇文章,这里深入探讨了这些概念。个别状况下,异步函数在所用同步函数执行完后才执行。
// Q14
async function foo() {return 10;}
console.log(foo()) // Promise{<fulfilled>: 10}
一旦函数申明为 async
,它总是返回一个 Promise
,无论外部逻辑是同步的还异步的。
// Q15
const delay = async (item) => new Promise(resolve => setTimeout(() => {console.log(item);
resolve(item);
}, Math.random() * 100)
)
console.log(1)
let arr = [3, 4, 5, 6]
arr.forEach(async item => await delay(item)))
console.log(2)
forEach
函数总是同步的,不论每个循环是同步的还是异步的,这意味着每个循环都不会期待另一个。如果要顺次执行每个循环并互相期待,能够改用 for of
。
9. 函数
// Q16
if(function f(){}) {console.log(f)
}
// error: ReferenceError: f is not defined
在下面的例子中,if
条件被满足,因为函数申明被认为是一个真值。然而,外部块无法访问函数申明,因为它们具备不同的块作用域。
// Q17
function foo() {
return
{name: 2}
}
foo() // 返回 undefined
因为主动分号插入(ASI)机制,return
语句将以分号完结,并且分号上面的所有内容都不会运行。
// Q18
function foo(a, b, a) {return a + b}
console.log(foo(1, 2, 3)) // 3+2 = 5
function foo2(a, b, c = a) {return a + b + c}
console.log(foo(1, 2)) // 1+2+1 = 4
function foo3(a = b, b) {return a + b}
console.log(foo3(1, 2)) // 1+2 = 3
console.log(foo3(undefined, 2)) // 谬误
前三次执行的很分明,然而最初一个函数执行会报错,因为 b
在申明之前就被应用了,相似于这样:
let a = b;
let b = 2;
10. 原型
// Q19
function Persion() {}
Persion.prototype.walk = function() {return this}
Persion.run = function() {return this}
let user = new Persion();
let walk = user.walk;
console.log(walk()) // window object
console.log(user.walk()) // user object
let run = Persion.run;
console.log(run()); // window object
console.log(user.run()); // TypeError: user.run is not a function
原型是存在于每个变量中的对象,用于从其父对象继承个性。例如,当您申明一个字符串变量时,该字符串变量具备一个继承自 String.prototype
的原型,这就是为什么您能够在字符串变量中调用字符串办法的起因,例如 string.replace(), string.substring()
等。
在下面的示例中,咱们将 walk
函数调配给 Persion
函数的原型,并将 run
函数调配给函数对象。这是两个不同的对象,函数应用 new
关键字创立的每个对象都将从函数原型而不是函数对象上继承办法。然而请记住,如果咱们将该函数调配给一个变量,如 let walk = user.walk
,该函数将遗记应用 user
作为执行上下文,而是返回到 Window
对象上。
原文:https://medium.com/@andreassu…