call、apply、bind 实现原理
这篇文章主要探究 call、apply、bind 的实现原理,对于使用方法不做太多概述。如果有不太了解的同学可以查看JavaScript MDN 官方文档 call apply bind 详解
前言
我们都知道 call、apply、bind 三个方法是用来改变 this 指向的,但是也只局限于会用状态。具体的实现原理,却没有思考过。所以有必要研究一下。在明白起原理的情况下,才能用的更好。
具体实现 call
众所周知这三个方法都是提供给 函数使用的,函数通过这三个方法调用把 this 传入进去就可以改变其 this 指向。通过这个特性我们可以推断出: 这三个方法都是写在 Function
对象的原型上的。也就是 Function.prototype
上
下边我们先在这个原型上写一个自己的 call 方法就叫 newCall
:
function person () {
console.log(this.name)
}
let obj = {
name: '渣渣辉'
}
Function.prototype.newCall = function (obj) {
console.log(this) // 这里的this指向的是 person 函数
console.log(obj) // {name: '渣渣辉'}
}
person.newCall(obj)
运行上边的代码,我们会发现 newCall 原型方法里的 this 指向的是 person
函数, 因为 newCall 这个方法是定义在 Function
对象原型上的,并且是 person 函数调用了 newCall 方法,这就印证了那句话,谁调用了这个方法 this 就指向谁。然后只要在 newCall 方法中想办法执行 this 指向的这个 person 函数就行。
然后我们可以再 obj 这个对象上添加一个属性,然后把 this 指向这个属性,然后在执行这个属性就可以了: 代码如下
function person () {
console.log(this.name) // 渣渣辉
}
let obj = {
name: '渣渣辉'
}
Function.prototype.newCall = function (obj) {
console.log(this)
console.log(obj)
// 在 obj 对象上添加一个属性 fn, 并且绑定 this (也就是 person) person 函数是 引用数据类型,是存在堆空间,栈空间通过指针指向 堆空间
obj.fn = this
// 执行 fn 函数
obj.fn()
// 防止作用域污染,使用完之后删除
delete obj.fn
}
person.newCall(obj)
这时候 顶部的 person 函数中 console.log(this.name)
执行的结果就是 渣渣辉
基本的 call 的操作已经完成了,但是还需要完善一下:
- call() 调用的时候 第一个参数一般是 this 或者一个对象,或者是 null 或者直接省略不传,这种情况下this 的值将会被绑定为全局对象。
- call(this, 1, 2, 3, 4) 接受一个参数列表
根据上边的特性再完善一下 newCall 方法:
function person (a, b, c, d) {
console.log(this.name) // 渣渣辉
console.log(a, b, c, d) // 1, 2, 3, 4
}
let obj = {
name: '渣渣辉'
}
Function.prototype.newCall = function (obj) {
// 如果参数为 为 null ,则默认为 window, 即访问全局作用域对象
var obj = obj || window
// 在 obj 对象上添加一个属性 fn, 并且绑定 this (也就是 person) person 函数是 引用数据类型,是存在堆空间,栈空间通过指针指向 堆空间
obj.fn = this
// 截取作用域对象参数后面的参数
var args = [...arguments].slice(1)
// 执行 fn 函数
obj.fn(...args)
// 防止作用域污染,使用完之后删除
delete obj.fn
}
// person.newCall(obj)
person.newCall(obj, 1, 2, 3, 4)
现在离成功又进了一步,但是有时候被call 调用的那个函数也是有返回值的,所以还需要完善一下
function person (a, b, c, d) {
console.log(this.name) // 渣渣辉
console.log(a, b, c, d) // 1, 2, 3, 4
// 有返回值
return {
name: this.name,
a: a, b: b, c: c, d: d
}
}
let obj = {
name: '渣渣辉'
}
Function.prototype.newCall = function (obj) {
// 如果参数为 为 null ,则默认为 window, 即访问全局作用域对象
var obj = obj || window
// 在 obj 对象上添加一个属性 fn, 并且绑定 this (也就是 person) person 函数是 引用数据类型,是存在堆空间,栈空间通过指针指向 堆空间
obj.fn = this
// 截取作用域对象参数后面的参数
var args = [...arguments].slice(1)
// 执行 fn 函数
var result = obj.fn(...args)
// 防止作用域污染,使用完之后删除
delete obj.fn
// 执行完成后返回结果
return result
}
// person.newCall(obj)
// person.newCall(obj, 1, 2, 3, 4)
var res = person.newCall(obj, 1, 2, 3, 4)
console.log(res) // {name: "渣渣辉", a: 1, b: 2, c: 3, d: 4}
到此,newCall 方法基本上已经完成了, 上边的写法中obj.fn(...args)
使用了 ES6 的扩展运算符,但是 call 方法在没有扩展运算符的年代就已经实现了,所以我们有必要在探究一下, 最终找到一个方法 eval()
方法运行 js 然后得到结果,最终版本如下
Function.prototype.newCall = function (obj) {
// 如果参数为 为 null ,则默认为 window, 即访问全局作用域对象
var obj = obj || window
// 在 obj 对象上添加一个属性 fn, 并且绑定 this (也就是 person) person 函数是 引用数据类型,是存在堆空间,栈空间通过指针指向 堆空间
obj.fn = this
// ES 5 的实现方法 ===============
var args = []
for (var i = 1; i < arguments.length; i++) {
args.push('arguments['+ i +']')
}
var result = eval('obj.fn('+ args +')')
// ===================
//ES 6 ================
// 截取作用域对象参数后面的参数
// var args = [...arguments].slice(1)
// // 执行 fn 函数
// var result = obj.fn(...args)
// ===================
// 防止作用域污染,使用完之后删除
delete obj.fn
// 执行完成后返回结果
return result
}
apply
说完了call 再说下 apply ,其实这两个方法功能基本上都是相同的,不同的地方是传参方式不同,
call 接收一个参数列表作为参数, apply 方法接受的是一个包含多个参数的数组。差别不大我们直接把 实现的 newCall 方法拿过来改改就好了,所以这里不在过多叙述实现过程了,直接上代码:
Function.prototype.newApply = function (obj, arr) {
var obj = obj || window
var result
obj.fn = this
if (!arr) {
obj.fn()
} else {
// ES 5 实现方式 =========
var args = []
for (var i = 0; i < arr.length; i++) {
args.push('arr['+ i +']')
}
result = eval('obj.fn('+ args +')')
// =========
// ES6 =========
// result = obj.fn(...arr)
// =========
}
delete obj.fn
return result
}
bind
- bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被bind的第一个参数指定,其余的参数将作为新函数的参数供调用时使用
- bind() 绑定的函数 支持
new 实例化
完整代码如下:
Function.prototype.newBind = function (obj) {
var self = this
// 截取绑定时的参数
var arge1 = Array.prototype.slice.call(arguments, 1)
// 定义中专构造函数,用于通过原型连接绑定后的函数和调用 bind 的函数
var F = function () {}
// 定义返回的新函数
var newF = function () {
// 截取调用时的参数
var arge2 = Array.prototype.slice.call(arguments)
// 参数合并
var argeSum = arge1.concat(arge2)
// 支持 new 实例化, 判断是否使用 new 关键字
// 改变作用域,注:aplly/call是立即执行函数,即绑定会直接调用
if (this instanceof F) {
return self.apply(this, argeSum)
} else {
return self.apply(obj, argeSum)
}
}
// 将调用函数的原型赋值到中专函数的原型上
F.prototype = self.prototype
// 通过原型的方式继承调用函数的原型
newF.prototype = new F
return newF
}
这是《JavaScript Web Application》一书中对 bind() 的实现:通过设置一个中转构造函数 F,使绑定后的函数与调用 bind() 的函数处于同一原型链上,用 new 操作符调用绑定后的函数,返回的对象也能正常使用 instanceof,因此这是最严谨的 bind() 实现
然后我们来验证一下
function person (a, b, c, d) {
console.log(this.name) // 渣渣辉
console.log(a, b, c, d) // 1, 2, 3, 4
// 有返回值
return {
name: this.name,
a: a, b: b, c: c, d: d
}
}
let obj = {
name: '渣渣辉'
}
person.newBind(obj, 1, 2, 3)(4)
var bb = person.newBind(this, '点赞', '收藏')
var aa = new bb('充电')
// ==================
var people = {
name: '张三',
getName: function (age) {
return this.name + age
}
}
var temp = people.getName
var context = temp.newBind(people)
console.log(context('18岁')) // 张三18岁
都能正确运行。