再探原型和闭包

最近看见一篇关于原型和闭包的博文,十分精彩,传送门:深入理解javascript原型和闭包(完结)-王福朋。看完有种茅舍顿开的感觉,之前自己总结的原型链和闭包,实在是有失偏颇,因此在参考这篇文章,重新整理了原型链和作用域链的知识,加以巩固。

<!--more-->

1. 原型链

1.1. 原型

所有的对象都是由其构造函数创建,基础对象最基本的构造函数是Obecjt()。
每个对象都有一个" __ proto__ "的属性,指向该对象构造函数的原型,该属性也被称为隐式原型。
每个函数都有一个"prototype"的属性,表示这个构造函数的原型,函数的原型实际上是一个对象,并且该对象有一个construct的属性,指向构造函数本身。

同时,由于函数也是对象,所有函数的构造函数都是Function()。函数的隐式原型就是Function()的显式原型。 甚至连Object()函数的构造函数都是Function(),假设存在构造函数Aoo()和其实例化对象a,存在下面的关系:

a.__proto__ == Foo.prototype; // 对象的隐式原型指向其构造函数的显式原型

Foo.__proto__ == Function.prototype; // 函数的构造函数是Function()

Foo.prototype.__proto__ == Object.prototype; // 原型也是一个对象,对象的基本构造函数是Object()

Object.__proto__ == Function.prototype; // Object()函数的构造函数也是Function()

Function.__proto__ == Function.prototype; // Function()函数是被自身创建的-_-

Function.prototype.__proto__ == Object.prototype; // Function()的原型也是对象

Object.prototype.__proto__ == null; // Object.prototype 是第一个对象,所有对象的基础属性和方法都源于此,所以他的隐式原型是null。

1.2. 继承

JS中的继承是通过原型链来体现的:访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着proto这条链向上找,这就是原型链。 如果将构造函数A的原型是构造函数B的实例对象,则A的实例对象就可以通过A.prototype获取到b的属性和方法(因为A.prototype == b)。 可以使用hasOwnProperty方法来判断一个属性到底是实例对象的还是其原型的。

可以通过使用instanceof来判断两个类是否存在继承关系,其规则为:
沿着左操作数的proto这条线来找,同时沿着右操作数的prototype这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回true。如果找到终点还未重合,则返回false。
通过上面的关系,就可以解释下面的问题了:

console.log(Object instanceof Function); // true 
console.log(Function instanceof Function); // true
console.log(Function instanceof Object); // true

2. 闭包

2.1. 环境上下文

环境上下文,分为全局上下文和函数上下文:

  • 全局上下文中的数据内容包括
    • 普通变量声明
    • 函数声明
    • this
  • 函数上下文中的数据内容除了上面三种情形,还包括
    • 参数
    • arguments
    • 自由变量(指不在该上下文中声明的变量)

所谓的执行环境上下文,指的是:在执行代码之前,解析器会进行变量声明提前,也就是说会把这段代码将要用到的所有变量都事先拿出来,有的直接赋值了,有的先用undefined占个空。

  • 变量、函数表达式声明,默认赋值为undefined占位;
  • this——赋值;
  • 函数语句声明——赋值;

2.2. 上下文栈

下面是代码执行的大致过程:

  1. 在加载程序时,已经确定了全局上下文环境以及执行上下文(变量声明提前等准备工作),并随着程序的执行对变量进行赋值。
  2. 在函数体的语句执行之前,arguments变量和函数的参数都已经被赋值。因此,函数每被调用一次,都会产生一个新的执行上下文环(即使是同一个函数(甚至是相同参数),在不同的调用下产生的上下文环境也是不一样的)。
  3. 当程序运行到将要调用函数的时候,会生成该函数的上下文环境,然后将此上下文压入上下文环境栈并设置为活动状态。
  4. 然后执行函数内部的代码,如果遇见函数调用则重复上述步骤:产生新的上下文->入栈并设置为活动状态->执行函数代码;
  5. 当这个函数调用完毕,则其上下文环境被销毁,从上下文环境栈弹出,此时程序又回到了其父作用域下的上下文环境,并在栈中将其置为活动状态
  6. 按照此过程进行,到最后上下文环境栈中只剩下了全局上下文环境(当然这是在没有闭包的情况下)

