在JS中,有许多关键字。今天,我们就来一探究竟this这个关键字。
1. this存在的意义
我们先来通过一个例子认识一下this。
function identify(context) {
return context.name.toUpperCase()
}
function speak(context) {
var gretting = 'Hello, I am ' + identify(context)
console.log(gretting);
}
var me = {
name: 'Tom'
}
speak(me)
我们先定义了一个函数speak,里面var了一个变量gretting,值为'Hello, I am '。我们想让它输出'Hello, I am Tom'并且想让Tom变成大写。
那我们就再定义一个对象me,里面存一个key为name值为‘Tom’的属性,让它作为实参传给函数speak的形参context。
我们又想让‘Tom’变成大写的‘TOM’,于是我们又写了一个函数identify,让它返回context.name的大写。
我们来看看输出结果。
这样就达到了我们想要的效果。
但我们一眼看上去这段代码,我们用了好几个形参和实参,当代码和函数数量越来越多时,可能会造成混乱。有没有什么更简洁的方法呢?
我们可以用this关键字改造一下这段代码。
function identify() {
return this.name.toUpperCase()
}
function speak() {
var gretting = 'Hello, I am ' + identify.call(this)
console.log(gretting);
}
var me = {
name: 'Tom'
}
speak.call(me)
我们先来直观的感受一下this的作用,之后你就能看懂这段代码。这样也能达成一样的效果。
这就是this存在的一个重大意义:this 让函数可以自动引用合适的上下文对象。
接下来我们就来详细的剖析一下this。
2. this的指向
我们先在v8引擎中输出一下this,看看它的输出结果。
我们发现,直接在全局中输出这个this,它会指向window,也就是全局。
所以对于this,我们有两个结论:
- this 是一个代词,在JS中永远代指某一个域,且this只存在于域中才有意义
- this 在全局下指向的是 window
2.1 默认绑定
了解了这两个结论之后,我们来探一下this在其它场景下的指向。
function foo() {
let person = {
name: '阿美',
age: 18
}
console.log(this);
}
foo()
我们定义了一个函数foo,在它里面又定义一个对象person,给对象添加了两个属性,然后在foo里面输出this,在全局触发掉这个函数foo。
请问此时this指向的是谁?我们刚刚在全局试了一下输出this,发现它是指向全局的。那么在这里,我们在foo的作用域里输出了this,它会指向foo吗?我们来看一下效果。
我们发现,this指向的还是全局,说明this的指向规则远没有这么简单。那么它到底是怎么指向的呢?
我们再来扩展一下上面那段代码。
function foo() {
let person = {
name: '阿美',
age: 18
}
console.log(this);
}
function bar() {
let person = {
name: '管总',
age: 18
}
foo()
}
bar()
我们又写一个函数bar,里面同样定义一个对象person。注意此时我们把函数foo拿到函数bar中调用了,上一次我们是在全局中调用了函数foo,这次我们在函数作用域中调用foo会有什么不一样吗?this到底会指向谁呢?
我们发现this依旧指向的是全局。
这是为什么呢?其实this的指向完全取决于拥有它的函数是怎么被调用的。
什么意思呢?我们发现,在上面两段代码里,不管函数foo写在了哪个域里,他都是被独立调用的。独立调用指的是函数自己触发掉了,没有对象去调用它,如:obj.foo(),这就不叫独立调用。
当一个函数是独立调用的,它里面的this一定指向全局,与函数写在哪里没有关系。
所以this的第一种指向:默认绑定:函数独立调用,this指向window。
2.2 隐式绑定
那要是函数不是独立调用的,this是如何指向的呢?我们来看一下。
function foo() {
console.log(this);
}
const obj = {
a: 1,
foo: foo
}
obj.foo()
我们写了一个函数foo,里面输出this,我们说this就是属于函数foo的,然后又定义了一个对象obj,里面有两个属性:一个key为a值为1;另一个key为foo值为函数foo。此时函数foo就是属于对象obj的了,我们obj.foo()去调用这个函数,这就是函数的非独立调用。那么this会指向谁呢?
我们来看一下输出结果。
我们发现输出的this指向了对象obj,里面是一个a和函数foo。
这就是this的第二种指向:隐式绑定:当函数的引用有上下文对象时(当函数被某一个对象所拥有且调用),this指向该上下文对象。
这种隐式绑定有一个特点,叫隐式丢失。我们来看一下。
function foo() {
console.log(this);
}
const obj = {
a: 1,
foo: foo
}
const obj2 = {
a: 2,
obj: obj
}
obj2.obj.foo()
我们又定义了一个对象obj2,给它添加了两个属性:一个key为a值为2;一个key为obj值为对象boj。此时我们去调用函数foo,写成obj2.obj.foo()。那么此时,this指向谁呢?foo现在也是非独立调用。那么它是指向obj还是obj2呢?我们来看一下结果。
我们发现,this指向的还是obj。
这就是隐式绑定的特点:隐式丢失:当函数的引用有一连串的上下文对象,this指向最近的那个对象。俗称:就近原则。
2.3 显示绑定
我们再来介绍this的第三种指向:显示绑定。这种绑定方法可以人为的去改变this的指向。我们通过一个例子来看一下。
function foo() {
console.log(this.a);
}
var obj = {
a: 1,
}
foo()
我们写了一个函数foo,里面输出this.a。还写了一个对象obj,里面有个属性a值为1.然后在全局调用了这个函数。我们现在知道,此时foo触发的是默认绑定,因为它是独立调用的,所以this指向window,而在window中没有a,所以输出结果应该是undefined。
那如果我们想让this指向obj应该怎么做呢?我们这样做。
function foo() {
console.log(this.a);
}
var obj = {
a: 1,
}
foo.call(obj)
JS官方给我们提供了一个方法call,它能强行的把foo里面的this指向obj,就是这么简单粗暴。所以此时输出结果就为1了。
那如果函数foo有接收参数呢?我们就这样使用。
function foo(x, y) {
console.log(this.a, x + y);
}
var obj = {
a: 1,
}
foo.call(obj, 2, 3)
直接在方法call后面加上你要接收的参数。所以输出结果应该是1和5。
显示绑定还有两种方法可以使用。方法apply和方法bind。它们是怎么使用的呢?
function foo(x, y) {
console.log(this.a, x + y);
}
var obj = {
a: 1,
}
// foo.call(obj, 2, 3)
foo.apply(obj, [2, 3])
当函数foo有接收参数时,我们用数组接收参数放在apply里,输出结果还是1和5。再来看方法bind。
function foo(x, y) {
console.log(this.a, x + y);
}
var obj = {
a: 1,
}
// foo.call(obj, 2, 3)
// foo.apply(obj, [2, 3])
let bar = foo.bind(obj)
bar(2, 3)
用方法bind返回的是一个函数体,我们用一个变量bar去接受这个函数,然后调用这个函数。当函数foo有接收参数时,我们既可以在bind里面接收这个参数,也可以在bar里接收参数,而在bind里没找到它才会去bar里找,bind的优先级更高。
这就是this的第三种指向:显示绑定:call、apply、bind 显示的将函数的this绑定到一个对象上。
2.4 new 绑定
new这个关键字也能去绑定。
我们在原型那篇文章中提到过new创建实例对象的四个步骤:
1. 创建一个this对象
2. 让构造函数中的逻辑正常执行,这就相当于往this对象上添加属性
3. 让this对象上的__proto__= 构造函数的prototype
4. return this对象
但其实有5个步骤,因为当时没有提到this,所以省略了一个步骤。
我们再来分析一下new的实现原理,它是怎么去绑定this的呢?
function Person() {
this.name = '阿伟'
this.age = 18
}
let p = new Person()
console.log(p);
我们创建了一个构造函数Person,然后new调用这个构造函数创建一个实例对象p,这里有一个小细节要提。当构造函数返回的是引用类型(数组、函数、对象)时,new的执行结果就是这个引用类型的数据。
我们来分析一下new的执行原理。
function Person() {
// let obj = {
// name: '阿伟',
// age: 18
// }
// Person.call(obj)
// obj.__proto__ = Person.prototype
// 判断 Person() 值如果是引用类型,则采用
// return Person() instanceof 'object' ? Person() : obj
this.name = '阿伟'
this.age = 18
}
let p = new Person()
console.log(p);
首先new会在构造函数里创建一个空对象,我们姑且叫它obj,然后构造函数里的代码正常运行。this.name = '阿伟'、this.age = 18,我们想让属性name和age出现在obj里,所以我们就要让this指向obj,而this是属于Person的,所以在这里我们用显示绑定,让Person里的this指向obj。于是构造函数里的代码正常运行,对象obj里就多了两个属性。
然后让对象的隐式原型对于构造函数的显示原型,此时判断一下构造函数的返回结果,如果是引用类型,则采用。最后就返回对象obj。于是变量p就被赋值成一个实例对象了。
1. 创建一个空对象obj
2. 将构造函数里的this指向obj
3. 正常运行构造函数里的代码
4. obj的隐式原型等于构造函数的显示原型
5. 返回obj
这就是new在创建实例对象执行的改变this指向的操作。
它会让构造函数里的this指向实例对象
2.5 开头代码复盘
了解完这四种this指向后,我们再回到文章开头的那段代码,现在你一定能够看得懂了。
function identify() {
return this.name.toUpperCase()
}
function speak() {
var gretting = 'Hello, I am ' + identify.call(this)
console.log(gretting);
}
var me = {
name: 'Tom'
}
speak.call(me)
我们首先执行了speak.call(me),将speak中的this指向了me。所以speak中的this就为me。然后在speak里面,又执行了identify.call(this),此时speak中的this相当于me,将identify里面的this又指向了me,所以identify里的this就为me。所以在identify里可以去调用me。
3. 箭头函数
我们得再来介绍一下箭头函数,它有点特殊。我们来看一下。
function foo() {
let bar = function () {
let baz = () => {
console.log(this);
}
baz()
}
bar()
}
foo()
我们定义了一个函数foo,在函数foo里又定义了一个函数bar,在bar里又定义了一个箭头函数baz,在baz里面输出this。我们说过,要想搞清楚this的指向,首先得搞清楚this是属于谁的。而在这里,虽然this写在了baz里,但它并不是属于baz的,它是属于bar的。因为箭头函数是没有this的。bar是独立调用的,所以this指向全局。
所以箭头函数的第一个特点:箭头函数没有this,写在了箭头函数中的this指向外部非箭头函数的this。
let Foo = () => {
this.name = '廖总'
}
let foo = new Foo()
我们再来看这段代码,我们创建了一个函数Foo当构造函数去使用了。但我们刚刚说过,在箭头函数里没有this这个概念,而我们知道,在new的执行原理里就得用到this去给创建的空对象赋值。所以在这里,这段代码会报错,因为箭头函数里没有this,所以它就不能当作构造函数去使用。
箭头函数的第二个特点:箭头函数不能作为构造函数使用。
4. 方法call的源代码
我们知道,方法call可以强行改变this的指向。那么它到底是怎么实现的呢?
我们一起来写一下它的源代码。
function foo(x) {
console.log(this.a, x);
}
const obj = {
a: 1
}
Function.prototype.myCall = function () {
}
foo.myCall(obj, 2, 3, 4)
我们写一个函数myCall来充当官方的call。应该怎么写呢?
我们先用自己的话来分析一下这个过程。我们想用myCall这个方法去让foo中的this指向obj,那在这里是不是就得用到隐式绑定了。我们得想个办法让obj去调用foo,这样foo就是非独立调用了,foo中的this就会指向obj。最后我们还得移除掉obj上的foo,因为我们不是真的想在obj上加上foo,我们只是想用隐式绑定。
function foo(x) {
console.log(this.a, x);
}
const obj = {
a: 1
}
Function.prototype.myCall = function () {
// 将foo引用到obj上
// 让obj调用foo
// 移除obj上的foo
}
foo.myCall(obj, 2, 3, 4)
首先我们不知道myCall会接收多少个参数,所以在这里我们可以用‘...’加一个变量去接收myCall传来的参数,它叫rest参数,它会把传来的参数存在数组里。
这个数组的第一位就是obj,所以我们再用个变量去接收这个obj,数组剩下的那些值我们再用个数组接收。
function foo(x) {
console.log(this.a, x);
}
const obj = {
a: 1
}
Function.prototype.myCall = function (...args) {
// 将foo引用到obj上
// 让obj调用foo
// 移除obj上的foo
const context = args[0]
const arg = args.slice(1) // [2,3,4]
}
foo.myCall(obj, 2, 3, 4)
接下来我们就接得将foo引用到obj上,此时obj就是这个context了,然后我们这样写:context.fn = this,此时的this是不是指向foo?因为foo.myCall()中的myCall它是非独立调用的,如果myCall里有this它一定会指向foo,所以context.fn = this就可以将foo引用到obj上了。
然后因为foo中传了参数进来,我们就把参数传给context.fn,context.fn此时就是foo了。所以我们这么写:
function foo(x) {
console.log(this.a, x);
}
const obj = {
a: 1
}
Function.prototype.myCall = function (...args) {
// 将foo引用到obj上
// 让obj调用foo
// 移除obj上的foo
const context = args[0]
const arg = args.slice(1) // [2,3,4]
context.fn = this
const res = context.fn(...arg)
}
foo.myCall(obj, 2, 3, 4)
三个点可以将零散的量存放到数组里,它逆过来用又可以将数组里的值变成零散的量。我们将数组arg又转换成零散的量供foo去使用,然后用一个变量res去接收foo的返回结果。此时foo就相当于被obj调用了,foo是非独立调用,所以foo中的this就会指向obj,达到了我们的目的。
执行完了这些操作,我们就得把obj里的foo删除掉,然后返回res。
function foo(x) {
console.log(this.a, x);
}
const obj = {
a: 1
}
Function.prototype.myCall = function (...args) {
// 将foo引用到obj上
// 让obj调用foo
// 移除obj上的foo
const context = args[0]
const arg = args.slice(1) // [2,3,4]
context.fn = this
const res = context.fn(...arg)
delete context.fn
return res
}
foo.myCall(obj, 2, 3, 4)
这样我们就手写了一个call方法出来,输出结果应该是1和2才对,我们来看一下。
成功输出了1和2,这样,call的源代码就被我们搞懂了。apply和bind原理也大差不差。各位可以自行探索。