任何足够先进的技术都和魔法无异! ——Arthur C. Clarke
初步理解 this
this 是在函数被调用时发生的绑定,它指向什么完全取决于函数的调用位置(而不是声明位置)
- this 是在运行时进行绑定的,并不是在编写时绑定
- 随着你的使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this 则不会这样
看几个例子初步理解一下 this:
// 1. 为什么要使用 this
let me = {
name: "Kyle",
};
// 如果不使用 this,需要显式传入一个上下文对象
function identify(obj) {
return obj.name.toUpperCase();
}
identify(me); // KYLE
// 改进!
function identify() {
return this.name.toUpperCase();
}
// call 方法使 identify 中的 this 指向 me,相当于 me.name
identify.call(me); // KYLE
// 2. this 并不像我们所想的那样指向函数本身
function foo() {
this.count++; // this => window,自动创建了全局变量count
console.log(this.count); // NaN
}
foo.count = 2; // 此 count 非 this.count -- WTF?
foo();
// 3. 具名函数,可以通过函数名从函数内部引用自身
function foo() {
foo.count++;
console.log(foo.count); //3
}
foo.count = 2;
foo();
// 4. 可以强制 this 指向函数自身
function foo() {
this.count++;
console.log(this.count); //3
}
foo.count = 2;
foo.call(foo);
那么到底 this 指向的规律是什么呢~ ↓
this 全面解析
this 的指向大致可以分为以下 4 种(以及四种 this 的绑定规则)
- 作为普通函数调用 (默认绑定,指向全局对象)
- 作为对象的方法调用 (隐式绑定,指向对象本身)
- 使用 call 方法 或 apply 方法调用(显示绑定,直接指定 this 的绑定对象)
- 使用构造器调用(new 绑定,实例绑定到构造函数的 this)
1. 默认绑定
- 作为普通函数调用时,应用 this 的默认绑定,指向全局对象 window (浏览器环境下)
- 但要注意全局对象与全局变量的区别!ES6 中规则是不同的。(参见扩展)
// var 声明的全局变量,等于全局对象的属性(ES5)即 window.a = 1 。this 又指向全局对象 window。
var a = 1;
function foo() {
console.log(this.a); // 1
}
foo();
// 注意,而 ES6 中 let 声明的全局变量,不再属于全局对象的属性。this 还是指向全局对象,会返回的 undefined。
let a = 1;
function foo() {
console.log(this.a); // undefined
}
foo();
2. 隐式绑定
- 当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。
- 作为对象的方法调用时,方法中 this 指向对象本身
let obj = {
key: "Jack",
setKey: function (key) {
this.key = key;
},
getKey: function () {
console.log(this); // obj
return this.key; // 相当于 obj.name
},
};
obj.setKey("Tom");
obj.getKey(); // Tom
- 隐式绑定丢失 this
let obj = {
key: "Jack",
setKey: function (key) {
this.key = key;
},
getKey: function () {
console.log(this); // window
return this.key;
},
};
let f = obj.getKey; // f 是对 obj.getKey 的引用,但调用位置在全局
f(); // undefined
// 假如全局定义了该属性的话
var key = "122";
let f = obj.getKey;
f(); // 122
// 同上 let 定义了也不行,因为不是定义在 window 上的。
let key = "122";
let f = obj.getKey;
f(); // undefined
- 同理,回调函数会丢失 this
var obj = {
a: 2,
foo: function () {
console.log(this.a);
},
};
function doFoo(fn) {
fn();
}
var a = "global";
doFoo(obj.foo); // global
- setTimeout 会导致 this 丢失
var obj = {
a: 2,
foo: function () {
console.log(this.a); // global
},
};
var a = "global";
setTimeout(obj.foo, 100); // 100ms后函数执行,this指向window
// setTimeout内部实现伪代码
function setTimeout(fn, delay) {
// 等待 delay 毫秒
fn(); // 调用位置
}
// 再看一个例子
var obj = {
a: 2,
foo: function () {
setTimeout(function () {
console.log(this.a);
}, 100);
},
};
var a = "global";
obj.foo(); // global
obj.foo.call({ a: "global" }); // global(因为是foo函数进行的call绑定,而不是settimeout的匿名回调函数)
// 可以改成这样!
var obj = {
a: 2,
foo: function () {
let _this = this;
setTimeout(function () {
console.log(_this.a);
}, 100);
},
};
var a = "global";
obj.foo(); // 2
// 也可以将setTimeout的回调函数改为使用箭头函数形式
function foo() {
setTimeout(() => {
console.log("id:", this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 }); // 42
3. 显示绑定
可参考文章 《call、apply 和 bind 的用法与区别》
1. call()、apply()、bind() 方法
可以直接指定 this 的绑定对象,称为显式绑定。function.call(thisArg, arg1, arg2, ...) 第一个参数是一个对象,调用该函数将 this 绑定到该对象上。
function foo() {
console.log(this.a);
}
var obj = { a: 2 };
foo.call(obj); // 把 foo 的 this 绑定到 obj 上,并将参数传入foo
显式绑定仍然无法解决我们之前提出的丢失绑定问题。但是可以采用下面的方式 ↓↓↓
2. 硬绑定方案
function foo() {
console.log(this.a);
}
var obj = { a: 2 };
var bar = function () {
foo.call(obj);
};
bar(); // 2
// 之后如何调用函数 bar,它总会手动在 obj 上调用 foo
setTimeout(bar, 100); // 2
4. new 绑定
基本概念
- 构造函数/构造器:使用 new 操作符时,被调用的普通函数
- 包括内置对象函数在内的所有函数都可以用 new 来调用,这种函数调用被称为构造函数调用。也叫构造器调用
- 实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”
构造函数调用时,会自动执行下面的操作:
- 创建一个全新的对象
- 新对象会被执行[[原型]]连接
- 新对象会绑定到函数调用的 this
- 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象
function foo(a) {
this.a = a;
}
var bar = new foo(2); // bar 会绑定到 foo 的 this 上
console.log(bar.a); // 2
扩展:ES6 关于顶层对象
顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。但 ES6 做出了改变。
顶层对象
- 顶层对象/全局对象,在浏览器环境指的是 window 对象,在 Node 指的是 global 对象。ES5 之中,顶层对象的属性与全局变量是等价的。
- ES6 为了保持兼容性,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性;
- 但 let 命令、const 命令、class 命令声明的全局变量,不属于顶层对象的属性。
// 设置了顶层对象的属性,则a就成为了全局变量。很容易创建错误的全局变量。
window.a = 1;
a; // 1
// 全局变量b 由var声明,所以它也在全局对象上。
var b = 1;
window.b; // 1
// 但ES6做出了改变!它不再是顶层对象的属性。
let c = 1;
window.c; // undefined
关于全局的 this 指向
同一段代码为了能够在各种环境,都能取到顶层对象,可以使用 this 变量
- 全局环境下,this 返回全局对象。Node.js 模块中 this 返回的是当前模块,ES6 模块中 this 返回的是 undefined !(比如用 let 定义的变量并不挂在全局对象上,this 取值默认为 undefined)
- 函数里面的 this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this 会指向顶层对象。但是,严格模式下,这时 this 会返回 undefined。
let j = 1;
this.j; // undefined
var k = 1;
this.k; // 1
参考
《你不知道的 JavaScript》(上卷)