关于前端:深入理解JavaScriptcallapplybind三大将

24次阅读

共计 4346 个字符,预计需要花费 11 分钟才能阅读完成。

之前在讲 this 关键字 时,咱们介绍过这三个 api,咱们得出这样的论断:call、apply 和 bind 都领有 ” 掰弯 ” this 指向的能力

介于过后的重点是 this,对这三个 api 没有具体介绍,本文,咱们来理解一下函数中所谓不多的原型办法——call、apply、bind

Call

MDN:call() 办法应用一个指定的 this 值和独自给出的一个或多个参数来调用一个函数

应用办法:

let bar = {name: 'johnny'}
function foo() {console.log(this.name)
}
foo.call(bar); // johnny

首先 call 是个原型办法,所以一个函数能很天然的应用此办法,即 foo 的 call 办法继承自 Function.prototype.call。其次,call 办法中传入的值会成为 foo 函数的 this 将来指向的对象

其本质是扭转 this 指向,将 this 指向了(call 的)传入值

实现 call

咱们依据之前的形容来实现 call

Function.prototype.mycall = function(context = window, ...args) {if (this === Function.prototype) {return undefined // 避免 Function.prototype.mycall 间接调用}
    const fn = Symbol()
    context[fn] = this;
    const result = context[fn](...args)
    delete context[fn]
    return result
}

咱们一步步剖析,首先不反对 Function.prototype.mycall 间接调用,

Function.prototype.call(bar) // undefined

这个很好了解,Function.prototype 中原本就没有 this,调用了也 XXX

let context = context || window

因为要思考如果 context 传入的是 null 呢

foo.call(null);

如果是 null 的话,context 就指向 window,这个很好了解,防御性代码

const fn = Symbol()
context[fn] = this;
const result = context[fn](...args)

在传入的 context 中设置一个属性,将 this 赋值给它,接着执行它

这是 call 函数的关键所在,咱们在前文讲 call 示例时,就说到 call 会扭转 this 指向,讲 this 指向传入的 context,所以咱们在模式实现 call 时,就能够先将 this 存在 context 上的一个属性上,再执行它,this 的规定是谁调用它,它指向谁。这样 this 就指向了 context

delete context[fn]
return result

删除 context 属性,开释内存,并返回后果值 result

call 实现就是如此,测试一波

let bar = {name: 'johnny'}
function foo() {console.log(this.name)
}
foo.mycall(bar);

apply

MDN:apply() 办法调用一个具备给定 this 值的函数,以及以一个数组(或一个类数组对象的模式提供的参数

应用办法:

let bar = {name: 'johnny'}
function foo(age, hobby) {console.log(this.name, age, hobby)
}
foo.apply(bar, [28, 'sleep']); // johnny 28 sleep
// call 应用办法
// foo.call(bar, 28, 'sleep');

apply 和 call 应用上差不太多,只是传参形式不同

foo.call(obj, param1, param2,...,paramN) // 传入一串参数
foo.apply(obj, [param1, param2,...,paramN]) // 第二参数为类数组 

所以实现上 和 call 大差不差

实现 apply

Function.prototype.myapply = function (context = window, args) {if (this === Function.prototype) {return undefined}
    const fn = Symbol()
    context[fn] = this
    
    let result;
    if (!Array.isArray(args)) {result = context[fn]()} else {result = context[fn](...args)
    }
   
    delete context[fn]
    return result
}

测试一波 myapply

let bar = {name: 'johnny'}
function foo(age, hobby) {console.log(this.name, age, hobby)
}
foo.myapply(bar, [28, 'sleep']); // johnny 28 sleep

bind

MDN:bind() 办法创立一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时应用

应用办法:

let bar = {name: 'johnny'}
function foo(age) {console.log(this.name, age)
}
// 返回一个函数
let bindBar = foo.bind(bar)

bindBar(28) // johnny 28

与 call、apply 一样,它是函数的原型办法,不过与它们不同的是,它是 ES5 新增的办法,它返回的是一个函数(并且它还反对传参)

看到返回的是一个函数,就阐明 bind 办法是一个闭包

实现 bind

Function.prototype.mybind = function (context, ...args1) {if (this === Function.prototype) {throw new TypeError('Error')
    }
       const _this = this
    return function F(...args2) {if (this instanceof F) {return new _this(...args1, ...args2)
        }
        return _this.apply(context, args1.concat(args2))
    }
}

咱们剖析如何实现 bind

首先 bind 不能原型办法调用,如果应用就提醒报错

其次咱们依据 bind 的一个个性,对其的应用分为两种

一个绑定函数也能应用 new 操作符创建对象:这种行为就像把原函数当作结构器。提供的 this 值被疏忽,同时调用时的参数被提供给模仿函数

也就是说,咱们要判断它是否为结构函数调用,如果是则用 new 调用以后函数;如果不是,则应用 apply 来呈现 context

// 判断是否是构造函数
if (this instanceof F) {
    // 如果是构造函数,则以 foo(即_this)为结构器调用函数
    return new _this(...args1, ...args2)
}
// 如果非构造函数,则用 apply 指向代码
return _this.apply(context, args1.concat(args2))

测试一波

// 测试非构造函数应用
let bar = {name: 'johnny'}
function foo(age, hobby) {console.log(this.name, age, hobby)
}
// 返回一个函数
let bindBar = foo.mybind(bar, 28)

bindBar('sleep') // johnny 28 sleep
// 测试构造函数时应用
function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.sayName = function() {console.log('my name is' + this.name)
}
let emptyObj = {}
var FakerPerson = Person.mybind(emptyObj)

var johnny = new FakerPerson('johnny', 28)
johnny.sayName() // my name is johnny

总结

call、apply、bind 三者都能批改 this 指向,其中 call、apply 在 ECMAScript 3 时定义,它们满足开发者的大部分需要,即扭转 this 的指向,其原理是在将 this 赋值给 context(传入的值)的一个属性,并执行它(谁调用 this,this 指向谁)

而这两者的区别就是调用办法不同,call 自第二个参数开始传入一连串参数,apply 的第二个参数是一个数组,承受所有参数

它们也是实现 继承(后续文章更新)的一种办法——借用构造函数

而 bind 则是在 ECMAScript 5 呈现,是对批改 this 指向的一种补充,它以闭包的模式存在,它有三个特点

  • 返回的是一个函数
  • 能够传入参数(应用 bind 和经 bind 生成的函数都能够传参)
  • 应用 bind 生成的函数作为构造函数时,bind 时的指定 this 会生效,但传入的参数仍然失效

衍生

思考题:数组和类数组有什么区别?

  1. 类数组是领有 length 属性和索引属性的对象
  2. 不具备数组所具备的办法

类数组是个一般对象,而实在的数组是 Array 类型,所以类数组的原型关系和数组不同

常见的类数组有:arguments、DOM 对象列表(document.querySelectorAll)

类数组转换为数组

  • Array.prototype.slice.call(arrayLike, 0)
  • […arrayLike](扩大运算符)
  • Array.from(arrayLike)

参考资料

  • JavaScript 深刻之 call 和 apply 的模仿实现
  • JavaScript 深刻之 bind 的模仿实现

系列文章

  • 深刻了解 JavaScript- 开篇
  • 深刻了解 JavaScript-JavaScript 是什么
  • 深刻了解 JavaScript-JavaScript 由什么组成
  • 深刻了解 JavaScript- 所有皆对象
  • 深刻了解 JavaScript-Object(对象)
  • 深刻了解 JavaScript-new 做了什么
  • 深刻了解 JavaScript-Object.create
  • 深刻了解 JavaScript- 拷贝的机密
  • 深刻了解 JavaScript- 原型
  • 深刻了解 JavaScript- 继承
  • 深刻了解 JavaScript-JavaScript 中的始皇
  • 深刻了解 JavaScript-instanceof——找祖籍
  • 深刻了解 JavaScript-Function
  • 深刻了解 JavaScript- 作用域
  • 深刻了解 JavaScript-this 关键字

正文完
 0