畅谈this的四种绑定方式

975 阅读7分钟

前置知识

源起

首先来复现下词法作用域、作用域链以及闭包,有如下代码:


var bar = {
    myName:"baidu.com",
    printName: function () {
        console.log(myName)
    }    
}
function foo() {
    let myName = "百度云"
    return bar.printName
}
let myName = "百度"
let _printName = foo()
_printName()
bar.printName()

相信你已经知道了,在 printName 函数里面使用的变量 myName 是属于全局作用域下面的,所以最终打印出来的值都是“百度”。这是因为 JavaScript 语言的作用域链是由词法作用域决定的,而词法作用域是由代码结构来确定的

不过按照常理来说,调用bar.printName方法时,该方法内部的变量 myName 应该使用 bar 对象中的,因为它们是一个整体。

所以在对象内部的方法中使用对象内部的属性是一个非常普遍的需求。但是 JavaScript 的作用域机制并不支持这一点,基于这个需求,JavaScript 又搞出来另外一套 this 机制。所以,在 JavaScript 中可以使用 this 实现在 printName 函数中访问到 bar 对象的 myName 属性了。具体操作:

printName: function () {
        console.log(this.myName)
    }    

注意作用域链和 this 是两套不同的系统,它们之间基本没太多联系。

this 是什么?

当一个函数被调用,会创建一个执行上下文,这个执行上下文会包含函数在哪里被调用、函数的调用方式、传入的参数等信息。this就是一个执行上下文的一个属性,会在函数执行的过程中用到。

this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this。

执行上下文主要分为三种:

  • 全局执行上下文
  • 函数执行上下文
  • eval 执行上下文

所以对应的 this 也只有这三种:

  • 全局执行上下文中的 this
  • 函数中的 this
  • eval 中的 this

this绑定出现的时机

this 是在运行时进行绑定的,并不是编写时绑定的。也就是 this 是在函数被调用时发生的绑定。它的指向是什么,完全取决于函数在哪里被调用。this在任何情况下都不指向函数的词法作用域

绑定规则

默认绑定

默认绑定时,使用严格模式,不能讲全局对象用于默认绑定,this会绑定到undefined

function foo() {
  console.log(this)
}
foo() // window

严格模式(strict mode):

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

foo() // undefined

隐式绑定

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。

本质上,判断隐式绑定时,必须是一个对象内部包含一个指向函数的属性,通过这个属性间接的引用函数,从而把this间接(隐式)绑定到这个对象上。

function foo() {
  console.log(this.a)
}

const obj = {
  a: 1,
  foo: foo
}

obj.foo() // 1

对象属性引用链中,只有上一层或者说最后一层的调用位置起作用。

function foo() {
  console.log(this.a)
}

const obj = {
  a: 1,
  foo: foo
}

const obj1 = {
  a: 2,
  obj: obj
}

obj1.obj.foo() // 1

隐式丢失

  1. 引用赋值隐式丢失;

    function foo() {
      console.log(this.a)
    }
    
    const obj = {
      a: 1,
      foo: foo
    }
    
    const bar = obj.foo
    bar() // undefined
    

    注意,bar 实际上引用的foo函数本身,所以应用了默认绑定。这时的this 指向window

  2. 参数传递隐式丢失;

    function foo() {
      console.log(this.a)
    }
    
    function doFoo(fn) {
      fn()
    }
    
    const obj = {
      a: 1,
      foo: foo
    }
    
    doFoo( obj.foo ) // undefined
    

    参数传递,其实也是一种隐式赋值,逻辑和上一个案例一样。

显式绑定

调用函数时,强制将函数绑定到this。直接指定this的绑定对象,这称为显式绑定。可以通过callapplybind方法实现。

function foo() {
  console.log(this.a)
}

const obj = {
  a: 1,
}
foo.call(obj) // 1
foo.apply(obj) // 1
const bindFoo = foo.bind(obj)
bindFoo() // 1

硬绑定

硬绑定等同于bind。重新定义了函数内部的this指向。

function foo() {
  console.log(this.a)
}
const obj = {
  a: 1,
}
const bar = function(){
  foo.call( obj )
}
bar() // 1
bar.call(window) // 1

以上代码,同bind的效果一致。

function foo() {
  console.log(this.a)
}
const obj = {
  a: 1,
}
const bar = foo.bind(obj)
bar() // 1

new绑定

function foo(a) {
  this.a = a
}

const bar = new foo(2)
console.log(bar.a) // 2

使用new来调用foo(...)时,new内部的实现会构造一个新对象,并把它绑定到foo(...)调用的this上。我们称为new绑定

