js的作用域和作用域链那些事
写这篇文章的目的是最近面试的时候一直碰到作用域题做不出来,总是似懂非懂。所以今天特意去深挖了一下。之后还会有更多自己的见解,不对的地方,希望大家能指正。文字较多,希望耐心看完。
在ES5之前并没有块级作用域的说法,直到es6出现才有块级作用域。所以本文只聊es5之前的作用域和作用域链。
-
作用域:
(1)任何未经var声明的就赋值的变量,自动归位全局所有,自动成为全局变量。window就是全局,window的域就是全局的域。
(2)一切声明的全局变量,全是window属性。在函数外声明的变量。(注: 在es5版本即之前的版本中,js是被函数划分成作用域,即函数作用域,es6之后才有块级作用域)
(3)每个JavaScript函数都是一个对象,对象属性有些可以直接访问,有些只提供给JavaScript引擎访问,[[scope]]属性只提供给JavaScript引擎访问,它就是作用域,存储了运行期上下文的集合。对于第一点,我们来看看例子。
<script>
a = 123;
function test(){
b = 234;
}
test();
console.log(window);
</script>
控制台中查看,看到window对象却是拥有了a、b两个属性的值。
接下来我们再打印看看这几个值,并判断这些值得关系。
console.log(window.a);
console.log(a);
console.log(window.b);
console.log(b)
if(window.a === a){
console.log('yes');
}
下图验证了 window的属性值就是单个变量的属性值,且它们是同一个。
对于第二点,大家可以按照同样的方法验证。
- 预编译: 代码执行分成两个阶段,预编译和执行
这个就是本文重点,同样也先上知识点,就是预编译过程。
1)全局的预编译:
(1)创建GO,Global Object 执行期上下文,
(2)找变量,进行变量提升。
(3)找函数声明,进行函数提升。
2)函数的预编译:
(1)创建AO Activity Object 执行期上下文 (作用域和执行期上下文区别可以参考网上其他的文章,如:https://segmentfault.com/a/1190000013915935)
(2)找形参和变量声明, 将变量和形参名作为AO属性名, 值为undefined (本质是:变量提升)
(3)将实参值和形参统一
(4)在函数体里面找函数声明,值赋予函数体 (本质是: 函数提升)
例子来说明一下
var a = 123;
function bar() {
var b = 234;
function foo (){}
console.log(a);
}
bar();
首先对于全局,先创建一个全局的GO,这个全局环境是预编译之后就只有一个。
然后找变量进行变量提升,找到变量声明var a,初始为undefined。
找到函数声明,进行函数提升,并创建,自己的作用域,并接到作用域链中。
这就是全局的预编译过程。
当bar()执行时,bar函数会在全局的作用域上创建活动对象(Activity Object,AO)。并进行变量提升和函数提升。
为了防止大家迷糊,接下来对上面的代进行一次完整的运行。这样机会解决你对变量提升的困惑。
console.log(a); // undefined
console.log(bar); // function(){...}
var a = 123;
console.log(a); // 123
function bar() {
console.log(a); // 123
console.log(b); // undefined
var b = 234;
console.log(b) // 234
function foo (){}
}
console.log(bar); // function(){...}
bar();
首先 创建全局的作用域
接下来对所有全局变量进行变量提升,这里只有a。
然后函数提升
下一步执行代码了,解释一句,执行一句。
- 从顶端开始执行代码,
(1) console.log(a); 打印undefined;
(2) console.log(bar); 打印出函数的内容。
(3) var a = 123; 可以拆分为var a; a = 123;
由于变量声明已经提升了var a; 所以执行时,只看 a = 123;所以a赋值为123之前,a的值为undefined,执行a = 123之后,打印a的值为123;
(4) function bar() {
// 省略代码
} 由于之前已经函数提升了,此处忽略。
(5) console.log(bar) 打印函数
(6) bar(); 开始执行函数,此处会创建函数自己的作用域,在全局的基础之上。创建 Activation Object ,也经历变量提升和函数提升,但是这个函数没有形参,不考虑。
转回去执行函数bar里面的内容。
(6.1) console.log(a); 在函数作用域中开始找,没找到a的定义, 根据上图通过作用域链往下找到全局作用域,找到a的定义,打印 123.
(6.2) console.log(b); 打印undefined。
(6.3) var b = 234;同样拆分成 var b; b= 234; 修改函数的作用域的值为234.
(6.4) console.log(b); 从函数作用域中找,打印234。
好了,过了一遍代码,我想你应该会了。
4. 接下来让我们来点刺激的。给两道题如下。
第一题
console.log(bb);
var bb = 1;
console.log(bb);
function aa() {
console.log(bb);
bb = 2;
console.log(bb); // 原题是alert(bb);
};
aa();
console.log(bb);// 原题是alert(bb);
第二题(注:牛客网原题, 当时我做错了,参考链接 https://www.nowcoder.com/profile/361980525/test/40369218/14917#summary)
console.log(bb);
var bb = 1;
console.log(bb);
function aa(bb) {
console.log(bb);
bb = 2;
console.log(bb); // 原题是alert(bb);
};
aa(bb);
console.log(bb);// 原题是alert(bb)
对于第一道题,先进行全局的预编译,创建GO(为了方便,简单写)。
变量提升, 函数提升。我们得到下面的结果。
开始从上到下执行代码,
console.log(bb); // 打印 undefined
var bb = 1; // 这里拆分成 var bb; bb = 1; 修改全局的bb的值为1
执行 console.log(bb); // 打印 1
console.log(bb); // undefined
var bb = 1; // 这里拆分成 var bb; bb = 1; 修改全局的bb的值为1
console.log(bb); // 1
function aa() { … } 已经进行函数提升,此处忽略了,往下继续执行。
执行aa(); 创建函数自己的作用域AO,无变量声明,无函数声明。得到如下结果。
转到函数内部去执行,由上面可以知道,函数的AO没有
没有bb这个变量,所以顺着作用域链往下找到全局作用域,此时的bb值为1,打印bb的值为1,执行bb = 2;(注:没有var声明自动成为全局变量) 修改全局的值为2,再执行打印语句,打印值为2。
函数执行完毕,跳出来执行最后一个语句值,打印2.
console.log(bb); // undefined
var bb = 1;
console.log(bb); // 1
aa();
function aa() { //为了方便解释,此处调整了aa(bb)和函数的位置。
console.log(bb); // 1
bb = 2;
console.log(bb); // 2
};
console.log(bb); // 2
对于第二题,差别就在于函数有参数,当然还是按照我们的步骤来。看下面的打印结果
console.log(bb); // 这里打印 undefined,
var bb = 1;
console.log(bb);// 这里打印 1
function aa(bb) {
console.log(bb); // 问题在这里 打印的是1,为啥?。
bb = 2;
console.log(bb); // 这里打印2
};
aa(bb);
console.log(bb);// 这里打印 1,为啥?
其实就是因为函数带参数的问题,我们来看看,这两题函数执行时的微妙变化。
其实就是函数执行过程中,形参的问题,第一题没有形参,所以预编译时arguments数组是没有的变量的,而第二道题就是有参数,执行函数时,将有一个步骤容易忽略就是:第3步,实参和形参相统一。
现在第二题从函数执行开始,预编译函数,没有新的变量,不用提升,有形参,要做到实参和形参相统一,将参数bb的值放到arguments数组中,得到下面的结果。
继续执行函数内的代码, 此时打印的值拿到的是arguments数组的值。
再执行bb = 2; 修改的是arguments中bb的值为2。而不是修改全局的值。
所以接下来函数内的打印语句应该是2,函数执行结束,销毁。再继续执行最后一句语句,此时bb是全局的bb,因此在全局域GO中拿到bb的值为1。打印1。
console.log(bb); // 这里打印 undefined,
var bb = 1;
console.log(bb);// 这里打印 1
aa(bb);
function aa(bb) { // 为了方便看,换个位置。
console.log(bb); // 打印的是1。
bb = 2;
console.log(bb); // 这里打印2
};
console.log(bb);// 这里打印 1,
最后来看看函数内的bb是不是归属在arguments中呢?
var bb = 1;
function aa(bb) {
bb = 2;
if(bb === arguments[0]){
console.log('我是函数内部的bb:',bb)
}else{
console.log('我是函数外的bb');
}
};
aa(bb);
上面我们做的都是变量和函数不同名的,现在我们来做一道函数和变量同名的题,按照上面的步骤来。
console.log(a);
a();
a = 123;
console.log(a)
function a(){
console.log(a);
}
a();
console.log(a);
var a;
全局预编译:创建GO,寻找变量声明,找到var a ;进行变量提升,值为undefined。
GO => {
a: undefined,
}
寻找函数声明,进行函数提升,因为们这题是变量a和函数a同名,此时undefined修改为function
GO => {
a: function(){...},
}
预编译完成了,执行代码:
执行第一句: 找到GO中的a的值是一个函数。
执行a();
执行 a = 123, 将全局的变量a修改成123。
执行下一句,打印出 123
GO => {
a: 123,
}
console.log(a); // 打印该函数题。
a(); // 执行并打印函数体。
a = 123; // 执行,并修改a的值为 123,
console.log(a) // 123
function a(){
console.log(a);
}
a(); // 执行到此发现之前全局的a已经不是变量了,此处报错。不在继续往下执行。
console.log(a);
var a;
当然真正写代码的时候,我们不会使用变量和函数同名。
在添加一些考题吧,供大家参考。严格按照步骤多做几题巩固一下,作用域和作用域链完全不在话下,什么变量提升困扰都解决。
var a = 456;
console.log(a); // 456
var def = 999;
console.log(def); // 999
function test() {
var abc = 123;
def = 234; // 提示: 没有声明,自动成为全局变量
a = 0 // 提示:修改的是全局的a
console.log(abc); // 123
console.log(def); // 234
};
test();
console.log(window.a); // 0
console.log(window.def); // 234
console.log(a); // undefined
console.log(b); // undefined
var a = 123;
var b = 444;
console.log(a); // 123
console.log(b); // 444
function test(b){ // 形参和实参相统一
console.log(b); // 444
var b = 555; // 提示:改变arguments中的值
console.log(b); // 555
}
test(b);
console.log(b); // 444
console.log(a); // 123