JS的闭包与this详解

工做中会遇到不少 this对象 指向不明的问题,你可能不止一次用过 _self = this 的写法来传递this对象,它往往会让咱们以为困惑和抓狂,咱们极可能会好奇其中到底发生了什么。前端

一个问题

如今先来看一个具体的问题:git

var name = 'The Window';
var obj = {
    name: 'My obj',
    getName: function() {
        return this.name;
    }
};

// 猜想下面的输出和背后的逻辑(非严格模式下)
object.getName();
(object.getName)();
(object.getName = object.getName)();

若是上面的三个你都能答对并知道都发生了什么,那么你对JS的this了解的比我想象的要多,能够跳过这篇文章了,若是没答对或者不明白,那么这篇文章会告诉你并帮你梳理下相关的知识。
它们的答案是:github

object.getName();    // 'My Obj'
(object.getName)();    // 'My Obj'
(object.getName = object.getName)();    // 'The Window'

函数的做用域

在函数被调用的时候,会建立一个执行环境及相应的做用域链,而后,使用arguments以及其余命名参数的值来初始化函数的活动对象(activation object,简称AO)。在做用域上,函数会逐层复制自身调用点的函数属性,完成做用域链的构建,直到全局执行环境。浏览器

function compare(value1, value2) {
    return value1 - value2;
}

var result = compare(5, 10);

图片描述

在这段代码中,result经过var进行了变量声明提高,compare经过function函数声明提高,在代码执行以前咱们的全局变量对象中就会有这两个属性。安全

每一个执行环境都会有一个变量对象,包含存在的全部变量的对象。全局环境的变量对象始终存在,而像compare函数这样的局部环境的变量对象,则只在函数执行的过程当中存在。当建立compare()函数时,会建立一个预先包含全局变量对象的做用域链,这个做用域链保存在内部的[[Scope]]属性中。闭包

在调用compare函数时,会为它建立一个执行环境,而后复制函数的[[scope]]属性中的对象构建起执行环境的做用域链。此后,又有一个活动对象(变量对象)被建立并被推入执行环境做用域链的前端。此时做用域链包含两个变量对象:本地活动对象和全局变量对象。显然,做用域链本质上是一个指向变量对象的指针列表,它只引用但不包含实际的变量对象。app

当访问函数的变量时,就会从做用域链中搜索。当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局做用域。函数

闭包

可是,闭包的状况有所不一样,在一个函数内部定义的函数会将外部函数的活动对象添加到它的做用域链中去。oop

function create(property) {
    return function(object1, object2) {
        console.log(object1[property], object2[property]);
    };
}

var compare = create('name');
var result = compare({name: 'Nicholas'}, {name: 'Greg'}); // Nicholas Greg

// 删除对匿名函数的引用,以便释放内存
compare = null;

在匿名函数从create()中被返回后,它的做用域链被初始化为包含create()函数的活动对象和全局变量对象。这样,该匿名函数就能够访问create中定义的全部遍历,更为重要的是当create()函数执行完毕后,其做用域链被销毁,可是活动对象不会销毁,由于依然被匿名函数引用。当匿名函数别compare()被销毁后,create()的活动对象才会被销毁。性能

图片描述

闭包与变量

咱们要注意到,闭包只能取到任意变量的最后值,也就是咱们保存的是活动对象,而不是肯定值。

function create() {
    var result = [];
    for (var i = 0; i < 10; i++) {
        result[i] = function() {
            return i;
        };
    }
    return result;
}

create()[3](); // 10

咱们经过闭包,让每个result的元素都可以返回i的值,可是闭包包含的是同一个活动对象i,而不是固定的1-10的值,因此返回的都是10。但咱们能够经过值传递的方式建立另一个匿名函数来知足咱们的需求。

function create() {
    var result = [];
    for (var i = 0; i < 10; i++) {
        // 经过值传递的方式固定i值
        result[i] = function(num) {
            // 这里闭包固定后的i值,即num值,来知足咱们的需求
            return function() {
                return num;
            };
        }(i);
    }
    return result;
}

create()[3](); // 3

闭包与this

咱们知道this对象是基于函数的执行环境绑定的,在全局的时候,this等于window,而当函数做为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具备全局性,所以this经常指向window。

var name = 'The Window';
var obj = {
    name: 'My obj',
    getName: function() {
        return function() {
            return this.name;
        };
    }
};

obj.getName()(); // 'The Window'

前面说过,函数在被调用时会自动取得两个特殊变量: this和arguments,内部函数在搜索这两个变量时,只会搜索到其活动对象,因此永远不会访问到外部函数的这两个变量。若是咱们想知足需求,能够固定this对象并改名便可。

