资讯专栏INFORMATION COLUMN

JS的闭包与this详解

fireflow / 2764人阅读

摘要:删除对匿名函数的引用,以便释放内存在匿名函数从中被返回后,它的作用域链被初始化为包含函数的活动对象和全局变量对象。闭包与变量我们要注意到,闭包只能取到任意变量的最后值,也就是我们保存的是活动对象,而不是确定值。

工作中会遇到很多 this对象 指向不明的问题,你可能不止一次用过 _self = this 的写法来传递this对象,它每每会让我们觉得困惑和抓狂,我们很可能会好奇其中到底发生了什么。

一个问题

现在先来看一个具体的问题:

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

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

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

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]]属性中的对象构建起执行环境的作用域链。此后,又有一个活动对象(变量对象)被创建并被推入执行环境作用域链的前端。此时作用域链包含两个变量对象:本地活动对象和全局变量对象。显然,作用域链本质上是一个指向变量对象的指针列表,它只引用但不包含实际的变量对象。

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

闭包

但是,闭包的情况有所不同,在一个函数内部定义的函数会将外部函数的活动对象添加到它的作用域链中去。

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

关于严格模式还需要注意的是,它的作用范围只有当前的函数或者

阅读需要支付1元查看
<