JavaScript基础之this/call/apply/bind


this

this 是什么

当一个函数被调用时,会创建一个活动记录(或者称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this 就是这个记录的一个属性,会在函数执行的过程中用到。

如果对这个解释看不太懂可以去看看JavaScript基础之执行上下文

this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

this 值如何确定

this 永远指向最后调用它的对象。

调用位置

在理解 this 的绑定过程之前,首先理解调用位置:调用位置就是函数在代码中被调用的位置

确定调用位置,最重要的时分析调用栈

baz() // baz 的调用位置

function baz() {
    // 当前的调用栈是 baz
    // 因此,当前调用位置是全局作用域
    console.log('baz')
    bar() // bar 的调用位置
}
function bar() {
    // 当前的调用栈是 baz -> bar
    // 因此,当前调用位置在 baz 中
    console.log('bar')
    foo() // foo 的调用位置
}
function foo() {
    // 当前的调用栈是 baz -> bar -> foo
    // 因此,当前调用位置在 bar 中
    console.log('foo')
}

绑定规则

默认绑定

对函数进行不带任何修饰的调用,即为默认绑定。

默认绑定的 this 指向全局对象 window (非严格模式)

独立的函数调用可以看作是 this 的默认绑定规则。

function foo() {
    console.log(this.a)
}
var a = 1
foo() // 1

foo() 函数是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定

通过分析调用位置,我们知道 foo 函数是在全局作用域中运行,那么 foo 函数中的 this 绑定的就是全局对象 window

如果使用严格模式:

function foo() {
    "use strict"
    console.log(this) // undefined
    console.log(this.a) // TypeError: Cannot read property 'a' of undefined at foo
}
var a = 1
foo() 
"use strict"
function foo() {
    console.log(this) // undefined
    console.log(this.a) // TypeError: Cannot read property 'a' of undefined at foo
}
var a = 1
foo() 

在严格模式下,默认绑定的 this 会指向 undefined

虽然 this 的绑定规则完全取决于调用位置,但是只有 foo() 运行在非 strict mode 下时,默认绑定才能绑定到全局对象,在严格模式下调用 foo() 则不影响默认绑定。

function foo() {
    console.log(this.a)
}
var a = 1
(function () {
    "use strict"
    foo() // 1
})()

隐式绑定

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

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

foo() 函数并不属于 obj 对象,然而,调用位置会使用 obj 上下文来引用函数,因此你可以说函数被 obj 对象“拥有”或者“包含”函数引用。当 foo() 被调用时,它的前面确实加上了对 obj 的引用,this 被绑定到 obj ,因此 this.aobj.a 是一样的。

分析下面的代码:

var a = 3
function foo() {
    console.log(this.a)
}

var obj1 = {
    a: 1,
    foo: foo
}

var obj2 = {
    a: 2,
    obj: obj1
}

obj2.obj.foo() // 1
  • this 永远指向最后调用它的对象
  • 如果函数调用前存在多个对象,this 指向距离调用自己最近的对象

实际上,上面两个表达的意思是相同的,把 obj2.obj.foo() 按照运算符的优先级分解:

  1. 先运算 obj2.obj 结果为 obj1
  2. obj1.foofooobj1 上的属性)结果为 foo(全局上的函数)
  3. foo() 执行

从上面的步骤中可以看出,最后调用 foo 函数的对象是 obj1,距离 foo 函数最近的对象也是 obj1,因此 foo 函数里的 this 指向 obj1

隐式丢失

思考下面的代码:

function foo() {
    console.log(this.a)
}
var obj = {
    a: 2,
    foo: foo
}

var bar = obj.foo
var a = 'window'

bar() // 'window'

虽然 barobj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此引用了默认绑定。

观察以下代码:

function foo() {
    console.log(this.a)
}
function doFoo(fn) {
    // fn 其实引用的是foo
    fn() // 调用位置
}
var obj = {
    a: 2,
    foo: foo
}
var a = 'window'
doFoo(obj.foo) // 'window'

我们发现,在函数回调的过程中, this 丢失了绑定对象,这种情况更微妙,但又更常见。

参数传递是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上面的例子一样。

显示绑定

使用函数的 call(..) 和 apply(..) 方法在某个对象上强制调用函数。

思考下面的代码:

function foo() {
    console.log(this.a)
}
var obj = {
    a: 2
}
foo.call(obj) // 2

通过 foo.call(..),我们可以在调用 foo 时强制把它的 this 绑定到 obj 上。

在 JavaScript中,当我们调用一个函数时,我们习惯称之为函数调用,函数处于一个被动的状态;而 callapply 让函数从被动变主动,函数能主动选择自己的上下文,所以这种写法我们又称之为函数应用