绑定优先级

显然,根据上面的案例,显示绑定 > 隐式绑定 > 默认绑定。那么new绑定和显示绑定哪个高?这里通过硬绑定来测试优先级。

function foo(value){
  this.a = value
}
const obj = {
  a: 1,
}
const bar = foo.bind(obj)
bar(2)
console.log(obj.a) // 2

const baz = new bar(3)
console.log(obj.a) // 2
console.log(baz.a) // 3

如此,那么绑定优先级的顺序为: new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

我们可以通过下面四条规则,判断this的绑定对象:

  1. 由new调用?
  2. 由call、apply、bind调用?
  3. 由上下文对象调用?
  4. 如上都不满足,则是默认绑定,严格模式下绑定到undefined。

绑定意外

null或者undefined作为this的绑定对象的this忽略问题

null或者undefined作为this的绑定对象传入call、apply、bind,这时,这些值在调用时会把忽略,实际应用的是默认绑定

如果函数并不关心this的话,这时,传入null,作为一个占位符,是个不错的选择。

但是使用null来忽略this绑定可能会有一些副作用。如果某个函数确实使用了this(比如第三方库的一个函数),那默认绑定规则,就会把这个this绑定到全局对象,这将会导致不可预计的后果(比如修改全局对象)。

如此,我们应该用一种更为安全的方法,来作为占位符。

我们可以创建一个空的非委托对象Object.create(null)。忽略this绑定时总是传入一个空的非委托对象,就不会对全局对象产生任何影响。

function foo(value){
  console.log(value)
}
var ø = Object.create(null)

foo.call(ø, 2) // 2

间接引用

创建一个函数的“间接引用”,这种情况下,调用这个函数会应用默认绑定规则。这种情况容易在赋值是发生:

function foo() {
  console.log(this.a)
}

const obj = {
  a: 1,
  foo: foo
}

obj.foo() // 1
const bar = obj.foo // 赋值
bar() // undefined

软绑定

硬绑定可以将函数的this强制绑定到指定的对象上。但是硬绑定存在一个问题,就是会降低函数的灵活性,并且在硬绑定之后无法再使用隐式绑定或者显式绑定来修改 this 的指向。

而软绑定就可以解决如上的问题,可以给默认绑定设置一个全局对象undefinednull之外的值,这样就可以实现和硬绑定同样的效果,同时保留了隐式绑定或者显式绑定来修改 this 的能力。

可以看看这篇关于软绑定的文章。实现代码如下(来源:你不知道的JavaScript》上卷):

if(!Function.prototype.softBind){
    Function.prototype.softBind=function(obj){
        var fn=this;
        var args=Array.prototype.slice.call(arguments, 1);
        var bound=function(){
            return fn.apply(
              // 判定当前this,如果绑定到了全局对象或undefined,null,则修改this为传入的obj,否则什么也不做
                (!this || this === (window || global)) ? obj : this,
                args.concat.apply(args,arguments)
            );
        };
        bound.prototype=Object.create(fn.prototype);
        return bound;
    };
}

看效果:

var name = 'global'
const obj = {
    name: 'obj',
    foo: function () {
        console.log(this.name)
    }
}
const obj1 = {
    name: 'obj1'
}

obj.foo() // => obj // 隐式绑定
setTimeout(obj.foo, 0) // 默认绑定 this丢失,global

// 现在我们使用软绑定
const softFoo = obj.foo.softBind(obj)
setTimeout(softFoo, 0) // obj 软绑定,this指向默认设置的obj

softFoo.call(obj1) // obj1,可以使用call显式改变this

obj1.foo = softFoo
obj1.foo() // obj1,软绑定的this指向成功被隐式绑定修改了,绑定到了obj1

setTimeout(obj1.foo, 0) // obj 回调函数相当于一个隐式的传参,本来会有默认绑定将this绑定全局,但是有软绑定,这里this还是指向obj

箭头函数

语法:

(param1, param2, …, paramN) => { statements }
(param1, param2, …, paramN) => expression

注意事项:

  • 箭头函数不使用this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定this
  • 箭头函数不能作为构造函数,new 箭头函数() 会程序报错;
  • 同理,由于箭头函数不能作为构造函数,内部也就没有自身的上下文对象,也就不能使用arguments对象;

小结

好了,以上就是关于 this 的一些介绍,讲解了 this 是什么,this 绑定出现的时机,关于 this 的四种绑定,以及绑定优先级等。完结✿✿ヽ(°▽°)ノ✿~