JavaScript中的this与它的三个函数

430 阅读4分钟

this

this 机制是 JavaScript 特有的,它是为了解决在对象内部的方法中使用对象内部的属性的需求。

如果不使用 this 来实现对对象内部的属性 JavaScript 可以使用对象.属性访问到:

let people = {
	name: 'litangmm',
        logName: function(){
            console.log(people.name)
        }
}

这种方法,方法与对象强耦合。对象变量变化,方法必须接着改变。如果有一个匿名对象数组,那么对象位置改变,方法就得改。

let peoples = [
    {name:'litangmm',logName: function(){console.log(peoples[0].name)}},
    {name:'litangmm1',logName: function(){console.log(peoples[1].name)}},
    {name:'litangmm2',logName: function(){console.log(peoples[2].name)}},
]

于是,this 出现了。

What is This?

概念: this可以理解为一个特殊的变量。它和执行上下文绑定,在运行时才确定。this 的指向遵循以下几条规则:

  1. 在全局环境下,this 指向全局的 window。
  2. 作为函数执行时,如果是作为对象的函数执行,则指向该对象本身,否则指向全局的window。
  3. 1 2都是非严格模式下的,全局模式下,this 拿不到全局 window 而是 undefined

实验一下:

'use strict'; 
// console.log(that);
let people = {
    name: 'litangmm on people',
    logName,
}
function logName(){
    console.log(this.name)
}
people.logName(); 

下面就来分析一下代码运行的结果:

console.log(that); // Uncaught ReferenceError: that is not defined

学过简单的JavaScript语法都知道,要用一个变量,我们就得先定义它,否则代码会报错。那为什么this可以当做一个变量来用呢?那肯定是有人帮我们创建了它,就像我们直接用window|document一样。

let people = {
    name: 'litangmm on people',
    logName,
}
let people2 = {
    name: 'litangmm2 on people',
    logName,
}
function logName(){
    console.log(this.name)
}
people.logName();  // litangmm on people
people2.logName(); // litangmm2 on people

可以理解为,当代码执行时,执行器会在上下文中声明一个this变量,如果代码当前执行的是对象对象函数this将指向该对象,这样我们就能通过this访问到该对象的属性。

那我们可以改变this的指向吗? 当然,this是一个变量,JavaScript允许我们能够将任何值付给一个变量。

bind apply call 都是属于函数的原型方法,可以改变函数执行的 this 的指向。它们的利用了this的第2条原则:

作为函数执行时,如果是作为对象的函数执行,则指向该对象本身。

call 和 apply

执行效果

  1. 改变 thisarg
  2. 接收函数入参
  3. 执行函数

说明

  1. 对于 thisarg ,在非严格模式下,如果 thisarg 为 null 或 undefined ,thisarg 会为 全局 window。
  2. 对于函数入参,call 接收 的是多个参数值,apply接收的是一个参数列表。
// mycall.js
Function.prototype.mycall = function(thisarg,...args){
    let context = thisarg || window
    context.fn = this 
    const res = context.fn(...args)
    delete context.fn
    return res
}
function logNameAndAge(age, sex){
    console.log(this.name,age,sex)
}
logNameAndAge.mycall({name:"litangmm"},21,'man') // litangmm 21 man

context.fn = this 这里做了一件事,就是在 context 上定义了一个属性fn,赋值为 this 。**this是调用mycall的函数A。**这样,在下一行,函数A就作为 context 对象的对象函数被调用,函数A执行的this自然指向context了。

当然,这代码还是有优化空间的:

  1. 判断 this 是否为 函数。
  2. 如果传入的 thisarg 上有 fn 属性,那么该属性会被覆盖。

我们考虑这些情况,实现apply

// myapply.js
Function.prototype.myapply = function(thisarg,args){
    if(typeof this !== 'function'){
        throw new TypeError('error')
    }
    let context = thisarg || window
    const fn = Symbol()
    context.fn = this 
    const res = context.fn(...args)
    delete context.fn
    return res
}
function logNameAndAge(age, sex){
    console.log(this.name,age,sex)
}
logNameAndAge.myapply({name:"litangmm"},[21,'man'])  // litangmm 21 man

说明:

  1. 通过 typeof 判断调用的是否是一个函数。
  2. 通过 Symbol 来解决属性冲突。

bind

执行效果

  1. 改变 thisarg。
  2. 接收函数入参。
  3. 返回函数。 对于返回函数:
  4. 可以接收入参。
  5. 可以作为构造函数使用。
  6. 作为构造函数使用时,bind 绑定的 this 会被忽略。

难点

  1. bind 执行时接收可以接收参数(类似call接收多个参数),返回的函数还可以接收参数。
  2. 判断返回函数是作为构造函数被使用。
// mybind.js
Function.prototype.mybind = function(thisarg,...args1){
    if(typeof this !== 'function'){
        throw new TypeError('error');
    }
	const that = this;
    let fBound = function(...args2){
        return that.apply(this instanceof fBound ? this: thisarg,args1.concat(args2));
    }
    let fNop = function(){};
    fNop.prototype = that.prototype;
    fBound.prototype = new fNop();  
    return fBound;
}

bind 本质是一个函数装饰器:

  1. 参数拼接。
  2. 如果是普通调用,使用bind的this。
  3. 当做返回函数作为构造函数,则使用返回函数的this。

说明

之所以可以通过this instanceof F 来判断是被当作构造函数被使用的,可以参考new 的原理

  1. 创建一个空对象。

  2. 指定空对象的__proto__为构造函数的prototype

  3. 绑定该空对象到构造函数。

  4. 执行绑定后的构造函数。

  5. 如果构造函数有返回值,则返回返回值,否则返回对象。

!注意2步,因为返回的构造函数 是 fBound,而,我们希望得到的that上的原型,所以,有代码

fBound.prototype = that.prototype;	

这代码是存在问题的,因为fBound.prototype=== that.prototype === 原型对象地址,所以操作fBound.prototype 修改会改变 that.prototype。所以,我们可以使用原型链来实现fBoundthat 的连接。

let fNop = function(){};
fNop.prototype = that.prototype;
fBound.prototype = new fNop();   

这样,fBound.prototype 就是一个对象,对象的__proto__指向that.prototype。可以参考这篇文章

!注意3、4步,构造函数执行的时候,空对象的__proto__已经指向了构造函数的prototype。所以,绑定后的构造函数的this 是可以在原型链上找到构造函数的。