var name = 'The Window';
var obj = {
    name: 'My obj',
    getName: function() {
        // 固定this对象,造成闭包,防止跟特殊的this重名
        var that = this;
        return function() {
            return that.name;
        };
    }
};

obj.getName()(); // 'My obj'

this的绑定

上面对this的说明能够说是很是的浅薄了,如今咱们详细的整理下this关键字,它是函数做用域的特殊关键字,进入函数执行环境时会被自动定义,实现原理至关于自动传递调用点的对象:

var obj = {
    name: 'Nicholas',
    speak() {
        return this.name;
    },
    anotherSpeak(context) {
        console.log(context.name, context === this);
    }
};

obj.name;    //'Nicholas'
obj.speak();    // 'Nicholas'
obj.anotherSpeak(obj);    // 'Nicholas' true

能够看到,咱们在anotherSpeak()中传递的context就是obj,也就是函数调用时,执行环境的this值。引擎的这种实现简化了咱们的工做,自动传递调用点的环境对象做为this对象。

咱们要注意的是this只跟调用点有关,而跟声明点无关。这里你须要知道调用栈,也就是使咱们到达当前执行位置而被调用的全部方法的栈,即全部嵌套的函数栈。

function baz() {
    // 调用栈是: `baz`
    // 咱们的调用点是global scope(全局做用域)

    console.log( "baz" );
    bar(); // <-- `bar`的调用点
}

function bar() {
    // 调用栈是: `baz` -> `bar`
    // 咱们的调用点位于`baz`

    console.log( "bar" );
    foo(); // <-- `foo`的调用点
}

function foo() {
    // 调用栈是: `baz` -> `bar` -> `foo`
    // 咱们的调用点位于`bar`

    console.log( "foo" );
}

baz(); // <-- `baz`的调用点

咱们整理了四种this对象绑定的规则:

默认绑定

function foo() {
    console.log( this.a, this === window );
}
var a = 2;

window.a;    // 2
foo();    // 2 true

在这种规则下,函数调用为独立的毫无修饰的函数引用调用的,此时foo的调用环境就是全局环境window,因此this就指向window,而在全局下声明的全部对象都属于window,致使结果为2。

可是在严格模式下,this不会被默认绑定到全局对象。MDN文档上写到:

第一,在严格模式下经过this传递给一个函数的值不会被强制转换为一个对象。对一个普通的函数来讲,this总会是一个对象:无论调用时this它原本就是一个对象;仍是用布尔值,字符串或者数字调用函数时函数里面被封装成对象的this;仍是使用undefined或者null调用函数式this表明的全局对象(使用call, apply或者bind方法来指定一个肯定的this)。这种自动转化为对象的过程不只是一种性能上的损耗,同时在浏览器中暴露出全局对象也会成为安全隐患,由于全局对象提供了访问那些所谓安全的JavaScript环境必须限制的功能的途径。因此对于一个开启严格模式的函数,指定的this再也不被封装为对象,并且若是没有指定this的话它值是undefined。

function foo() {
    "use strict";
    console.log( this );
}

foo();    // undefined

关于严格模式还须要注意的是,它的做用范围只有当前的函数或者<script>标签内部,而不包括嵌套的函数体:

function foo() {
    console.log( this.a );
}

var a = 2;

(function(){
    "use strict";

    foo(); // 2
})();

隐含绑定

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};

obj.foo(); // 2

在这个函数调用时,其调用点为环境对象obj,因此函数执行时,this指向obj。

须要注意多重嵌套的函数引用,在调用时只考虑最后一层:

function foo() {
    console.log( this.a );
}
var obj2 = {
    a: 42,
    foo: foo
};
var obj1 = {
    a: 2,
    obj2: obj2
};

obj1.obj2.foo(); // 42

若是函数并不直接执行,而是先引用后执行,那么咱们应该明白,该变量得到的是另外一个指向该函数对象的指针,而脱离了引用的环境,因此天然失去了this的绑定,这被称为隐含绑定的丢失

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
// 函数引用!其实获得的是另外一个指向该函数的指针,脱离了obj环境
var bar = obj.foo;

var a = "oops, global";

bar(); // "oops, global"

明确绑定

咱们除了上面的两种默认绑定方式,还能够对其进行明确的绑定,主要经过函数内置的call/apply/bind方法,经过它们能够指定你想要的this对象是什么:

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2
};

foo.call( obj ); // 2

咱们给foo的调用指定了obj做为它的this对象,因此this.a即obj.a,结果为2。

call/apply方法须要传递一个对象,若是你传递的为简单原始类型值null,undefined,则this会指向全局对象。若是传递的为基本包装对象,则this会指向他们的自动包装对象,即new String(), new Boolean(), new Number(),这个过程称为封箱(boxing)。

这里咱们应该清楚call/apply方法都只在最后一层嵌套生效,因此咱们称呼它为明确绑定:

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2
};

var bar = function() {
    foo.call( obj );
};

// `bar`将`foo`的`this`硬绑定到`obj`, 因此它不能够被覆盖
bar.call( window ); // 2

但若是咱们想复用并返回一个新函数,并固定this值时,能够这样作:

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}

// 简单的`bind`帮助函数
function bind(fn, obj) {
    return function() {
        return fn.apply( obj, arguments );
    };
}

var obj = {
    a: 2
};

var bar = bind( foo, obj );

var b = bar( 3 ); // 2 3
console.log( b ); // 5

这种方式被称为硬绑定,也是明确绑定的一种,这个函数在被建立时就已经明确的声明了做用域,也就是该对象被放置在了[[Scope]]属性里。这种方式有时很经常使用,因此被内置在ES5后的版本里,其内部实现(Polyfill低版本补丁)为:

if (!Function.prototype.bind) {
    Function.prototype.bind = function(oThis) {
        if (typeof this !== "function") {
            // 可能的与 ECMAScript 5 内部的 IsCallable 函数最接近的东西
            throw new TypeError( "Function.prototype.bind - what " +
                "is trying to be bound is not callable"
            );
        }

        var aArgs = Array.prototype.slice.call( arguments, 1 ),
            fToBind = this,
            fNOP = function(){},
            fBound = function(){
                return fToBind.apply(
                    (
                        this instanceof fNOP &&
                        oThis ? this : oThis
                    ),
                    aArgs.concat( Array.prototype.slice.call( arguments ) )
                );
            }
        ;

        fNOP.prototype = this.prototype;
        fBound.prototype = new fNOP();

        return fBound;
    };
}

在ES6里,bind()生成的硬绑定函数拥有一个name属性,源自于目标函数,此时显示为bound foo

在一些语言内置的函数里,提供了可选参数做为函数执行时的this对象,这些函数的内部实现方式和bind()相似,也是经过apply/call来明确绑定了你传递的参数做为this对象,如:

var obj = {
    name: 'Nicholas'
};
[1,2,3].forEach(function(item) {
    console.log(item, this.name);
}, obj);
// 1 "Nicholas"
// 2 "Nicholas"
// 3 "Nicholas"

new绑定

new操做符会调用对象的构造器函数来初始化类成为一个实例。它的执行过程为:

  1. 一个全新的对象被凭空建立
  2. 这个新构造的对象被接入原型链(__proto__指向该构造函数的prototype)
  3. 这个新构造的对象被绑定为函数调用的this对象
  4. 除非函数返回一个其它对象,这个被new调用的函数将返回这个新构建的对象。

实质上加new关键字和()只不过是该函数的不一样调用方式而已,前者为构造器调用,后者为执行调用,在调用过程当中,this指向不一样,返回值不一样。

在new绑定的规则中,this指向新建立的对象。

箭头函数绑定

如今咱们看一个十分特别的this绑定,ES6中加入的箭头函数,前面的四种都是函数执行时经过调用点确认this对象,而箭头函数是在词法做用域肯定this对象,即在词法解析到该箭头时为该函数绑定this对象为当前对象:

function foo() {
    setTimeout(() => {
        // 这里的`this`是词法上从`foo()`采用
        console.log( this.a );
    },100);
}

var obj = {
    a: 2
};

foo.call( obj ); // 2

绑定的优先级

经过一些具体的实例对比,咱们能够得出不一样绑定方式的优先级:
new绑定 > 明确绑定 > 隐含绑定 > 默认绑定

箭头函数属于词法做用域绑定,因此其优先级更高,可是跟上面的不冲突。

最初的问题

如今咱们再来看下最初的问题:

var name = 'The Window';
var obj = {
    name: 'My obj',
    getName: function() {
        return this.name;
    }
};

// 猜想下面的输出和背后的逻辑(非严格模式下)
obj.getName();    // 'My obj'
(obj.getName)();    // 'My obj'
(obj.getName = obj.getName)();    // 'The Window'

咱们能够看出第一个直接绑定this对象为obj,第二个加上括号好像是引用了一个函数,但object.getName(object.getName)定义一致,因此this依然指向obj;第三个赋值语句会返回函数自己,因此做为匿名函数来执行,就会返回'The Window'。

参考资料

  1. 简书 - this与对象原型: http://www.jianshu.com/p/11d8...
  2. MDN - bind: https://developer.mozilla.org...
  3. Github - 深刻变量对象:https://github.com/mqyqingfen...
  4. JS高级程序设计:第五章(引用类型),第七章(函数表达式)
相关文章
相关标签/搜索