this 关键字
# this 关键字
JavaScript 中的 this 关键字与其他语言略有不同,在绝大多数情况下,函数的调用方式决定了 this 的值(运行时绑定)。this 不能在执行期间被赋值,并且在每次函数被调用时 this 的值也可能不同。
ES5 引入了 bind 方法来设置函数的 this 值,而不用考虑函数是如何被调用的。同时,箭头函数不提供自身的 this 绑定。
在不同模式下,this 返回的值有所不同:
// 普通模式下,this 永远指向一个对象。默认为全局对象
function f1() {
return this;
}
// 在浏览器中
f1() === window; // true
// 在 Node 中:
f1() === globalThis; // true
2
3
4
5
6
7
8
9
10
// 在严格模式下,this 可以是任意值。如果没有设置,保持为 undefined
function f2() {
"use strict" // 严格模式
return this;
}
// 直接调用,没有在进行执行环境时设置 this 的值,保持 undefined
f1() === undefined; // true
// 通过 window 进入执行环境,此时 this 的值应为 window
window.f2() === window; // true
2
3
4
5
6
7
8
9
10
11
上面两个例子可以很好的说明 this 的用法以及指向。
# 对象上下文
当调用一个对象中的方法时,函数内的 this 是会绑定到对象上的。
var o = {
prop: 37,
f: function() {
return this.prop;
}
};
console.log(o.f()); // 37
2
3
4
5
6
7
8
# 函数上下文
一个对象(A)可以作为 bind、apply、call 函数的第一个参数绑定到另一个对象(B)上,此时对象(B)的 this 会指向对象(A)。如果没有指向,它默认就是 window 对象。
var obj = {a: 'Custom'};
var a = 'Global';
function whatsThis() {
return this.a; // this 的值取决于函数被调用的方式
}
whatsThis(); // 'Global' 因为在这个函数中 this 没有被设定,所以它默认为 全局/ window 对象
whatsThis.call(obj); // 'Custom' 因为函数中的 this 被设置为obj
whatsThis.apply(obj); // 'Custom' 因为函数中的 this 被设置为obj
2
3
4
5
6
7
8
9
10
# this 和对象转换
在非严格模式下,使用 call 和 apply 时,如果用作 this 的值不是对象,则会尝试将其转换为对象,null 与 undefined 全部被转换为全局对象。例如:7 => new Number(7):
function bar() {
console.log(Object.prototype.toString.call(this));
}
bar.call(7); // [object Number]
bar.call('foo'); // [object String]
bar.call(undefined); // [object global]
2
3
4
5
6
7
# 箭头函数
在箭头函数中,this 与封闭词法环境的 this 保持一致:
- 全局代码中,
this为全局对象 - 函数中,为当前调用对象
如果将 this 传递给 call、apply、 或 bind 来调用箭头函数,它将被忽略,不过仍然可以传递参数。最好的方法,第一个参数应该设置为 null。
// 刚接触到 JavaScript 的时候,对于这样的 this 绑定一定头疼过
var obj = {
bar: function() {
var x = (() => this);
return x;
}
}
var fn = obj.bar();
fn() === obj;
var fn2 = obj.bar;
fn2()() === window;
2
3
4
5
6
7
8
9
10
11
12
13
解析
obj 是一个对象,它内里面有一个 bar 属性,该属性绑定了一个匿名方法,这个匿名方法返回了一个 x 对象,该对象同样绑定了一个匿名的箭头函数,返回的一个 this,这个 this 指向的是 bar 所引用的匿名方法,这点要清楚。
当我们创建 fn 时,fn 绑定了 obj.bar(),这里直接执行了 bar(),是通过 obj 执行的,所以 bar 所绑定的匿名方法的 this,此时指向的是 obj,所以当执行 fn() 的时候,返回的自然是 obj,故 fn() === obj 为 true。
如果第一步清楚了,再看 fn2。它绑定的是 obj.bar,也就是说 fn2 节间绑定了 bar 所绑定的匿名函数,好,那么也表示 fn2 绑定了一个匿名函数。此时执行 fn2(),它应该返回 x 的内容,也就是 (() => this),但是 fn2() 是在全局下执行的,所以此时 fn2 的 this 应该是 window,那么再执行一次 fn2() 的结果,也就是 fn2()(),此时返回了 this,也就是 window。所以 fn2()() === window 也为 true。
# bind 方法
bind 会创建一个与调用者相同函数体和作用域的函数,并且将其永久地绑定到 bind 的第一个参数上,无论它后续是如何调用的。同时,bind 只生效一次。
function f() {
return this.a;
}
var a = "a";
var g = f.bind({a: "g"});
var h = f.bind({a: "h"});
console.log(g()); // g
console.log(h()); // h
2
3
4
5
6
7
8
9
10
11
# 手写 bind
手写 bind 方法需要注意几点:
- 它会返回一个新的函数,并且不执行
- 处理绑定的对象,如果没有应当绑定
window - 支持多个参数,使用列表元素填充参数列表
- 使用
new关键字进行构造时,应当有类的原型
Function.prototype.myBind = function() {
// 参数转列表
var args = Array.prototype.slice.call(arguments);
// 取出第一个参数,作为要 bind 的对象
var contenxt = args.shift();
// 保存当前 this
var self = this;
// 创建一个空方法
var Fn = function() {};
var result = function() {
// 获取执行时的参数
var rest = Array.prototype.slice.call(arguments);
self.apply(this instanceof Fn ? this : contenxt, args.concat(rest));
}
// 通过 new 创建的,重新绑定原型链
result.prototype = this.prototype;
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# call 方法与 apply 方法
这两个方法也可以改变 this 的指向,与 bind 不同的是,它们是立即执行方法。
它们之间的区别也很简单,call 接受的是一个参数列表,而 apply 接受的是一个包含多个参数的数组。
function.call(thisArg, arg1, arg2, ...)
function.apply(thisArg, [argsArray])
2
使用 call 或 apply 都可以改变 this 的指向,这在很多时候都非常有用。
# 手写 call
手写 call 方法需要注意的是,该方法接受的是一个参数列表:
Function.prototype.myCall = function(context) {
const ctx = context || window;
// 获取参数列表
var args = Array.prototype.slice.call(arguments, 1);
ctx.fn = this;
ctx.fn(...args);
delete ctx.fn;
}
2
3
4
5
6
7
8
# 手写 apply
手写 apply 方法需要注意的是,该方法接受的是一个包含多个参数的数组:
Function.prototype.myApply = function(context, arr) {
const ctx = context || window;
// 获取参数数组
if (arr && !Array.isArray(arr)) {
throw new TypeError("参数类型不正确");
}
ctx.fn = this;
ctx.fn(arr);
delete ctx.fn;
}
2
3
4
5
6
7
8
9
10
11
# 构造函数
当一个函数用作构造函数时,它的 this 被绑定到正在构造的新对象上。
# 类上下文
this 在类中的表现与函数类似,因为类本身也是一种函数,在构造过程中,所有费静态方法都会被添加到 this 原型中。
class Example {
constructor() {
const proto = Object.getPrototypeOf(this);
console.log(Object.getOwnPropertyNames(proto));
}
first(){}
second(){}
static third(){} // 静态方法不会添加到 this 中。它是类本身的方法 / 属性
}
new Example(); // ['constructor', 'first', 'second']
2
3
4
5
6
7
8
9
10
11
# 类中的 this
和普通函数一样,方法中的 this 值取决于它们何时被调用。
class Car {
constructor() {
// Bind sayBye but not sayHi to show the difference
this.sayBye = this.sayBye.bind(this);
}
sayHi() {
console.log(`Hello from ${this.name}`);
}
sayBye() {
console.log(`Bye from ${this.name}`);
}
get name() {
return 'Ferrari';
}
}
class Bird {
get name() {
return 'Tweety';
}
}
const car = new Car();
const bird = new Bird();
// The value of 'this' in methods depends on their caller
car.sayHi(); // Hello from Ferrari
bird.sayHi = car.sayHi;
bird.sayHi(); // Hello from Tweety
// For bound methods, 'this' doesn't depend on the caller
bird.sayBye = car.sayBye;
bird.sayBye(); // Bye from Ferrari
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 派生类
与普通基类不同,派生类的构造函数没有初始的 this 绑定,而是在构造函数中通过调用 super() 生成一个 this 绑定,从而达到派生的效果。
具体可以参考 super 关键字
# 原型链中的 this
对于在对象原型链上某处定义的方法,this 调用该方法就像在指向对象上调用一样。
var o = {
f: function() {
return this.a + this.b;
}
};
var p = Object.create(o);
p.a = 1;
p.b = 4;
console.log(p.f()); // 5
2
3
4
5
6
7
8
9
10
这里,对象 p 并没有 f 方法,但是其原型链中有,那么就可以直接使用,但原型链中没有 a 和 b 属性,this 可以始终指向 p,同时调用原型链上游的 f 属性,一切就像都在 p 中一样。(因为 this 始终是从 p 开始查找,所以它始终都会指向 p,这是原型链的特殊性)