注意,如果在使用 call 之类的方法改变 this 指向时,指向参数提供的是 null 或者 undefined ,那么 this 将指向全局对象。

function foo() {
    console.log(this.a)
}
var obj = {
    a: 'obj'
}
var a = 'window'

foo.call(obj) // 'obj'
foo.call(null) // 'window'
  1. 硬绑定

绑定丢失可以通过应绑定来解决,观察以下代码:

function foo() {
    console.log(this.a)
}
function doFoo(fn) {
    // fn 其实引用的是foo
    fn.call(obj) // 调用位置
}
var obj = {
    a: 2,
    foo: foo
}
var a = 'window'
doFoo(obj.foo) // 2

我们创建了函数 doFoo() ,并在它的内部手动调用了 foo.call(obj),因此强制把 foothis 绑定到了 obj

硬绑定是一种非常常见的模式,所以 ES5 提供了内置的方法 Function.prototype.bind,它的用法如下:

function foo(something) {
    console.log(this.a, something)
    return this.a + something
}
var obj = {
    a: 2
}
var bar = foo.bind(obj)
var b = bar(3) // 2 3
console.log(b) // 5

bind(..) 会返回一个硬编码的新函数,它会把你指定的参数设置为 this 的上下文并调用原始函数。

  1. API 调用“上下文”

在 JavaScript 的 API 中部分方法也内置了显式绑定,以 forEach 为例:

function foo(el) {
    console.log(el, this.id)
}
var obj = {
    id: 'awesome'
}
// 调用 foo(..)时把 this 绑定到 obj
[1, 2, 3].forEach(foo, obj) // 1 awesome 2 awesome 3 awesome

new 绑定

new 操作符的具体操作过程在这里不提,我们只需要知道 new 操作符创建的新对象会绑定到其构造函数的 this

function foo(a) {
    this.a = a
}
var bar = new foo(2)
console.log(bar.a) // 2

箭头函数绑定

箭头函数的 this 继承上层执行上下文里面的 this

function foo() {
    return () => {
        console.log(this.a)
    }
}
var obj1 = { a: 2 }
var obj2 = { a: 3 }
var bar = foo.call(obj1)
bar.call(obj2) // 2

foo() 内部创建的箭头函数会捕获调用时 foo()this。由于 foo()this 绑定到了 obj1barthis 也会绑定到 obj1箭头函数的绑定无法被修改。(new 也不行!)

思考下面的代码:

var name = 'laozhang'
var grandFather = {
    name: 'laowang',
    father: {
        name: 'xiaowang',
        son: {
            name: 'xiaoming',
            getName: () => {
                console.log(this.name) // 1
            }
        }
    },
    getName: function () {
        console.log(this.name) // 2
        var name = 'xiaowang'
        var son = {
            name: 'xiaoming',
            getName1: () => {
                console.log(this.name) // 3
            },
            getName2: function() {
                console.log(this.name) // 4
            }
        }
        son.getName1()
        son.getName2()
    }
}
grandFather.father.son.getName()
grandFather.getName()

