你不知道的 "this指向" 规则

479 阅读4分钟

特点

  1. this永远指向一个对象。
  2. this指向 在函数执行时确定。(执行上下文三大属性之一)

执行上下文概念可在往期文章 作用域与作用域链 | 8月更文挑战 中了解。

规则

摘自《你所不知道的JavaScript》上卷,用来判断this指向。顺序代表着优先级,由高到低。(箭头函数不适用)

  1. 由new调用,则绑定到新创建的对象。
  2. 由call、apply、bind调用,绑定到指定的对象。
  3. 由上下文对象调用,绑定到上下文对象。
  4. 默认:非严格模式下绑定到undefined,否则绑定到全局对象。

下文将按照当前规则顺序讲述,最后单独讨论箭头函数

new 调用

new调用构造函数创建新对象,this指向新对象。

function person(name) {
  this.name = name
}
let obj = new person('瑾行')
console.log(obj.name) // 瑾行

Tips:若构造函数有返回值,只返回构造函数返回的对象。
手写new函数

function _new() {
  let obj = new Object()
  Constructor = [].prototype.shift.call(arguments)
  obj.__proto__ == Constructor.prototype
  let ret = Constructor.apply(obj, arguments)
  return typeof ret === 'object' ? ret : obj
}

call、apply、bind

call、apply、bind都拥有一样的能力去做同一件事(更改函数运行时的执行上下文对象),主要讲讲三者用法以及差异。

call

function.call (thisArg, arg1, arg2, ...) thisArg: 函数上下文对象,可选参数。若nullundefined自动替换全局对象。
arg1,arg2,...: 参数列表。

function introduce(job) {
  console.log(this.name + '是一名' + job)
}
let obj = { name: '瑾行' }
let job = '前端工程师'
introduce.call(obj, job) // 瑾行是一名前端工程师

apply

与call用法大致相同,差异在利用数组包裹参数列表。

function.apply(thisArg, [arg1, arg2, ...])

function introduce(job) {
  console.log(this.name + '是一名' + job)
}
let obj = { name: '瑾行' }
let job = '前端工程师'
introduce.apply(obj, [job]) // 瑾行是一名前端工程师

bind

bind 与 call、apply差异在于不是立即调用函数,而是返回改变函数上下文后的函数。 注意: 多次bind是无效的。

function.bind(thisArg, arg1, arg2, ...)

function introduce(job) {
  console.log(this.name + '是一名' + job)
}
let obj = { name: '瑾行' }
let job = '前端工程师'
let func = introduce.bind(obj, job) 
func() // 瑾行是一名前端工程师

在返回的函数上再次绑定新的对象,输出值不变。

function introduce(job) {
  console.log(this.name + '是一名' + job)
}
let obj = { name: '瑾行' }
let obj1 = { name: '七金' }
let job = '前端工程师'
let func = introduce.bind(obj, job).bind(obj1, job) 
func() // 瑾行是一名前端工程师

第二次bind是改变返回函数执行上下文对象,而非原函数。编写简单的bind函数覆盖原生bind实现,并执行下述代码。

Function.prototype.bind = function(oThis) {
  if(typeof this !== 'function') {
      return;
  }
  var self = this,
      // 当前arguments是bind函数的传参列表
      args = Array.prototype.slice.call(arguments, 1);
  return function() {
      // 当前arguments是返回函数的传参
      console.log(oThis,this)
      return self.apply(oThis, args.concat(Array.prototype.slice.call(arguments)));
  }
}

function introduce(job) {
  console.log(this.name + '是一名' + job)
}
let obj = { name: '瑾行' }
let obj1 = { name: '七金' }
let job = '前端工程师'
let func = introduce.bind(obj, job).bind(obj1, job) 
func() // 瑾行是一名前端工程师

image.png

从截图可以得出结论:
第一行是func, this指向是window
第二行是introduce.bind(obj, job), this指向obj1。也就是说再次绑定的bind改变的是introduce.bind(obj, job)的执行上下文对象,而非原函数introduce。✌

对象调用

若调用位置上下文有对象,this则绑定到指定对象。但注意会存在丢失绑定对象的情况。

var person = {
  name: '瑾行'getName: getName
}
var name = '七金'
function getName() {
  console.log(this.name)
}
person.getName() // 瑾行
var func = person.getName
func() // 七金

默认情况

指在任何作用域下,直接通过函数名调用。
特点: 非严格模式下绑定到全局对象window,否则绑定到undefined。

// 非严格模式
var name = '瑾行'
function getName() {
    console.log(this, this.name)
}
getName() // Window{},瑾行
// 严格模式
var name = '瑾行'
function getName() {
    "use strict"
    console.log(this, this.name)
}
getName() // undefined,报错
// 函数作用域下调用
var name = '瑾行'		
function getName() {
    console.log(this,this.name)
}
function func() {
    getName()
}
func() // Window{},瑾行

箭头函数

上文规则不再适用箭头函数。

箭头函数与function函数有如下区别:

  1. 箭头函数语法简洁、清晰。
  2. 箭头函数本身没有绑定this
  3. 箭头函数this是从上层的作用域寻找的,被定义时就已确定,且this指向永远不变。
var name = '瑾行'
function getName() {
    setTimeout(() => {
        console.log(this.name)
    }, 1000)
}
var obj = {name: '七金'}
getName(obj) // 瑾行
getName.call(obj) // 七金

// 原因:
// 箭头函数定义时找到上层函数getName的this,getName函数的this指向window,所以输出 瑾行。
// getName.call(obj) 改变了getName的this, 又因为箭头函数定义时就继承了上层作用域的this,所以输出 七金。 
  1. 箭头函数不能作为构造函数使用。
let getName = () =>  {
    this.name = '瑾行'
}
var obj = new getName()
// 报错:Uncaught TypeError: getName is not a constructor。
// 原因:因为箭头函数不存在显式原型,无法完成构造。底层实现相关代码:obj.__proto__ = Constructor.prototype。
  1. new、call、apply、bind无法改变箭头函数执行上下文对象。
let obj = {name: '瑾行'}
let obj1 = {name: '七金'}
function getName() {
  return () => {
    // 继承foo
    console.log(this.name)
  }
}
let func = getName.bind(obj)
let func1 = func().bind(obj1) 
func1() // 输出瑾行
  1. 箭头函数没有arguments对象。
var name = '瑾行'
let getName = () =>  {
    console.log(arguments)
}
getName()
// 报错:ReferenceError: arguments is not defined
// 原因:箭头函数 和 全局环境 均没有 arguments参数
  1. 箭头函数没有原型prototype
var name = '瑾行'
let getName = () =>  {
}
console.log(getName.prototype) // undefined
  1. 箭头函数不可当做Generator函数,也不能内部使用yield。

参考

彻底搞懂JavaScript中的this指向问题
JavaScript的this指向问题深度解析
JavaScript深入之bind模拟实现