js执行环境、作用域链、闭包、this

执行环境(execution context) 是js中非常重要的概念,它定义了变量或函数能够访问的其它数据。事实上执行环境中还包含了很多js里面很多重要的概念,比如作用域链、闭包、this指向、变量对象活动对象等等,这里我以执行环境的过程为顺序大致总结一下,将这些知识点串联起来,加深理解。

 

执行环境也可以称作执行上下文,每当函数调用的时候都会创建一个执行上下文。执行上下文分为两个阶段: 创建阶段和执行阶段。

 

一、创建阶段(编译阶段,函数被调用,但还没有执行函数内部代码)

创建阶段包括创建变量对象、创建作用域链、确定this指向。

 

1. 创建变量对象(variable object)

变量对象的属性包含当前执行上下文的arguments、变量、函数。

 

变量对象的创建过程:  检查当前执行上下文的参数,创建arguments对象 -> 检查当前执行上下文的函数声明,创建以该函数为名的属性,将其值指向函数所在内存地址的引用 -> 检查当前执行上下文的变量声明,创建以该变量为名的属性,将其值赋值为undefined.

这也就解释了为什么会发生变量声明提升。 

 

2. 创建作用域链

var a = 0;
function fun(){
    var a = 10;
    infun(){
        console.log(a); //10
    }
    infun();
}
fun();
console.log(a); //0

作用域链可以看作一个数组,在上面的例子中,infun()创建了一个作用域链scope[],并且将其执行上下文的变量对象vo(infun)放到作用域链的第0索引处scope[0],接着依次向上将fun函数的执行上下文的变量对象、全局执行上下文的变量对象放进数组,最后形成了scope[vo(infun), vo(fun), vo(global)] 这样的作用域链。

 

作用域链的作用就是保证变量对象的有序访问,js规定只有内层的变量对象可以访问作用域链外层的变量对象。

 

于是就有了闭包这个概念,当内层函数执行并访问了外层函数的变量时,就形成了闭包。闭包的一些特性有时候会帮助我们完成特殊的任务,比如创建私有变量,避免污染全局,阻止垃圾回收机制等,看个有趣的栗子:

var a=0,b=0;
function A(a){
  A=function(b){console.log(a+b++);}
  console.log(a);
}
A(1);//1
A(12);//13

js中有一个自动垃圾回收机制,会定期清除不再被引用的变量,并释放其内存,如果我们想保留某个变量的值使它避免被清除,就可以用到闭包的特性。其原理是当外部函数执行完毕退出时,执行环境栈即作用域链就会将其执行环境弹出并清除其中的变量对象,但是如果全局执行上下文中的变量对象仍拥有对其内部函数的引用,那么其内部函数中的变量对象就不会被清除,而在闭包的情况下,内部函数还拥有外部函数的变量对象的引用,所以外部函数中的变量对象也不会被清除。

 

上面的例子中,内部函数被赋值给全局变量A,还访问了外部函数A中的参数a,因此函数A中的变量对象不会被清除,第一次函数调用后的变量b值仍然为1,所以第二次调用结果为13,是不是很简单。

 

3. 确定this指向

每一个普通函数都绑定了一个this指针,指向一个对象。从这里可以看出,this的指向是在函数调用的时候确定的,那么程序是如何确定this的指向的呢,事实上,this始终指向函数直接被调用的对象,当然使用bind、call、apply以及箭头函数例外,在严格模式中函数调用的this的指向为undefined。来个简单的例子:

var a={
  b: function(){
    console.log(this); //Object a
  }
}

a.b()  

这里的b函数是由对象a调用,那么b函数中的this就是指向对象a,再看一个例子:

var a={
  b: function(){
    console.log(this);//Object window
  }
}

var fun = a.b;
fun();

为什么b函数中的this变成了window对象了呢,这里我们将b函数赋值给了变量fun, 然后直接调用了fun()函数,事实上,这只要在没有对象调用函数的情况下,函数内部的this都是指向window对象的。

 

当然还有一种情况,那就是使用new关键字调用函数创建实例时,this会指向这个新对象。说到这里,我们来看看new关键字执行时做了什么:

1. 创建一个新的空对象

2. 将this指针指向这个新的对象

3. 执行构造函数,为这个对象添加属性

4. 返回这个新对象

 

还有apply/call/bind,它们都可以改变函数的this绑定,具体使用看一下这个例子:

var a = {
  name: "cooco"
}
var b = function(m,n){
  console.log(m+" "+n+","+this.name);
}

b.call(a,"hello","world");  //hello world,cooco
b.apply(a,["hello","world"]); //hello world,cooco
b.bind(a,"hello","world")();//hello world,cooco

从这个例子里面可以看出,call/apply/bind都可以将函数的this指向第一个参数对象,并且向该函数中传递参数。call和apply都是直接返回函数的调用,可以直接执行出结果,而bind只返回一个函数,还需要进行调用才可以执行。而call和apply的区别只是参数传递的方式不同,apply函数的第二个参数是一个包含传递参数的数组,而call函数从第二个参数开始依次传入该函数。

 

OK,现在讲讲箭头函数,es6中的箭头函数的风格简单且不绑定this,通常是编写简单函数的更好选择,也就是说箭头函数是没有自己的this指针的,它只会从自己的作用域链的上一层继承this,看个例子:

function Person(){
  this.age = 0;

  setTimeout(() => {
    this.age++; 
    console.log(this.age);  //1
  }, 1000);
}

var p = new Person();

这是我在mdn给出的例子上稍微修改了一下的例子,在setTimeout函数中作为第一个参数的箭头函数,继承了其作用域链中的上一层Person函数的执行上下文中的this,正确指向了p实例。另外,如果通过call,apply调用箭头函数,则其第一个参数会被忽略, bind也一样。最后,箭头函数不绑定arguments对象。

 

二、执行阶段

执行阶段包括变量赋值、函数引用、执行其它代码。

在创建阶段,变量对象中的属性都不能被访问,而在进入执行阶段之后,变量对象转变为了活动对象,里面的属性都可以进行访问了。

 

最后还要提到的是执行环境栈,js引擎使用执行环境栈来处理所有的执行上下文,每当一个执行上下文被创建,都会被放入栈中,栈底永远都是全局执行上下文,栈顶就是当前正处于执行阶段的执行上下文。当栈顶的执行上下文执行结束,就会被弹出栈,接着执行下面的执行上下文。如果程序在运行中需要调用另一个函数,那么当前的执行上下文就会被挂起,然后创建一个新的执行上下文放入栈顶,等起执行结束再继续执行被挂起的执行上下文。

 

---更多文章请关注我的个人网站http://coocochen.com

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值