我所知道的JavaScript——变量提升(面试强化版)这是更加全面的针对【变量提升】类型面试题的文章。包含了基本考察的
往期回顾
在上一篇文章里,我们聊到了JS的变量提升,并且延伸了编译器、作用域、JS引擎查找变量的2种方式等知识点,比较适合入门阶段阅读,构建知识体系的“地基”。
(建议您阅读上一篇文章,这将有利于您在这一篇文章的阅读体验。)
前言
当一篇文章涉及的知识点比较广的时候,往往很难做到“面面俱到”——即深度,这也是我写的第一篇文章所缺少的方面。
知识的理解和掌握离不开实际的应用,当我们将理论化为武器之后,实战的考验是很有必要的。
因此,在参与了一些面试之后,针对之前的这一篇文章,我做了一个面试强化版的改造。
话不多说,直接看题吧~
题目训练
基础与变式
基础
先来看一道最基础的变量提升
// 题目1
console.log(a);
// output: undefined
var a = 2;
console.log(a);
// output: 2
// 题目2
function test() {
console.log(a);
// output: undefined
var a = 2;
console.log(a);
// output: 2
}
test();
在这一题里,我们将变量的声明和赋值放到了test函数内部,并同样尝试打印a两次。可以看到,结果和第一题是一样的。这是因为在函数作用域中,a变量被提升至了函数作用域顶部完成声明,赋值语句留在原地。
通过这两题我们可以知道,对于用var关键字声明的变量,在全局作用域和函数作用域中均会发生提升。
那么,在块级作用域又会如何呢?
// 题目3
if (true) {
console.log(a);
// output: undefined
var a = 2;
console.log(a);
// output: 2
}
看到输出结果你可能会认为,啊,a在块级作用域里也被提升了。所以变量提升也会发生在块级作用域。
事实真的如此吗?
// 题目4
var a = 1;
if (true) {
console.log(a);
var a = 2;
console.log(a);
}
上面这道题的输出结果是?
为什么,居然是1和2?
因为var
关键字并不受块级作用域的限制。实际上【题目3】之所以会输出undefined
、2
是因为变量提升发生在了全局作用域,引擎在查找a
变量的时候,是从块级作用域跳到全局作用域去的。
顺便,我们可以验证一下【题目2】中提到的函数作用域是否真的会发生变量提升:
//题目5
var a = 1;
function test() {
console.log(a);
// output: undefined
var a = 2;
console.log(a);
// output: 2
}
test();
if (true) {
console.log(a);
// output: 1
var a = 2;
console.log(a);
// output: 2
}
因此现在我们可以得出结论了:
变量提升只会发生在函数作用域和全局作用域
变式
在上一章节我们聊了有关变量提升会发生的作用域。在这一章节,我们聊一聊函数和函数表达式的变量提升。
// 题目1
test();
function test() {
console.log("Hello");
}
上述代码是否会导致ReferenceError
的报错呢?
并不会报错,会正常打印Hello
,test
的函数声明同样会被提升,因此【题目1】的代码可以看成是这样:
function test() {
console.log("Hello");
}
test();
看完了函数声明,我们知道在js中,还可以通过函数表达式去创建一个函数对象,如:
// 题目2
test2();
var test2 = function () {
console.log("Bye");
};
结果是TypeError
,而不是ReferenceError
,说明test2
变量还是被提升了的,只不过作为赋值的函数表达式留在了原地。test2();
就当于是undefined()
,因为会报错TypeError:test2 is not a function
,
至此,根据上面两题的内容,我们可以做个总结
函数声明会被提升✔,但是函数表达式不会被提升❌
组合
那么如果一个变量被重复声明了,并且存在函数形式的重复声明,结果又会如何呢?
// 题目1
test();
var test = 1;
function test() {
console.log("Hello");
}
test = function () {
console.log("Bye");
};
判断上方代码的输出情况?
让我们利用所学的知识,一起试试看吧~
- 首先我们确定一下,都有哪几个角色期望扮演test:
var test = 1;
function test()...
test = function ()...
- 而后,我们知道会发生变量提升,声明语句被提升至作用于的顶部,赋值语句留在原地。因此代码可以写成这样:
function test() { console.log("Hello"); } test(); test = 1; test = function () { console.log("Bye"); };
- 虽然在【题目1 】中的
var test
语句顺序写在了函数声明之前,但是它是重复的声明,因此被忽略了。因为函数声明会提升到普通变量之前。 - 因此,这段代码执行会,会输出
Hello
接下来,我们再做一些变化,我们如果重复2次函数声明,结果又会如何呢?
// 题目2
test();
var test = 1;
function test() {
console.log("Hello");
}
function test() {
console.log("Bye");
}
上面代码执行后的结果如下:
可以看到,尽管重复的var
声明会被忽略掉,但是重复的函数声明与之不同。出现在后面的函数声明会覆盖前面的函数声明。
我们同样可以将【题目2】的代码转换成下方这样:
function test() {
console.log("Bye");
}
test();
test = 1
由 var
到 let
的扩展
虽然我们关于变量提升聊了很多的内容,但是实际上我们在写业务的时候,用的都是ES6+,因此声明变量采用的都是let
、const
关键字。
那么问题来了,
let
会发生变量提升吗?
直接从代码例子来看的话
// 题目1
console.log(a);
let a = 2;
console.log(a);
可以看到,报错了ReferenceError
。不过我们会发现后面跟着的报错的信息是不是有点不太一样?
我们写一个同样会触发ReferenceError
的例子:
console.log(a)
我们汇总一下两种报错:
- ReferenceError: Cannot access 'a' before initialization
- ReferenceError: a is not defined
前者是不给访问,后者是真的找不到。
所以,实际上let
还是会发生“变量提升”的,不过这个变量在被初始化赋值之前,无法被访问,这也就是另一个面试中会遇到的问题:
🕵️:什么是暂时性死区?
暂时性死区: 用
let
、const
或class
声明的变量可以称其从代码块的开始一直到代码执行到变量声明的位置并被初始化前,都处于一个“暂时性死区”(Temporal dead zone,TDZ)中。当变量处于暂时性死区之中时,其尚未被初始化,并且任何访问其的尝试都将导致抛出
ReferenceError
。当代码执行到变量被声明的位置时,变量会被初始化为一个值。如果变量声明中未指定初始值,则变量将被初始化为undefined
。——MDN
由 let
到 const
的扩展
const
和 let
最明显的区别在于使用const
创建的变量是不可变的。当然,这里的不可变是指不能通过使用赋值运算符来更改。
比如:
const a = 1
a = 2
但是如果使用const
创建的变量的值是一个对象,则可以对这个对象的属性进行增删改。
const a = { b: "Hello" };
// 增
a.c = 123;
// 改
a.b = 0;
// 增
a.d = 4;
console.log(a);
// 删
delete a.d;
console.log(a);
从 const
的不可变性扩展到对象的不可变
聊到const
,可能面试官会发散一个新的问题:
就是期望让
const
创建的对象的属性,也不能够被改变,该怎么做?
看似这道题考察的是const
,实际上面试官考察的是我们对Object
的静态方法的了解程度。
1. 使用Object.freeze()
方法
Object.freeze()
静态方法可以使一个对象被冻结。冻结对象可以防止扩展,并使现有的属性不可写入和不可配置。被冻结的对象不能再被更改:不能添加新的属性,不能移除现有的属性,不能更改它们的可枚举性、可配置性、可写性或值,对象的原型也不能被重新指定。freeze()
返回与传入的对象相同的对象。
const obj = Object.freeze({
prop1: "value1",
prop2: "value2",
});
// 尝试修改属性
obj.prop1 = "new value";
// 尝试添加属性
obj.prop3 = "new value";
// 尝试删除属性
delete obj.prop1;
console.log(obj);
2. 使用Object.seal()
方法
Object.seal()
静态方法密封一个对象。密封一个对象会阻止其扩展并且使得现有属性不可配置。密封对象有一组固定的属性:不能添加新属性、不能删除现有属性或更改其可枚举性和可配置性、不能重新分配其原型。只要现有属性的值是可写的,它们仍然可以更改。seal()
返回传入的同一对象。
如果要使属性值也不可更改,可以结合使用Object.defineProperty()
。
const obj = Object.seal({
prop1: "value1",
prop2: "value2",
});
// 使用Object.defineProperty()来使属性不可写
Object.defineProperty(obj, "prop1", {
writable: false,
});
// 尝试修改属性
obj.prop1 = "new value";
// 尝试添加属性
obj.prop3 = "new value";
// 尝试删除属性
delete obj.prop1;
console.log(obj);
3. 使用Object.defineProperty()
或Object.defineProperties()
单独或批量地为对象的每个属性使用Object.defineProperty()
或Object.defineProperties()
来定义属性的特性,并设置writable
为false
。
const obj = {};
Object.defineProperty(obj, 'prop1', {
value: "value1",
writable: false
});
// 或者批量定义
Object.defineProperties(obj, {
prop2: { value: "value2", writable: false },
prop3: { value: "value3", writable: false }
});
// 尝试修改属性将会失败
obj.prop1 = "new value"; // 不会生效
可能有同学会问了,那我如果使用Object.defineProperty
去修改一个writable: false
的已经存在于obj
中的props
时,能够修改成功吗?
毕竟作为Object.defineProperty
的第3个参数,我们可以通过value
这个字段去设置props
的值。
不行哦,JS也想到咯,不给大家卡BUG的机会,哈哈~
总结
以上方法中,Object.freeze()
是最简单和最直接的方法来创建一个完全不可变对象。然而,需要注意的是,这些方法仅影响直接属性,如果对象包含其他对象或数组,则需要递归地应用这些方法来确保整个对象结构都是不可变的。
思考
在【题目训练/基础与变式/组合】这一章节,我们聊到了函数表达式和函数声明,在函数表达式里,我们写的都是匿名函数,比如:
test = function () {
console.log("Bye");
};
假如我们在函数表达式里写上具名函数,并且试图首先调用这个具名函数,又会如何呢?
test();
test2();
function test() {
console.log("Hello");
}
var fn = function test2() {
console.log("Hye");
};
当我们运行这段代码时,输出结果如下:
可以看到,报错ReferenceError
,这说明就算是具名函数,只要它以函数表达式的形式出现,它就会被留在原地,不会发生提升。
结语
希望这篇面试强化版的变量提升专题文章能够让你有所收获,期待与你在系列的下一篇文章相遇~
转载自:https://juejin.cn/post/7405260565679603721