1 call、apply、bind 用法及比照
1.1 Function.prototype
三者都是 Function
原型上的办法,所有 函数 都能调用它们
Function.prototype.call
Function.prototype.apply
Function.prototype.bind
1.2 语法
fn
代表一个函数
fn.call(thisArg, arg1, arg2, ...) // 接管参数列表
fn.apply(thisArg, argsArray) // apply 接管数组参数
fn.bind(thisArg, arg1, arg2, ...) // 接管参数列表
1.3 参数阐明
thisArg
:在 fn 运行时应用的 this 值
arg1,arg2,...
:参数列表,传给 fn 应用的
argsArray
:数组或类数组对象(比方 Arguments 对象),传给 fn 应用的
1.4 返回值
call
、apply
:同 fn 执行后的返回值
bind
:返回一个原函数的拷贝,并领有指定的 this
值和初始参数。并且返回的函数能够传参。
const f = fn.bind(obj, arg1, arg2, ...)
f(a, b, c, ...)
// 调用 f 相当于调用 fn.call(obj, ...args)
// args 是调用 bind 传入的参数加上调用 f 传入的参数列表
// 即 arg1,arg2...a,b,c...
1.5 作用
三个办法的作用雷同:扭转函数运行时的 this
值,能够实现函数的重用
1.6 用法举例
function fn(a, b) {console.log(this.myName);
}
const obj = {myName: '蜜瓜'}
fn(1, 2)
// 输入:undefined
// 因为此时 this 指向全局对象,全局对象上没有 myName 属性
fn.call(obj, 1, 2)
fn.apply(obj, [1, 2])
// 输入:蜜瓜
// 此时 this 指向 obj,所以能够读取到 myName 属性
const fn1 = fn.bind(obj, 1, 2)
fn1()
// 输入:蜜瓜
// 此时 this 指向 obj,所以能够读取到 myName 属性
1.7 三个办法的比照
办法 | 性能 | 参数 | 是否立刻执行 |
---|---|---|---|
apply |
扭转函数运行时的 this 值 |
数组 | 是 |
call |
扭转函数运行时的 this 值 |
参数列表 | 是 |
bind |
扭转函数运行时的 this 值 |
参数列表 | 否。返回一个函数 |
apply
和call
会立刻取得执行后果,而bind
会返回一个曾经指定this
和参数的函数,须要手动调用此函数才会取得执行后果apply
和call
惟一的区别就是参数模式不同- 只有
apply
的参数是 数组 ,记忆办法:apply
和数组array
都是a
结尾
2 实现 call、apply、bind
2.1 实现 call
2.1.1 易混同的变量指向
当初咱们来实现 call
办法,命名为myCall
咱们把它挂载到 Function
的原型上,让所有函数能调用这个办法
// 咱们用残余参数来接管参数列表
Function.prototype.myCall = function (thisArg, ...args) {console.log(this)
console.log(thisArg)
}
首先要明确的是这个函数中 this
、thisArg
别离指向什么
看看咱们是怎么调用的:
fn.myCall(obj, arg1, arg2, ...)
所以,myCall
中的 this
指向 fn
,thisArg
指向obj
(指标对象)
咱们的目标是让 fn
运行时的 this
(留神这个this
是fn
中的)指向 thisArg
即指标对象
换句话说就是 让fn
成为 obj
这个对象的办法来运行(外围思路)
2.1.2 简易版 call
咱们根据上述外围思路能够写出一个简略版本的myCall
Function.prototype.myCall = function (thisArg, ...args) {
// 给 thisArg 新增一个办法
thisArg.f = this; // this 就是 fn
// 运行这个办法,传入残余参数
let result = thisArg.f(...args);
// 因为 call 办法的返回值同 fn
return result;
};
call
办法的基本功能就实现了,然而显然存在问题:
- 假使有多个函数同时调用这个办法,并且指标对象雷同,则存在指标对象的
f
属性被笼罩的可能
fn1.myCall(obj)
fn2.myCall(obj)
- 指标对象上会永远存在这个属性
f
解决方案:
ES6
引入了一种新的原始数据类型Symbol
,示意举世无双的值,最大的用法是用来定义 对象的惟一属性名。delete
操作符 用于删除对象的某个属性
2.1.3 优化显著问题后的 call
优化后的myCall
:
Function.prototype.myCall = function (thisArg, ...args) {
// 生成惟一属性名,解决笼罩的问题
const prop = Symbol()
// 留神这里不能用.
thisArg[prop] = this;
// 运行这个办法,传入残余参数,同样不能用.
let result = thisArg[prop](...args);
// 运行完删除属性
delete thisArg[prop]
// 因为 call 办法的返回值同 fn
return result;
};
至此 myCall
办法的性能就绝对残缺了,然而还有一些细节须要补充
2.1.4 补充细节后的 call
如果咱们传入的 thisArg
(指标对象)是undefined
或者 null
,咱们就将其替换为指向 全局对象(MDN 文档就是这么形容的)
// 残缺代码
Function.prototype.myCall = function (thisArg, ...args) {
// 替换为全局对象:global 或 window
thisArg = thisArg || global
const prop = Symbol();
thisArg[prop] = this;
let result = thisArg[prop](...args);
delete thisArg[prop];
return result;
};
2.2 实现 apply
apply
和 call
实现思路一样,只是传参模式不同
// 把残余参数改成接管一个数组
Function.prototype.myApply = function (thisArg, args) {
thisArg = thisArg || global
// 判断是否接管参数,若未接管参数,替换为[]
args = args || []
const prop = Symbol();
thisArg[prop] = this;
// 用... 运算符开展传入
let result = thisArg[prop](...args);
delete thisArg[prop];
return result;
};
2.3 实现 bind
2.3.1 简易版 bind
实现思路:bind
会创立一个新的 绑定函数,它包装了原函数对象,调用绑定函数会执行被包装的函数
后面曾经实现了 call
和apply
,咱们能够选用其中一个来绑定this
,而后再封装一层函数,就能失去一个简易版的办法:
Function.prototype.myBind = function(thisArg, ...args) {
// this 指向的是 fn
const self = this
// 返回绑定函数
return function() {
// 包装了原函数对象
return self.apply(thisArg, args)
}
}
2.3.2 留神点
- 留神
apply
的参数模式是数组,所以咱们传入的是args
而非...args
-
为什么要在
return
前定义self
来保留this
?因为咱们须要利用闭包将
this
(即 fn)保存起来,使得myBind
办法返回的函数在运行时的this
值可能正确地指向fn
具体解释如下:
// 如果不定义 self
Function.prototype.myBind = function(thisArg, ...args) {return function() {return this.apply(thisArg, args)
}
}
const f = fn.myBind(obj) // 返回一个函数
// 为了看得分明,写成上面这种模式
// 其中 thisArg、args 保留在内存中,这是因为造成了闭包
const f = function() {return this.apply(thisArg, args)
}
// 当初咱们调用 f
// 会发现其 this 指向全局对象(window/global)// 而非咱们冀望的 fn
f()
2.3.3 让 bind 返回的函数(绑定函数)能够传参
后面说了 bind
返回的参数能够传参(见 1.4
),当初来对myBind
进行改良:
Function.prototype.myBind = function(thisArg, ...args) {
const self = this
// 返回绑定函数,用残余参数接管参数
return function(...innerArgs) {
// 合并两次传入的参数
const finalArgs = [...args, ...innerArgs]
return self.apply(thisArg, finalArgs)
}
}
2.3.4“new + 绑定函数”存在什么问题
MDN:绑定函数也能够应用
new
运算符结构,它会体现为指标函数曾经被构建结束了似的。提供的this
值会被疏忽,但前置参数仍会提供给模仿函数。
这是 MDN 文档中的形容,意思是绑定函数能够作为构造函数来创立实例,并且先前作为 bind
办法的第一个参数传入的指标对象 thisArg
生效,然而先前提供的参数仍然无效。
先来看咱们的myBind
绑定函数的外部:
// 绑定函数 f
function(...innerArgs) {
...
// 为了看得分明,这里间接将 self 写成了 fn
return fn.apply(thisArg, finalArgs)
}
用 new
来创立 f
的实例:
const o = new f()
咱们都晓得(如果不晓得看这篇:手写实现 new),new 的过程中会执行构造函数的代码,即此处绑定函数 f
中的代码会被执行。
包含 fn.apply(thisArg, finalArgs)
这句代码,并且其中的 thisArg
依然无效,这就不合乎原生 bind
办法的形容了
2.3.5 绑定函数中怎么辨别是否应用了 new
如何解决:用 new
创立绑定函数的实例时,让先前传入的 thisArg
生效
事实上对于绑定函数 f
来说,执行时的 this
值并不确定。
- 如果咱们间接执行
f
,那么绑定函数中的this
指向 全局对象。 - 如果咱们用
new
来创立f
的实例,那么f
中的this
指向 新创建的实例。(这点如果不分明看这篇:手写实现 new)
Function.prototype.myBind = function(thisArg, ...args) {
const self = this
return function(...innerArgs) {console.log(this) // 留神此处的 this 并不确定
const finalArgs = [...args, ...innerArgs]
return self.apply(thisArg, finalArgs)
}
}
// 绑定函数
const f = fn.myBind(obj)
// 如果咱们间接执行 f,那么绑定函数中的 this 指向全局对象
f()
// 如果咱们用 new 来创立 f 的实例,那么 f 中的 this 指向新创建的实例
const o = new f()
基于上述两种状况,咱们能够批改 myBind
返回的绑定函数,在函数内对 this
值进行判断,从而辨别是否应用了 new
运算符
对 myBind
进行如下更改:
Function.prototype.myBind = function(thisArg, ...args) {
const self = this
const bound = function(...innerArgs) {const finalArgs = [...args, ...innerArgs]
const isNew = this instanceof bound // 以此来判断是否应用了 new
if (isNew) { }
// 未应用 new 就跟原来一样返回
return self.apply(thisArg, finalArgs)
}
return bound
}
2.3.6 补充完绑定函数外部操作
当初咱们须要晓得如果是 new
结构实例的状况应该进行哪些操作。
看看应用原生 bind
办法是什么后果:
const fn = function(a, b) {
this.a = a
this.b = b
}
const targetObj = {name: '蜜瓜'}
// 绑定函数
const bound = fn.bind(targetObj, 1)
const o = new bound(2)
console.log(o); // fn {a: 1, b: 2}
console.log(o.constructor); // [Function: fn]
console.log(o.__proto__ === fn.prototype); // true
能够看到,new bound()
返回的是以 fn
为构造函数创立的实例。
依据这点能够补充完 if (new) {}
中的代码:
Function.prototype.myBind = function(thisArg, ...args) {
const self = this
const bound = function(...innerArgs) {const finalArgs = [...args, ...innerArgs]
const isNew = this instanceof bound // 以此来判断是否应用了 new
if (isNew) {
// 间接创立 fn 的实例
return new self(...finalArgs)
}
// 未应用 new 就跟原来一样返回
return self.apply(thisArg, finalArgs)
}
return bound
}
const bound = fn.myBind(targetObj, 1)
const o = new bound(2)
这样,const o = new bound(2)
相当于 const o = new self(...finalArgs)
,因为构造函数如果显式返回一个对象,就会间接笼罩new
过程中创立的对象(不晓得的话能够看看这篇:手写实现 new)
2.3.7 残缺代码
Function.prototype.myBind = function(thisArg, ...args) {
const self = this
const bound = function(...innerArgs) {const finalArgs = [...args, ...innerArgs]
const isNew = this instanceof bound
if (isNew) {return new self(...finalArgs)
}
return self.apply(thisArg, finalArgs)
}
return bound
}
事实上,这段代码仍存在和原生 bind
出入的中央,然而这里只是表白实现 bind
的一个整体思路,不用奢求完全一致
3 补充
apply
、call
办法还有一些细节咱们没有实现:如果这个函数(fn)处于非严格模式下,则指定为null
或undefined
时会主动替换为指向全局对象,原始值会被包装(比方1
会被包装类Number
包装成对象)。bind
办法也是函数柯里化的一个利用,不相熟柯里化的能够看看这篇内容:JS 函数柯里化
公众号【前端嘛】获取更多前端优质内容