预警:【你不知道的javascript】开头的文章,是用来总结书籍《你不知道的Javascript》中我不熟悉的知识,单纯的用来巩固学到的知识,和加深记忆。
可能有些地方我理解的不透彻,所以不能够保证内容的正确性,欢迎指正。
前提为什么 js 中会有明明提升?
这是因为 js 是一门解释性语言,在执行代码之前会对代码进行扫描,进行代码解析和预处理,在这个编译的阶段会对所有 var 声明的变量 和函数声明提升到作用于的顶端,但是变量的赋值不会被提升。
重点知识
- var 声明的变量,会提升至全局作用域名/函数作用域 的顶部
- 不存在块级作用域,只有全局/函数作用域
- 允许重复声明
- 只提升声明,并且会初始化为 undefined
- 不提升赋值
- 实际步骤是
- 创建变量,在内存中开辟空间【会提升】
- 初始化变量,值为 undefined【会提升】
- 真正的赋值【不提升】
- function 函数声明会提升到全局作用域名/函数作用域的顶部
- 函数声明会被提升
- 函数名和 var 变量同名时,保留函数的声明
- 函数表达式不会被提升,具名函数表达式也不会被提升
- 函数表达式是指:var fn = function(){} 这样声明的
- 意思是函数表达式的声明,只提升了那个函数名的变量【也就是fn】,值会被初始化为 undefined
- 实际步骤
- 创建变量,在内存中开辟空间【会提升】
- 初始化函数【会提升】
- 真正的赋值,就是函数体【会提升】!!,但是函数表达式不会被提升
- 注意 return 语句并不会影响函数的提升,函数内容 return 之后,如果还有 var 声明,那么这个声明依旧会提升,只不过赋值语句不会执行,请看下面两个例子
-
var name = 1 function test() { console.log(name) return } test() // 打印 1
-
var name = 1 function test() { console.log(name) return var name = 2 } test() // 打印 undefined
- 函数声明优先于 var 变量,但是函数和 var 变量重名时,函数声明有效
- let / const 声明的变量, 在声明之前使用,会出现暂时性死区
- 会存在块级作用域,就是可以使用花括号 { } 单独隔起来
- 提升到块级作用于的顶端
- 不允许重复声明
- const 声明的不允许改动
- 报错,所以暂时性死区 和 未定义的报错是不同的,所以不能说 let 没有提升,只能说它的初始化没有提升
- 暂时性死区:Cannot access 'a' before initialization
-
var name = 1 function test() { console.log(name) let name = 2 } test() // Cannot access 'name' before initialization
-
- 未定义:a is not defined
-
function test() { console.log(name) } test() // name is not defined
-
- 暂时性死区:Cannot access 'a' before initialization
- 实际步骤是
- 创建变量,再内存中开辟空间【会提升】
- 初始化变量,值为 undefined【不会提升】,所以有暂时性死区
- 真正的赋值【不提升】
代码实践
光说不练假把式,还是需要看你个输出结果的题目来巩固一下,关于命名提升这块,常见的面试题目就是给你一段代码,让你写出他的输出,下面有几个例子:
示例一
var getName = function () { console.log(1);};
function getName() { console.log(2);}
getName()
你猜猜这三行代码会打印啥?
答案是【打印1】,想不到吧
- 编译阶段
- 提升 var 声明的变量,并赋值为 undefined【其实这样也叫函数表达式,因为给他赋值成了一个函数】
- 提升 getName函数声明,并覆盖同名的变量
- 执行阶段
- 给全局的 getName 重新赋值为【function () { console.log(1);};】
- 打印1
示例二
var getName
function getName() { console.log(2);}
getName()
那你再看看这个打印什么?
结果是打印 2
因为这里在执行阶段没有给 getName 赋值的操作
示例三
var getName = 1
function getName() { console.log(2);}
getName()
再看看这个呢?
答案是报错【getName is not a function】
这是在执行阶段给 getName 重新赋值为了 1,很显然1 不是一个函数,所以报错
万事具备,看一下下面这个比较难的题目
示例四
function Foo() {
getName = function () { console.log(1); };
return this;
}
Foo.getName = function () { console.log(2);};
Foo.prototype.getName = function () { console.log(3);};
var getName = function () { console.log(4);};
function getName() { console.log(5);}
Foo.getName();
getName();
Foo().getName();
getName();
这个例子不算简单,因为他涵盖了关于命名提升、this 指针、原型这三方面的知识【这是我在面试美团的时候遇到的面试题,没答上来,面试也没过~】
来解释一下
- 编译阶段
- 声明了函数 Foo,把整个代码提升到全局作用域顶部
- 虽然本来就在顶部
- 注意,函数的提升,没有赋值这一说【因为没有等号 】,但是有初始化这一说,初始化就是函数的主代码,肯定是跟着函数名一块提升的
- 而且这个时候不需要考虑函数内部的任何代码
- 使用 var 声明了全局变量 getName,提升到全局作用域顶部并初始化为 undefined【注意这里赋值没有提升,会方法后面执行阶段执行,到时候千万别忘了】
- 声明了函数 getName,需要提升到全局作用域顶部
- 但是和全局变量重名了,所以保留函数的声明
- 这个时候只有一个全局的名为 getName 的函数了,没有值为 undefined的 getName 变量了
- 声明了函数 Foo,把整个代码提升到全局作用域顶部
- 执行阶段
- 给 getName 赋值为【 function () { console.log(4);};】的函数
- 在编译阶段结束,全局有一个名字为 getName 的函数
- 但是在代码的第7行,有一个赋值操作不能遗忘了,编译阶段只提升了声明,这个赋值没有执行
- 所以运行到这里会把全局的 getName 函数再更新为打印4
- 这个实在太重要了,总是忘掉
- 调用Foo.getName()
- Foo 是已经命名提升了的全局函数
- 注意是调用 Foo的属性 getName,而不是调用函数 Foo
- Foo 有自定义的 getName属性,所以【打印2】
- 调用 getName
- 此时是调用全局的 getName 函数
- 所以会【打印4】
- 注意现在全局没有名字为 getName 的变量了,只有一个名为 getName 的函数,因为同名的提升,保留函数的
- 调用Foo().getName()
- Foo 是已经命名提升了的全局函数
- 这句代码是先运行 Foo,然后调用它返回值的 getName 函数
- Foo 里面给 getName 赋值,会在作用域链上查找名字为 getName 的变量或着函数,此时会找到全局 getName 的函数【这个时候全局getName 是打印4】
- 把全局名为 getName 的函数重新赋值为打印 1
- 返回 this,这个 this 是调用 Foo 的位置的执行上下文,这里就是全局【注意this 指针的执行取决于调用位置】
- 下一步相当于调用 this.getName(),也就是运行全局的getName【打印1】
- 调用 getName
- 这个时候就是单纯的调用全局的 getName方法了【打印1】
- 给 getName 赋值为【 function () { console.log(4);};】的函数
所以这个实例的打印结果是2、4、1、1
示例五
function Foo() {
getName = function () { console.log(1); };
var getName = function() {console.log(6)}
getName()
return this;
}
Foo.getName = function () { console.log(2);};
Foo.prototype.getName = function () { console.log(3);};
var getName = function () { console.log(4);};
function getName() { console.log(5);}
Foo.getName();
getName();
Foo().getName();
getName();
把上面的例子稍作改动,在 Foo 里面增加了 2 行代码
打印结果是【2、4、6、4、4】
在调用 Foo 函数的时候,没有修改全局的 getName,因为 Foo 函数内部有使用 var 声明的 getName 也会在编译阶段【注意是函数的编译阶段】然后后面修改和调用的都是函数内部的 getName 变量。全局的 getName 没有收到影响
示例六
var x = 1, y = 0, z = 0
var add = function (x) {
return x = x + 1
}
y = add(x)
function add(x) {
return x = x + 3
}
z = add(x)
console.log(x, y, z)
打印结果是 【1, 2,2】
知识点,函数的 return 语句中的赋值表达式返回的是一个值,而且函数内部的 x 是局部变量。
示例七
(function(){
var x = y = 1;
})();
var z;
console.log(y);
console.log(z);
console.log(x);
打印结果是 【1, undefined,x is not defined】
知识点,立即执行函数中,未被 var 声明的变量,会提升到全局,所以y 是全局变量。
var 声明多个变量,是使用 逗号隔开的。
总结
关于 js 中命名提升的知识点是很重要的,感觉只要搞懂这篇文章中的几个例子,一般的问题就都能理解了。