JavaScript 函数中的外部变量——理解 this

3,529 阅读6分钟
原文链接: my.oschina.net
js 中的 this 指向确实是个坑,网上有人轰轰烈烈地讨论它,讨论 js 闭包,其实并没有那么玄学,让我们一点点剥开它的面纱。 很多知识性内容来自 [邱桐城《JavaScript 中的 this》](https://zhuanlan.zhihu.com/p/24107744) 的启发,基于他的文章,我写下了我的总结。 ### 在全局作用域下 在浏览器环境下: ```js console.log(this); // Window { .. } this === window; // true ``` 全局作用域下,this 指向 Window 对象,这很好理解,仍然是传统 js 的结果。 在 node 环境下: ```js console.log(this); // global this === global; // true ``` 全局作用域下,this 指向 global 对象。 严格模式,在 node 环境下: ```js 'use strict'; console.log(this); // {} ``` 遵循严格模式的规范,this 不再指向全局对象。 ### 函数对象作用域下 ```js function foo() { console.log(this); } foo(); // global / Window ``` 严格模式,在 node 环境下: ```js 'use strict'; function foo() { console.log(this); } foo(); // undefined ``` 经过我的测试,虽然满足了规范要求,但在 node 7.2.0 下仍然出现了如上所示的不一致结果。 ### 对象方法作用域下 ```js let obj = { foo: function() { console.log(this); } }; obj.foo(); // { foo: [Function] } // obj 的值实际上是个匿名类的对象,foo 的值实际上是个匿名函数 ``` 作为对象方法时,this 指向该对象。 ```js function func() { console.log(this); } let obj = { foo: func }; obj.foo(); // { foo: [Function func] } let foo1 = obj.foo; foo1(); // global ``` 注意到:在函数体内使用的、在函数体外定义(声明)的变量,是 `传引用` 的。 你可能对这个例子印象深刻: ```js var foos = []; for (var i = 0; i < 3; ++i) { foos.unshift(function () { console.log(i); }); } console.log(i); // 3 for (var j in foos) { ++i; console.log(i); // 4 5 6 foos[j](); // 4 5 6 } ``` i 变量在函数内是外部变量的引用,所以当函数外的 i 值变化了,函数内的 i 值也一同变化。 避免这样的外部变量引用也很简单,使用 `const`、`let` 这样的新关键字是最简单的一种,它们比传统的 `var` 有着更严谨的定义域,**使该变量无法被运行时上下文访问到**而保证其值不被替换: ```js const foos = []; for (let i = 0; i < 3; ++i) { foos.unshift(function () { console.log(i); }); } for (let i in foos) { foos[i](); } // 0 1 2 ``` 并不是新的关键字解决了该问题,而是新的关键字拥有更严谨的作用域。如果该变量在运行时上下文中仍能访问,那问题依旧: ```js const foos = []; let i; for (i = 0; i < 3; ++i) { foos.unshift(function () { console.log(i); }); } for (let j in foos) { foos[j](); } // 3 3 3 ``` 看过上面的例子,你应该明白,函数内的 this 也是这样一个外部变量的引用。在传统使用 `function` 关键字的语法中,确实如此;但在 ES6 引入的新式 `=>` 语法中,事情不一样了,你可以看做在函数内插入了这么一行伪代码: ```js // 这不是真正的 js 代码 const this = outer.this; ``` 试看一例: ```js 'use strict'; const obj1 = { foo: function() { console.log(this); return () => { console.log(this); }; } }; const foo1 = obj1.foo(); // { foo: [Function: foo] } foo1(); // { foo: [Function: foo] } const obj2 = { foo: function() { console.log(this); return function () { console.log(this); }; } }; const foo2 = obj2.foo(); // { foo: [Function: foo] } foo2(); // undefined ``` `=>` 语法定义的函数对象,其 (const) this 指向**定义时**的上下文的 this,而不像 `function` 关键字定义的函数对象,其 (var) this 会跟随外部 this 的变化而变化。 ### 在构造函数对象作用域下(使用 `new` 关键字) ```js 'use strict'; function A() { console.log(this); } var a = new A(); // A {} console.log(a); // A {} var b = A(); // undefined console.log(b); // undefined ``` 构造函数中 this 指向其构造出来的对象,但是一定不要忘记使用 `new` 关键字。 ### call / apply / bind js 中的函数对象,其 prototype 中定义了如下三个函数: ```js func.call(thisArg[, arg1[, arg2[, ...]]]); ``` 执行函数 func,使用第一个参数作为 this,其他参数作为 func 的实参,一一对应。 ```js func.apply(thisArg[, [arg1, arg2, ...]]); ``` 执行函数 func,使用第一个参数作为 this,第二个参数为数组,数组中的每个元素作为 func 的实参,一一对应。 ```js var foo = func.bind(thisArg[, arg1[, arg2[, ...]]]); ``` 绑定 func 的 this 和所有参数,返回一个新的函数,但不执行它。 bind 的 this 对 `new` 关键字无效,但其他实参有效: ```js function A(name) { console.log(this.name); this.name = name; console.log(this.name); } var obj = { name: "obj" }; var B = A.bind(obj, "B"); var b = new B('b'); // undefined B console.log(obj.name); // obj ``` 要注意,`=>` 语法下的 this 不受影响,该语法下 this 视为 const 变量,不接受修改。

js 中的 this 指向确实是个坑,网上有人轰轰烈烈地讨论它,讨论 js 闭包,其实并没有那么玄学,让我们一点点剥开它的面纱。

很多知识性内容来自 邱桐城《JavaScript 中的 this》 的启发,基于他的文章,我写下了我的总结。

在全局作用域下

在浏览器环境下:

console.log(this);
// Window { .. }
this === window;
// true

全局作用域下,this 指向 Window 对象,这很好理解,仍然是传统 js 的结果。

在 node 环境下:

console.log(this);
// global
this === global;
// true

全局作用域下,this 指向 global 对象。

严格模式,在 node 环境下:

'use strict';
console.log(this);
// {}

遵循严格模式的规范,this 不再指向全局对象。

函数对象作用域下

function foo() {
    console.log(this);
}
foo();
// global / Window

严格模式,在 node 环境下:

'use strict';
function foo() {
    console.log(this);
}
foo();
// undefined

经过我的测试,虽然满足了规范要求,但在 node 7.2.0 下仍然出现了如上所示的不一致结果。

对象方法作用域下

let obj = {
    foo: function() {
        console.log(this);
    }
};
obj.foo();
// { foo: [Function] }
// obj 的值实际上是个匿名类的对象,foo 的值实际上是个匿名函数

作为对象方法时,this 指向该对象。

function func() {
    console.log(this);
}
let obj = {
    foo: func
};
obj.foo();
// { foo: [Function func] }

let foo1 = obj.foo;
foo1();
// global

注意到:在函数体内使用的、在函数体外定义(声明)的变量,是 传引用 的。

你可能对这个例子印象深刻:

var foos = [];
for (var i = 0; i < 3; ++i) {
    foos.unshift(function () {
        console.log(i);
    });
}
console.log(i);
// 3
for (var j in foos) {
    ++i;
    console.log(i);
    // 4 5 6
    foos[j]();
    // 4 5 6
}

i 变量在函数内是外部变量的引用,所以当函数外的 i 值变化了,函数内的 i 值也一同变化。

避免这样的外部变量引用也很简单,使用 constlet 这样的新关键字是最简单的一种,它们比传统的 var 有着更严谨的定义域,使该变量无法被运行时上下文访问到而保证其值不被替换:

const foos = [];
for (let i = 0; i < 3; ++i) {
    foos.unshift(function () {
        console.log(i);
    });
}
for (let i in foos) {
    foos[i]();
}
// 0 1 2

并不是新的关键字解决了该问题,而是新的关键字拥有更严谨的作用域。如果该变量在运行时上下文中仍能访问,那问题依旧:

const foos = [];
let i;
for (i = 0; i < 3; ++i) {
    foos.unshift(function () {
        console.log(i);
    });
}
for (let j in foos) {
    foos[j]();
}
// 3 3 3

看过上面的例子,你应该明白,函数内的 this 也是这样一个外部变量的引用。在传统使用 function 关键字的语法中,确实如此;但在 ES6 引入的新式 => 语法中,事情不一样了,你可以看做在函数内插入了这么一行伪代码:

// 这不是真正的 js 代码
const this = outer.this;

试看一例:

'use strict';

const obj1 = {
    foo: function() {
        console.log(this);
        return () => {
            console.log(this);
        };
    }
};
const foo1 = obj1.foo();
// { foo: [Function: foo] }
foo1();
// { foo: [Function: foo] }

const obj2 = {
    foo: function() {
        console.log(this);
        return function () {
            console.log(this);
        };
    }
};
const foo2 = obj2.foo();
// { foo: [Function: foo] }
foo2();
// undefined

=> 语法定义的函数对象,其 (const) this 指向定义时的上下文的 this,而不像 function 关键字定义的函数对象,其 (var) this 会跟随外部 this 的变化而变化。

在构造函数对象作用域下(使用 new 关键字)

'use strict';

function A() {
    console.log(this);
}

var a = new A();
// A {}
console.log(a);
// A {}

var b = A();
// undefined
console.log(b);
// undefined

构造函数中 this 指向其构造出来的对象,但是一定不要忘记使用 new 关键字。

call / apply / bind

js 中的函数对象,其 prototype 中定义了如下三个函数:

func.call(thisArg[, arg1[, arg2[, ...]]]);

执行函数 func,使用第一个参数作为 this,其他参数作为 func 的实参,一一对应。

func.apply(thisArg[, [arg1, arg2, ...]]);

执行函数 func,使用第一个参数作为 this,第二个参数为数组,数组中的每个元素作为 func 的实参,一一对应。

var foo = func.bind(thisArg[, arg1[, arg2[, ...]]]);

绑定 func 的 this 和所有参数,返回一个新的函数,但不执行它。

bind 的 this 对 new 关键字无效,但其他实参有效:

function A(name) {
    console.log(this.name);
    this.name = name;
    console.log(this.name);
}
var obj = {
    name: "obj"
};
var B = A.bind(obj, "B");
var b = new B('b');
// undefined B
console.log(obj.name);
// obj

要注意,=> 语法下的 this 不受影响,该语法下 this 视为 const 变量,不接受修改。


本文对你有帮助?欢迎扫码加入前端学习小组微信群: