题目
求打印结果。
{
function foo() {123}
foo = 1;
foo = 2;
function foo() {456}
foo = 10;
}
console.log(String(foo));
你可能说,要么是10,要么是下面的函数声明。很遗憾,结果是2。测试如下:
{
console.log(0, String(foo), String(window.foo)) // "function foo() {456}" "undefined"
function foo() {123}
debugger;
console.log(1, String(foo), String(window.foo)) // "function foo() {456}" "function foo() {456}"
foo = 1;
debugger;
console.log(2, String(foo), String(window.foo)) // "1" "function foo() {456}"
foo = 2;
debugger;
console.log(3, String(foo), String(window.foo)) // "2" "function foo() {456}"
function foo() {456}
debugger;
console.log(4, String(foo), String(window.foo)) // "2" "2"
foo = 10;
debugger;
console.log(5, String(foo), String(window.foo)) // "10" "2"
}
debugger;
console.log(6, String(foo), String(window.foo)); // "2" "2"
为什么会这样?
骑墙的“非严格模式”、变量初始化提升现象
非严格模式
ES6引入了块级作用域,明确允许在块级作用域之中声明函数。ES6规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。
但是,浏览器为了兼容老的代码,会宽泛看待块级作用域,也就是说,如果将{}
视为包裹语句的关键字,也能解释得通的话,那么,引擎会同时既将{}
视为包裹语句的关键字,同时也视为块级作用域,以避免报错。
当然,浏览器的这个做法是不规范的,所以,ES6也引入了严格模式,即"use strict"
,在顶部加上"use strict"的话,就会报错了。
现在我们知道,非严格模式下,{}
里面存在函数声明的话,会在全局和块级作用域都出现变量,指向不同的内存地址。同样的,var
声明也是这样的处理方式。
但是,let
和const
就不是,因为它俩是新语法,才不搞这种恶心的兼容。
变量提升现象
变量赋值和函数声明都会提升,原则跟书写顺序无关,原则是:
- var的变量,会提升到最顶部,隐式生成
var a;
,此时a等于undefined。 - 下面是函数声明提升,会变量名和函数内容一起提升,并不会只提升变量名,不提升函数体。
- 再下面是
a = 1234
,也就是变量赋值。
非严格模式+变量提升,就产生下面的结果:
console.log(String(foo), String(window.foo)); // 全都undefined
debugger; // 在这个点,你会发现全局作用域有foo变量,为undefined
{
debugger; // 在这个点,你会发现局部作用域有foo变量,是函数,全局foo变量依然undefined
function foo() {123}
debugger; // 在这个点,你会发现块级作用域和全局作用域全都有foo函数
console.log(String(foo), String(window.foo)); // 全都打印出函数体
}
console.log(String(foo), String(window.foo)); // 全都打印出函数体
可见,非严格模式的不规范兼容操作,发生在显式声明函数之后,也就是function foo() {123}
之后,浏览器做的操作是:把内部foo函数复制一份(不同的内存地址),赋值给全局foo。
如果后续再次遇到显式的函数声明,就再做一次兼容操作。比如function foo() {456}。
foo = 1是什么作用?
简单的常识是,未声明、直接赋值的变量,是全局变量。如果题目中没有函数声明,那么它毫无疑问指向全局变量,但是,由于有函数声明,且提升到顶部,所以,foo = 1
其实修改的是块级作用域的foo
变量,foo
自此由指向函数变为指向1
。
foo = 2之后,foo
指向2。
在这道题里,由于有隐式函数提升,所以foo = 1这种语句完全合法。
注意,由于没使用var,所以这里不需要讨论var变量提升。
全盘过一遍
-
函数声明提升且有一次重赋值,所以局部作用域的“第0句”语句就是
function foo() {456}
。 -
function foo() {123}
显式声明会促使浏览器做兼容,给全局foo赋值一次,此时内外变量都是函数456。 -
foo = 1;
和foo = 2;
重赋值局部的foo,现在外部foo是函数456,内部foo是数字。 -
function foo() {456}
促使浏览器再一次兼容操作,内部foo的值(此时是2)会赋值给外部foo,现在内外foo都是2。 -
foo = 10
让内部foo变成10,外部foo依然是2。 -
最后一句console,由于写在全局作用域,它打印的foo都是全局foo,所以都是2。
网友评论