js的作用域和作用域链那些事

js的作用域和作用域链那些事

写这篇文章的目的是最近面试的时候一直碰到作用域题做不出来,总是似懂非懂。所以今天特意去深挖了一下。之后还会有更多自己的见解,不对的地方,希望大家能指正。文字较多,希望耐心看完。

在ES5之前并没有块级作用域的说法,直到es6出现才有块级作用域。所以本文只聊es5之前的作用域和作用域链。

  1. 作用域:
    (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)全局的预编译:
    (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. 从顶端开始执行代码,
    (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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值