2.3. 函数内部的数据内容

上面提到,函数上下文中的数据内容,除了普通变量声明,函数声明和this之外,还多了参数,arguments对象和自由变量。

2.3.1. 自由变量

自由变量的定义:在fn函数中使用的变量x,却没有在A作用域中声明,对于fn函数作用域来说,x就是一个自由变量。
函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。

var a = 10;
function fn(){
    console.log(a);
}

function foo(f){
    var a = 20;
    f();
}

foo(fn); // 10

2.3.2. this

跟自由变量不同的是,函数内部的this取何值,是在函数真正被调用执行的时候确定的,因为函数定义的时候根本确定不了。

  • 如果函数作为构造函数用,那么其中的this就代表它即将new出来的对象,在原型链中,this代表的也都是当前对象的值;但是如果直接把构造函数当作普通函数调用,则其的this会变成window
  • 如果函数作为对象的一个属性时,并且作为对象的一个属性被调用时,函数中的this指向该对象;但是如果方法函数被赋值到了另一个变量中,并没有作为obj的一个属性被调用,那么this的值就是window,此时就无法在函数中使用this获取原对象的属性
  • 当一个函数被call和apply调用时,this的值就取传入的对象的值
  • 全局环境下,this永远是window,普通函数在调用时,其中的this也都是window
  • 闭包函数中的this也是window。

2.4. 作用域

JS中只有全局作用域和函数作用域,并没有“块级作用域”的概念,函数作用域是在函数定义时生成的。
作用域只是一个抽象的概念,其中没有变量。要通过作用域对应的执行上下文环境来获取变量的值。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。所以,如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中寻找变量的值。牢牢记住,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。

作用域内部声明的变量(包括参数)会覆盖掉外部的同名变量,这正是我们需要的。那么,程序是如何确定作用域下的某个上下文中所使用的自由变量呢?

前面提到过:函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。
在fn函数中,取自由变量x的值时,要到创建fn函数的那个作用域中取,无论fn函数将在哪里调用。 如果跨了一步,还没找到呢?接着跨!一直跨到全局作用域为止。要是在全局作用域中都没有找到,那就是真的没有了。

这个一步一步“跨”来寻找自由变量的路线,就是我们常说的“作用域链”。

2.5. 闭包函数

了解了作用域链,理解闭包就十分轻松了。关于闭包,有一个不那么精准的定义:一个作为函数返回值或者函数参数的函数。

// 作为函数返回值
function fn(){
    var a = 100;
    return function foo(x){
        if (x > a){
            console.log("more");
        }else {
            console.log("less or equal");
        }
    }
}
var a = 10;
var f = fn();
f(50); // "less or equal"

// 作为参数
var a = 10;
var fn = function(x){
    if (x > a){
        console.log("more");
    }else {
        console.log("less or equal");
    }
}

!(function(f){
    // 这里的闭包是形参f而不是实参fn,fn只是一个普通的函数表达式声明的函数
    var a = 100;
    f(50); // "more"
})(fn);

在前面的上下文栈中提到:当函数调用结束,其执行上下文将从上下文栈中弹出并被销毁,其中的变量也随之被销毁,这里的例外就是闭包。

由于闭包函数是在函数内部定义的函数(函数可以创建一个独立的作用域),因此如果在闭包中使用其定义函数上下文中的自由变量,或者在其他地方调用该函数,则必须保证其定义函数的执行上下文仍然存在(如果跟普通的函数一样调用结束就被销毁,则就无法找到其中的数据内容了。),所以如果函数上下文中存在闭包,则在调用结束之后不会被销毁,而是保存在上下文栈中(尽管活动状态已经被切换为上一层),而之所以说使用闭包会增加内存开销,就是这个原因。

总之,理解闭包,弄清楚环境上下文和作用域链是十分必要的,此外,牢记“函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域 ”。

3. 最后

这个世界上有无数优秀且勤奋的人,然而我并不是其中的一个。感谢这么多前辈愿意在互联网上分享自己的学习笔记和心得,我现在能做的就是抓紧学习,希望有朝一日也能为社区的发展贡献绵薄之力。加油吧!