前言
从来没有想过一个成语——“借尸还魂”,就可以把apply、call、bind原理分析的透透的,不由感叹世间万物如此互通,相互关联,接下来让我们一起进入今日的学习。
apply、call、bind基础知识
语法
function.apply(thisArg, [param1,param2,...])
function.call(thisArg, param1, param2, ...)
function.bind(thisArg, param1, param2, ...)
基本逻辑解释,说白了就是借尸还魂。我是thisArg,一个感情丰富的灵魂,缺乏肉体。想用function老哥你的肉体,但不要你的灵魂,通过借你肉体上身,使用你的肉体,输出我自己灵魂的思想。(我看到自己的这解释,我都惊了。)
调用肉体(function函数)时,涉及this绑定问题,遵循this的指向取决于函数在哪里被调用。
参数
thisArg
- 可选;
- function的this指向thisArg对象。特殊情况(bind中,如果使用
new
运算符构造绑定函数,则忽略该值); - 非严格模式下,值为
null
或undefined
时会自动替换为指向全局对象; - 值为原始值(数字,字符串,布尔值),
this
会指向该原始值的自动包装对象,如String
、Number
、Boolean
;特殊情况(当使用bind
在setTimeout
中创建一个函数(作为回调提供)时,作为thisArg
传递的任何原始值都将转换为object
)。
param
- 可选;
- apply 一个数组或者类数组对象,call / bind 一个指定的参数列表。
返回值
- call / apply:
fun
执行的结果,若该方法没有返回值,则返回undefined
; - bind:返回
fun
的拷贝,并拥有指定的this
值和初始参数;
作用
借用方法,改变函数执行时的this指向。
注意事项
call、apply和bind是挂在Function对象上的三个方法,只有函数才有这些方法。
区别
执行:
- call / apply改变了函数的this上下文后马上执行该函数;
- bind则是返回改变了上下文后的函数,不执行该函数;
返回值:
- call / apply 返回function的执行结果;
- bind返回function的拷贝,并指定了function的this指向,保存了function的参数;
使用选择
call,apply的效果完全一样,但是在应用选择上有一些不一样的地方:
- 参数数量或者顺序确定时就用call,不确定就用apply;
- 考虑参数数量,数量多就用合成数组,使用apply,反之使用call;
手写call、apply、bind
手写call
根据 “借尸还魂”理论,以下是手写call的步骤:
- 传参正确性判断处理(灵魂校验修正);
- 根据call的定义,为
context
设置临时属性,绑定函数this(灵魂进入肉体); - 通过隐式绑定(待补充)执行函数,并传递参数。(灵魂控制肉体干活);
- 删除临时属性,返回函数执行结果(灵魂脱离肉体,把干活的成果交出来)
Function.prototype.myCall = function (context, ...arr) {
// 第一步
if(context === null || context === undefined) {
context = window
} else {
// 值为基本数据类型时,this会指向该基础数据类型的实例对象
context = Object(context)
}
// 第二步
const specialPrototype = Symbol('特殊属性Symbol')
context[specialPrototype] = this;
// 第三步
const result = context[specialPrototype](...arr)
// 第四步
delete context[specialPrototype];
return result;
}
手写apply
手写apply的思路同call差不多,不在详讲步骤。主要的不同点是处理参数数组问题,需要判断是否为类数组对象。
Function.prototype.myCall = function (context) {
// 第一步
if (context === null || context === undefined) {
context = window;
} else {
// 值为基本数据类型时,this会指向该基础数据类型的实例对象
context = Object(context);
}
// 第二步
const specialPrototype = Symbol("特殊属性Symbol");
context[specialPrototype] = this;
// 第三步
// 获取参数数组 因为默认传入是数组,所以取arguments的索引为1的值就好
let args = arguments[1];
const result = undefined;
// JavaScript权威指南判断是否为类数组对象
function isArrayLike(o) {
if (
o && // o不是null、undefined等
typeof o === "object" && // o是对象
isFinite(o.length) && // o.length是有限数值
o.length >= 0 && // o.length为非负值
o.length === Math.floor(o.length) && // o.length是整数
o.length < 4294967296
)
// o.length < 2^32
return true;
else return false;
}
if (args) {
// 是否传递第二个参数
if (!Array.isArray(args) && !isArrayLike(args)) {
throw new TypeError("myApply 第二个参数不为数组并且不为类数组对象抛出错误");
} else {
// 转为数组
args = Array.from(args);
// 执行函数并展开数组,传递函数参数
result = context[specialPrototype](...args);
}
} else {
// 执行函数
result = context[specialPrototype]();
}
// 第四步
delete context[specialPrototype];
return result;
};
手写bind
根据bind的基本定义—-bind是返回改变了上下文后的函数,不执行该函数;套用**“借尸还魂”理论**,以下是手写bind的思路步骤:
- 拷贝源函数;
- 构造绑定函数,运用 “借尸还魂”,新函数内部return 改变了函数的this上下文后的执行结果;
- 返回绑定函数;
注意事项:
- 调研bind 绑定的函数时,需要注意绑定函数是否通过 new 调用。
- 如果源函数有原型(
prototype
),需要将绑定函数的原型指向源函数的原型,这样保证bind拷贝函数的完整性; - 判断绑定this + 传递参数(bind绑定是传参,以及返回绑定函数后传参)。
Function.prototype.myBind = function (context, ...params) {
// 第一步
const that = this;
// 第二步
const fnToBind = function (...fnToBindParams) {
// 判断是否通过new调用,为真则是被new调用
const isNew = that instanceof fnToBind;
// new调用就绑定到this上,否则就绑定到传入的context上(使用Object()包裹,可以包装原始值对象)
const _context = isNew ? this : Object(context);
// 用call调用源函数绑定this的指向并传递参数,返回执行结果
return that.call(_context, ...params, ...fnToBindParams);
};
if (that.prototype) {
// 复制源函数的prototype
fnToBind.prototype = Object.create(that.prototype);
}
// 第三步
return fnToBind;
};
以上就是我们今日的学习,完结撒花✿✿ヽ(°▽°)ノ✿~