JS夯实之ThisBinding的四条准则

1,653 阅读5分钟

前言

在上篇文章《JS夯实之执行上下文与词法环境》中提到了词法环境的创建过程,跳过了ThisBinding的绑定过程的陈述。而this的指向问题不管在面试或者业务工作中都是经久不衰的“坑”。

其实只要熟记四条准则,不论多么复杂的场景,你都可以正确判断出this的指向。

隐式绑定

关键词:.

隐式绑定发生在对象方法调用的时候,即通过点标识符调用对象方法。此时方法内的this就指向点.左边的对象:

var kid = {
    name: '小明',
    getName: function(){
        return this.name
    }
}
kid.getName() // 小明

这里需要注意一下函数别名的情况,如下:

var name = '小红'
var kid = {
    name: '小明',
    getName: function(){
        return this.name
    }
}
var fx = kid.getName
fx()  // 小红

此时fxthis应该要用下文中“默认绑定”的场景来判定。

显式绑定

关键词:call apply bind

callapplybind都是Function原型链上的方法 —— Function.prototype.call Function.prototype.apply Function.prototype.bind。它们都可以改变调用函数时this的指向。 在MDN中关于callapply的解释:

调用一个给定this值的函数,并接收参数 call签名: (thisArg, arg1, arg2, ...) => any apply签名: (thisArg, [argsArray]) => any

call与apply之间的区别就是call()方法接受的是参数列表,而apply()方法接受的是一个参数数组。在非严格模式下,thisArg指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。

使用call和apply调用函数,可以改变函数调用时this的指向:

function print({...args}){
    console.log(this.name + ':' + args.join(',') )
}
var kid = {
    name: '小明'
}
getName.call(kid, 1,2,3)  // 小明:1,2,3
getName.apply(kid, [1,2,3]) // 小明:1,2,3

bind相比于call和apply,对待this则更为“粗暴”,bind方法会创建一个新的函数,新函数包装了原函数,并且这个新函数强绑定了this,调用这个新函数时将无视隐式绑定和默认绑定:

function print(){
    console.log(this.name)
}

let kid = {
    name: '小明',
}

let printName = print.bind(kid)
printName() // --> 小明

let kid2 = {
    name: '小红',
    getName: printName
}
kid2.getName()  // --> 小明

如果bind后的函数被当做构造函数调用,则不受限制,this仍指向构建的新对象。

let kid = {
    name: '小明'
}
function People(name){
    this.name = name
}
const BindedPeople = People.bind(kid)
let kid2 = new BindedPeople('小红')
kid2.name  // 小红

熟悉React的开发者会经常和bind或箭头函数打交道,React在给子组件传递方法时,通常需要将方法的this绑定为父组件的实例,这样可以正确读取到父组件中一些属性和方法:

class Parent extends React.Component {
  constructor() {
    super();
    this.name = 'i am parent'
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    console.log(this.name)  // i am parent
  }

  render() {
    return (
      <Child onClick={this.handleClick}>click here</Child>
    );
  }
}

构造函数绑定

关键词:new

当一个函数被用作构造函数使用时(new Foo(...)),则函数内的this指向实例化后的对象:

function Child(name){
    this.name = name
    this.getName = function(){
        return this.name
    }
}
let kid = new Child('小明')
kid.getName()  // 小明

getName中的this此时指向是对象kid,可以读取到对象kid上的name属性。当然也可以使用隐形绑定的准则来判断出this的指向。

关于new具体做了什么,可以移步《这是一篇关于JavaScript原型知识的还债帖》查看更多。 使用new操作符实例化对象时,默认调用的函数的[[Construct]]内部属性,ECMA中关于[[Construct]]的描述。

默认绑定

将“默认绑定”放在其他三条规则之后说,是因为默认绑定是经过以上特定场景准则后仍无法判断this绑定时的兜底准则。一般发生在独立的函数调用和全局环境中。

var printThis = function(){
    console.log(this)
}
printThis()  // 输出结果视严格模式与否而定

在严格模式下,即在所有语句之前放一个特定语句 "use strict"时,以上代码输出为undefined;
在非严格模式下,此时this指向全局对象 —— window | global,代码输出为全局对象;

小结

四条准则按照优先级排列,应该是:构造函数绑定 --> 显式绑定 --> 隐式绑定 --> 默认绑定

箭头函数

在ES6中新增了一种函数定义方式——箭头函数,()=>{}。箭头函数与普通函数相比,在一些内部属性访问上有很大的不同。引自ECMA中关于箭头函数的说明:

An ArrowFunction does not define local bindings for arguments, super, this, or new.target. Any reference to arguments, super, this, or new.target within an ArrowFunction must resolve to a binding in a lexically enclosing environment.Typically this will be the Function Environment of an immediately enclosing function.

即箭头函数没有自己的arguments super this new.target内部属性(简称“四大护法”,😝),依据词法环境的outer引用的层层递进,在箭头函数中访问的arguments super this new.target都是读取自外层最近的普通函数的词法环境。箭头函数的this始终指向函数定义时的this,而非执行时。

由于没有“四大护法”的加持,箭头函数自然无法成为构造函数,也就无法使用new来调用了。

proposal-bind-operator

tc39有一个提案proposal-bind-operator,使用一个新的操作符::来完成this的绑定操作。

obj::func
// 等价于:
func.bind(obj)

::obj.func
// 等价于:
obj.func.bind(obj)

obj::func(val)
// 等价于:
func.call(obj, val)

::obj.func(val)
// 等价于:
obj.func.call(obj, val)

借助babel插件可以提前使用::运算符

最后

码字不易,如果:

  • 这篇文章对你有用,请不要吝啬你的小手为我点赞;
  • 有不懂或者不正确的地方,请评论,我会积极回复或勘误;
  • 期望与我一同持续学习前端技术知识,请关注我吧;
  • 转载请注明出处;

您的支持与关注,是我持续创作的最大动力!

本文首发于我的Blog仓库