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
对象上。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。