前言
在初级前端面试中,只会问 call
、apply
、bind
的概念和用法。在中高级前端的面试中,不会问这么低级的问题,一般会叫你手写 call
、apply
、bind
,这样即考察了你对 call
、apply
、bind
的概念和用法的掌握程度,也考察了你对 Javascript 的掌握程度。
其实 call
、apply
、bind
的内部实现逻辑大体一致,只是对于 call
、apply
、bind
之间不同的用法做了对应的逻辑处理,那么只要会手写其中一个方法,其余两个也就会了。
这里只提一下 call
、apply
、bind
三者之间的不同,至于其详细的概念和用法可以看这篇专栏。
-
第二个参数不同,
call
和bind
接收一个参数列表,但apply
不一样,接收一个包含多个参数的数组。 -
执行返回不同,
call
和apply
返回的是调用函数执行后的值,bind
返回的是函数需要再次调用。
本专栏将从青铜到王者来介绍怎么手写 call
、apply
、bind
,以及每个段位对应的能力。
青铜段位
Function.prototype.myCall = function(target,params){
this(params);
}
Function.prototype.myApply = function(target,params){
this(params);
}
Function.prototype.myBind = function(target,params){
return function (){
this.(params)
}
}
这个段位的代码,只实现了 call
、apply
、bind
调用函数的能力,而且 call
从第二个参数开始可以接收很多参数,这里只接收了一个。还有 apply
第二个参数是个数组,怎么可以直接传给调用的函数。
这明显不合格,得多打排位。
白银段位
Function.prototype.myCall = function() {
const params = [...arguments]
const args = params.slice(1)
this(...args)
}
Function.prototype.myApply = function() {
const params = [...arguments]
const thisArg = params[0];
const args = params[1];
args? thisArg.fn(...args) : thisArg.fn()
}
Function.prototype.myBind = function(){
const params = [...arguments]
const args = params.slice(1)
const _this = this;
return function (){
_this(...args)
}
}
利用 arguments
来处理函数实参的数量无法固定的场景。arguments
是个类数组,用 ES6 中扩展运算符...
把 arguments
转成真正的数组赋值给常量 params
。经过处理把 params
中属于传给调用函数的参数集合赋值给常量 args
,因为 args
是个数组,故又使用 ES6 中扩展运算符 ...
,把 args
数组转为用逗号分隔的参数序列,再传给调用函数。
但是还没实现 call
、apply
、bind
最重要的功能改变调用函数执行时的 this
指向。还是得继续打排位。
虽然代码逻辑有错误,但是在这里还是可以向面试官展示你的三个编程能力。
- 对函数内部
arguments
对象的了解和应用。 - ES6 扩展运算符的应用。
- 如何处理函数的参数不固定的能力
黄金段位
怎么改变函数调用时的 this
指向呢?首先要了解 this
指向哪里了。
在 JavaScript 中函数调用的形式有普通函数、对象的方法、构造函数、箭头函数这几种。其中箭头函数是 ES6 定义,其 this
指向定义箭头函数的上下文,使用 ES5 定义 call
、apply
、bind
也无法改变其指向问题,这里先不管箭头函数的调用了。函数的每种调用的形式对应的 this
指向也不同。
- 普通函数:
this
指向全局对象,对于浏览器而言则是window
对象。
var color = 'red';
function sayColor() {
console.log(this.color)
}
sayColor();
在这个例子中,sayColor
函数调用时,this
指向 window
,那么在 sayColor
函数中 this.color
相当 window.color
,var color = 'red'
定义 window.color
为 red
。故控制台会打印出 red
。
- 对象的方法:
this
指向该对象,此时便可以通过this
访问对象的其他成员变量或方法。
var obj = {
color = 'green'
}
function sayColor() {
console.log(this.color)
}
obj.sayColor();
在这个例子中,sayColor
是作为对象 obj
的方法调用,那么 this
就是对象 obj
,this.color
相当 obj.color
,故 sayColor
函数执行 console.log(this.color)
,会在控制台会打印出 green
。
- 构造函数:作为普通函数调用时,
this
指向全局对象,也就是window
。用 new 来调用,this
指向构造函数实例化对象。
从上述可以得知当函数作为对象的方法调用时,this
指向该对象。那么只要把一个函数赋值给一个对象的属性,例如把函数 sayColor
赋值给 obj.fn
,函数 sayColor
就变成对象 obj
的一个方法,执行 obj.fn()
相当调用函数 sayColor
,此时函数 sayColor
的 this
指向对象 obj
。如果再把函数 sayColor
赋值给 target.fn
,执行 target.fn()
时,函数 sayColor
的 this
就变成指向对象 target
,这样就能起到改变 this
的作用。所以代码可以这么实现。
Function.prototype.myCall = function() {
const params = [...arguments]
const thisArg = params[0];
thisArg.fn = this;
const args = params.slice(1)
thisArg.fn(...args)
}
Function.prototype.myApply = function() {
const params = [...arguments]
const thisArg = params[0];
thisArg.fn = this;
const args = params[1];
args? thisArg.fn(...args) : thisArg.fn()
}
Function.prototype.myBind = function() {
const params = [...arguments]
const thisArg = params[0];
thisArg.fn = this;
const args = params.slice(1)
return function() {
thisArg.fn(...args)
}
}
在这里可以向面试官展示你的三个编程能力。
- 对
this
概念的了解 - 对函数调用形式及调用时
this
指向的了解 - 如何改变函数调用时
this
指向的能力
铂金段位
上面段位代码虽然实现了 call
、apply
、bind
的基本功能,但是未考虑不传参数的场景。
在调用 call
、apply
、bind
时,若其第一个参数不传值时,就把 this
要指向全局对象。用 (function(){return this})()
来获取当前环境的全局对象。
Function.prototype.myCall = function() {
const params = [...arguments]
let thisArg = params[0];
if(!thisArg){
thisArg =(function(){return this})()
}
thisArg.fn = this;
const args = params.slice(1)
thisArg.fn(...args)
}
Function.prototype.myApply = function() {
const params = [...arguments]
let thisArg = params[0];
if(!thisArg){
thisArg =(function(){return this})()
}
thisArg.fn = this;
const args = params[1];
args? thisArg.fn(...args) : thisArg.fn()
}
Function.prototype.myBind = function() {
const params = [...arguments]
let thisArg = params[0];
if(!thisArg){
thisArg =(function(){return this})()
}
thisArg.fn = this;
const args = params.slice(1)
return function() {
thisArg.fn(...args)
}
}
在这里可以向面试官展示你的两个编程能力。
- 考虑问题的全面性
- 如何根据当前环境来获取全局对象的能力
砖石段位
铂金段位中的代码,看起来已经完全实现了 call
、apply
、bind
方法功能。但是在这个段位应该深入去了解了 call
、apply
、bind
的用法,具体可以去 MDN 上查看。
function.call(thisArg, arg1, arg2, ...)
function.apply(thisArg, [argsArray])
function.bind(thisArg[, arg1[, arg2[, ...]]])
在 MDN 中介绍了参数 thisArg
的定义
在 function 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
从上述的定义中。可以得知,call
、apply
、bind
内部中有以下一些逻辑:
- 当参数
thisArg
值为null
或undefined
时,在非严格模式下,才会把thisArg
指向全局对象。 - 当参数
thisArg
是原始值时,在非严格模式下,才会去调用包装函数包装参数thisArg
。 - 在严格模式下,若参数
thisArg
值为null
或undefined
,则直接把指定的参数列表arg...
传入调用函数执行。
严格模式下,有个很明显的特征就是 this
为 undefined
。故可以用以下逻辑来判断是不是严格模式。
let isStrict = (function(){return this === undefined}())
那么在严格模式下 isStrict
为 true
。
又因为 call
、apply
、bind
是在 ES6之前定义的。故其原始值只有 Null、Undefined、Boolean、Number、String 这五种。
此外只有 Boolean、Number、String 这三种原始值有包装函数,分别是 new Boolean()
、new Boolean()
那么具体实现代码如下:
Function.prototype.myCall = function() {
const params = [...arguments];
const args = params.slice(1);
let thisArg = params[0];
let isStrict = (function() {
return this === undefined
}())
if (!isStrict) {
if (thisArg === undefined || thisArg === null) {
thisArg = (function() { return this })()
} else {
const thisArgType = typeof thisArg
if (thisArgType === 'number') {
thisArg = new Number(thisArg)
} else if (thisArgType === 'string') {
thisArg = new String(thisArg)
} else if (thisArgType === 'boolean') {
thisArg = new Boolean(thisArg)
}
}
}
if (thisArg === undefined || thisArg === null) {
return this(...args)
}
thisArg.fn = this;
thisArg.fn(...args)
}
Function.prototype.myApply = function() {
const params = [...arguments];
const args = params[1];
let thisArg = params[0];
let isStrict = (function() {
return this === undefined
}())
if (!isStrict) {
if (thisArg === undefined || thisArg === null) {
thisArg = (function() { return this })()
} else {
const thisArgType = typeof thisArg
if (thisArgType === 'number') {
thisArg = new Number(thisArg)
} else if (thisArgType === 'string') {
thisArg = new String(thisArg)
} else if (thisArgType === 'boolean') {
thisArg = new Boolean(thisArg)
}
}
}
if (thisArg === undefined || thisArg === null) {
return this(...args)
}
thisArg.fn = this;
args? thisArg.fn(...args) : thisArg.fn()
}
Function.prototype.myBind = function() {
const params = [...arguments];
const args = params.slice(1);
let thisArg = params[0];
let isStrict = (function() {
return this === undefined
}())
if (!isStrict) {
if (thisArg === undefined || thisArg === null) {
thisArg = (function() { return this })()
} else {
const thisArgType = typeof thisArg
if (thisArgType === 'number') {
thisArg = new Number(thisArg)
} else if (thisArgType === 'string') {
thisArg = new String(thisArg)
} else if (thisArgType === 'boolean') {
thisArg = new Boolean(thisArg)
}
}
}
if (thisArg === undefined || thisArg === null) {
const _this = this;
return function() {
_this(...args)
}
}
thisArg.fn = this;
return function() {
thisArg.fn(...args)
}
}
在这里可以向面试官展示你的三个编程能力。
- 严格模式和非严格模式的判别能力
- 了解原始值及其定义时间
- 如何包装函数的能力
星耀段位
上述段位的代码,已经很完整了,但是还要有两个可以优化的地方。
-
如果参数
thisArg
原本有fn
这个属性,那执行thisArg.fn = this;
是不是把属性fn
原先的值覆盖。在 ES6 中可以用 Symbol 来解决。 -
在非严格模式下,当参数
thisArg
为null
或undefined
没必要特意把thisArg
执行全局对象,因为此时被调用函数当作普通函数的形式调用,return this(...args)
,那么被调用函数的this
自然指向全局对象。
Function.prototype.myCall = function() {
// 通过arugments对象,我们能拿到所有实参
const params = [...arguments];
const args = params.slice(1);
let thisArg = params[0];
// 判断是不是严格模式
let isStrict = (function() {
return this === undefined
}())
if (!isStrict) {
// 如果是其他原始值,需要通过构造函数包装成对象
const thisArgType = typeof thisArg
if (thisArgType === 'number') {
thisArg = new Number(thisArg)
} else if (thisArgType === 'string') {
thisArg = new String(thisArg)
} else if (thisArgType === 'boolean') {
thisArg = new Boolean(thisArg)
}
}
// 第一参数值为undefined或null,被调用函数按普通函数形式调用
if (thisArg === undefined || thisArg === null) {
return this(...args)
}
// 创建一个全局唯一属性 fn
const fn = Symbol(thisArg)
// 改变被调用函数的 this 指向到 thisArg 上
thisArg[fn] = this;
// 返回目标函数执行的结果
thisArg[fn](...args)
}
Function.prototype.myApply = function() {
const params = [...arguments];
const args = params[1];
let thisArg = params[0];
let isStrict = (function() {
return this === undefined
}())
if (!isStrict) {
const thisArgType = typeof thisArg
if (thisArgType === 'number') {
thisArg = new Number(thisArg)
} else if (thisArgType === 'string') {
thisArg = new String(thisArg)
} else if (thisArgType === 'boolean') {
thisArg = new Boolean(thisArg)
}
}
if (thisArg === undefined || thisArg === null) {
return args ? this(...args) : this()
}
const fn = Symbol(thisArg)
thisArg[fn] = this;
args ? thisArg[fn](...args) : thisArg[fn]()
}
Function.prototype.myBind = function() {
const params = [...arguments];
const args = params.slice(1);
let thisArg = params[0];
let isStrict = (function() {
return this === undefined
}())
if (!isStrict) {
const thisArgType = typeof thisArg
if (thisArgType === 'number') {
thisArg = new Number(thisArg)
} else if (thisArgType === 'string') {
thisArg = new String(thisArg)
} else if (thisArgType === 'boolean') {
thisArg = new Boolean(thisArg)
}
}
if (thisArg === undefined || thisArg === null) {
const _this = this;
return function() {
_this(...args)
}
}
const fn = Symbol(thisArg)
thisArg[fn]= this;
return function() {
thisArg[fn](...args)
}
}
在这里可以向面试官展示你的两个编程能力。
- 代码逻辑的严谨性
- 了解 ES6 中 Symbol 的概念和应用
王者段位
在上述段位代码中,对参数的处理用到 ES6 的扩展运算符 ...
,若参数 thisArg
上属性 fn
已定义后防止被覆盖用到 ES6 的 Symbol。
但是 call
、apply
、bind
都是在 ES5 中定义的,那么不要用 ES6 的方法,怎么实现呢。
这里就不做介绍,有兴趣可以思考以下,在评论中,留下答案,王者属于你。
总结
综上所述,面试官叫你手写call
、apply
、bind
,其实是要考察你各方面的能力。例如
- 白银段位
- 对函数内部
arguments
对象的了解和应用。 - ES6 扩展运算符的应用。
- 如何处理函数的参数不固定的能力
- 对函数内部
- 黄金段位
- 对
this
概念的了解 - 对函数调用形式及调用时
this
指向的了解 - 如何改变函数调用时
this
指向的能力
- 对
- 铂金段位
- 考虑问题的全面性
- 如何根据当前环境来获取全局对象的能力
- 砖石段位
- 严格模式和非严格模式的判别能力
- 了解原始值及其定义时间
- 如何包装函数的能力
- 星耀段位
- 代码逻辑的严谨性
- 了解 ES6 中 Symbol 的概念和应用
所以不要去死记硬背一些手写代码的面试题,最好自己动手写一下,看看自己达到那个段位了。