上面的代码执行的结果是什么?

  • 1处为 'laozhang',这里的 this 在箭头函数内,指向上层执行上下文,为全局对象 window
  • 2处为 'laowang', 这里的 this 在普通声明函数中,指向最后一个调用它的对象(grandFather
  • 3处为 'laowang',这里的 this 在箭头函数中,指向上层执行上下文,为 grandFather 对象
  • 4处为 'xiaoming',这里的 this 在普通声明函数中,指向最后一个调用它的对象(son

对执行上下文的不是很理解的话,可以看看JavaScript基础之执行上下文.

优先级

  • new 绑定 > 隐式绑定 > 默认绑定

  • 显示绑定 > 隐式绑定 > 默认绑定

newcall/apply/bind 无法一起使用,不存在使用场景,无需去比较它们的优先级。

练习

如果觉得上面的内容都理解了,那么可以试试 js 从两道面试题加深理解闭包与箭头函数中的this

call/apply/bind

定义

在MDN中的定义:

call:

call()方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

apply:

apply() 方法调用一个具有给定this值的函数,以及作为一个数组(或类似数组对象)提供的参数。

bind:

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

语法

call

func.call(thisArg, arg1, arg2, ...)

参数:

  • thisArg:可选的。在 function 函数运行时使用的 this 值。
  • arg1, arg2, ...:指定的参数列表。(原函数所需的参数)

作用:

改变函数的 this 指向。

示例:

function Product(name, price) {
  this.name = name
  this.price = price
}

function Food(name, price) {
  Product.call(this, name, price)
  this.category = 'food'
}

console.log(new Food('cheese', 5).name) // 'cheese'

apply

func.apply(thisArg, [argsArray])

参数:

  • thisArg:必选的。在 func 函数运行时使用的 this 值。
  • argsArray:可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。

作用:

改变函数的 this 指向。

示例:

const numbers = [5, 6, 2, 3, 7];

const max = Math.max.apply(null, numbers);

console.log(max);
// expected output: 7

const min = Math.min.apply(null, numbers);

console.log(min);
// expected output: 2

bind

func.bind(thisArg[, arg1[, arg2[, ...]]])

参数:

  • thisArg:调用绑定函数时作为 this 参数传递给目标函数的值。如果使用new运算符构造绑定函数,则忽略该值。
  • arg1, arg2, ...:当目标函数被调用时,被预置入绑定函数的参数列表中的参数。

作用:

改变函数的 this 指向。

返回值:

返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。

示例:

const module = {
  x: 42,
  getX: function() {
    return this.x;
  }
};

const unboundGetX = module.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// expected output: undefined

const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// expected output: 42

区别

  1. callapplybind 都用于改变 this 绑定,但 callapply 在改变 this 指向的同时还会执行函数,而 bind 在改变 this 后是返回一个全新的 boundFcuntion 绑定函数,这也是为什么上方例子中 bind 后还加了一对括号 () 的原因。

  2. bind 属于硬绑定,返回的 boundFunctionthis 指向无法再次通过 bindapplycall 修改;callapply 的绑定只适用当前调用,调用完就没了,下次要用还得再次绑。

  3. callapply 功能完全相同,唯一不同的是 call 方法传递函数调用形参是以散列形式,而 apply 方法的形参是一个数组。在传参的情况下,call 的性能要高于 apply,因为 apply 在执行时还要多一步解析数组。

手动实现

call

Function.prototype.myCall = function (context) {
  // 判断调用对象是否为函数
  if (typeof this !== 'function') {
    throw new TypeError(`${this} is not a function`)
  }
  // 判断传入的对象是否存在,默认是 window
  context = context ? Object(context) : window
  // 把当前调用的函数赋值给传入对象的 context.fn
  context.fn = this
  // 声明一个数组用来保存当前调用函数的参数
  let args = []
  for (let i = 1; i < arguments.length; i++) {
    args.push('arguments['+i+']')
  }
  // 执行调用函数,并返回结果
  let result = eval('context.fn(' + args + ')')
  // 删除 fn
  delete context.fn
  return result
}

apply

Function.prototype.myApply = function (context, args) {
  // 判断调用对象是否为函数
  if (typeof this !== 'function') {
    throw new TypeError(`${this} is not a function`)
  }
  // 判断传入的对象是否存在,默认是 window
  context = context ? Object(context) : window
  // 把当前调用的函数赋值给传入对象的 context.fn
  context.fn = this
  // 判断是否传入函数参数
  if(!args){
    return context.fn()
  }
  // 判断传入的参数是否为数组
  if (!(args instanceof Array)) {
    throw new TypeError(`${args} is not a array`)
  }
  // 执行调用函数,并返回结果
  let result = eval('context.fn('+ args +')')
  // 删除 fn
  delete context.fn
  return result
}

bind

Function.prototype.myBind = function (context) {
  // 判断调用对象是否为函数
  if (typeof this !== 'function') {
    throw new TypeError(`${this} is not a function`)
  }
  // 获取调用函数
  let that = this
  // 将调用函数的参数转成数组
  let bindArgs = Array.prototype.slice.call(arguments, 1)
  // 声明一个新函数 Fn
  function Fn() {}

  function bindFn() {
    // 将 myBind 函数的参数转成数组
    let args = Array.prototype.slice.call(arguments)
    // 绑定 this 指向
    // 如果绑定的函数被 new 执行,当前函数的 this 就是当前的实例
    that.apply(this instanceof bindFn ? this : context, bindArgs.concat(args))
  }
  // new 出来的结果可以找到原有类的原型
  Fn.prototype = that.prototype
  bindFn.prototype = new Fn()
  // 返回一个绑定后的函数
  return bindFn;
}

参考

《你不知道的JavaScript上卷》

MDN–call/apply/bind

js 五种绑定彻底弄懂this,默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定详解

this、apply、call、bind


评论
 上一篇
JavaScript基础之原型/继承 JavaScript基础之原型/继承
原型定义 所有的引用类型的数据都有 __proto__ 这个属性,该属性即为隐式原型,所有的函数都有 prototype 属性,该属性即为显式原型。 这两个属性分别是什么?有什么联系? prototype我们从原型的定义上知道所有的函数都
2020-08-19
下一篇 
JavaScript基础之闭包 JavaScript基础之闭包
闭包定义MDN定义: 闭包是指那些能够访问自由变量的函数。 自由变量: 自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的的变量。 《你不知道的JavaScript》里的定义: 当函数可以记住并访问所在的词法作用域
2020-08-16
  目录