手写 call
, apply
和 bind
实现函数原型方法 call
, apply
和 bind
call
的实现
- 第一个参数为
null
或者undefined
时,this
指向全局对象window
,值为原始值的指向该原始值的自动包装对象,如String
、Number
、Boolean
- 为了避免函数名与上下文 (
context
) 的属性发生冲突,使用Symbol
类型作为唯一值 - 将函数作为传入的上下文 (
context
) 属性执行 - 函数执行完成后删除该属性,不然会对传入对象造成污染
- 返回执行结果
点我查看详细
方法 1
js
// 函数原型方法 `call` 的实现
// 将要改变 this 指向的方法挂到目标上执行并返回
Function.prototype.myCall = function (context) {
if (typeof this !== 'function') {
throw new TypeError('not funciton')
}
// context = context || window 改为如下 (这里使用 globalThis 处理 nodejs 执行无 window 的情况)
if ([undefined, null].includes(context)) {
context = globalThis || window
}
let tempFn = Symbol()
context[tempFn] = this
const args = [...arguments].slice(1)
const result = context[tempFn](...args)
delete context[tempFn]
return result
}
// prettier-ignore
// 其他一些思考或说明
// 1. [...arguments].slice(1) 等同 [].slice.call(arguments, 1),
// 但这里不推荐使用,手写 call 不应该内部再使用 call 方法
// 2. Symbol() 也可以用随机数 Math.random().toString() 方法同时辅助缓存,避免已存在
// 为什么要使用 Symbol()?避免函数名与上下文属性冲突
// 3. 在调用时,能不使用 ... 语法吗?这会增加难度,难以处理参数格式
// 想到的办法,就是拼接 eval 或 new Function 了,实现如下
// `
// let args = [];
// for (let i = 1, len = arguments.length; i < len; i++) {
// args.push('arguments[' + i + ']');
// }
// let result = eval('context.fn(' + args + ')'); // 相当于传参 ...args
// `
// 4. context[tempFn] = this 为什么要这样做?
// 将当前被调用的方法定义在 context.tempFn 上。(为了能以对象调用形式绑定 this)
// 以对象调用形式调用 tempFn, 此时 this 指向 context 也就是传入的需要绑定的 this 指向
// 5. context[tempFn](...args)
// 执行保存的函数,这个时候作用域就是在执行方的对象的作用域下执行,这会改变的 this 的指向
// 如果 args 为空数组时,这里写法兼容无参数,无需判断 args 长度
// 6. delete context[tempFn] 为什么还要执行这个?
// 执行这个,是为了删除该方法,不然会对传入对象造成污染(被添加该方法)
方法 2
js
Function.prototype.myCall = function (context, ...args) {
const fn = Symbol()
try {
context[fn] = this
return context[fn](...args)
} catch (e) {
// Turn primitive types into complex ones 1 -> Number, thanks to Mark Meyer for this.
context = new context.constructor(context)
context[fn] = this
}
return context[fn](...args)
}
Function.prototype.myApply = function (context, args) {
return this.call(context, ...args)
}
Function.prototype.myBind = function (context, ...args) {
return (...args2) => this.call(context, ...args, ...args2)
}
apply
的实现
- 前部分与
call
的实现一样 - 第二个参数可以不传,但类型必须为数组或者类数组
点我查看详细
js
// 函数原型方法 `apply` 的实现
// 模拟实现,能否不用高级语法,如 ... 语法
Function.prototype.myApply = function (context) {
if (typeof this !== 'function') {
throw new TypeError('not funciton')
}
const arr = arguments[1]
if (typeof arr !== 'undefined' && !Array.isArray(arr)) {
throw new TypeError('not array')
}
if ([undefined, null].includes(context)) {
context = globalThis || window
}
let tempFn = Symbol()
context[tempFn] = this
// arr 可能未传值,... 语法是兼容的
// const result = arr ? context[tempFn](...arr) : context[tempFn]()
const result = context[tempFn](...arr)
delete context[tempFn]
return result
}
bind
的实现
需要考虑:
bind
除了this
外,还可传入多个参数;bind
创建的新函数可能传入多个参数;- 新函数可能被当做构造函数调用;
- 函数可能有返回值;
实现方法:
bind
方法不会立即执行,需要返回一个待执行的函数;(闭包)- 实现作用域绑定(
apply
) - 参数传递(
apply
的数组传参) - 当作为构造函数的时候,进行原型继承
点我查看详细
js
// https://www.smashingmagazine.com/2014/01/understanding-javascript-function-prototype-bind/
// 非常简单的示例
// 方案一
Function.prototype.simpleBind = function simpleBind(scope) {
var fn = this
return function simpleBinded() {
return fn.apply(scope)
}
}
// 方案二
Function.prototype.fakeBind = function (obj, ...args) {
return (...rest) => this.call(obj, ...args, ...rest)
}
// 函数原型方法 `bind` 的实现
Function.prototype.myBind = function myBind(context, ...args) {
if (typeof this !== 'function') {
// if (!(this instanceof Function)) {
// 当前调用 bind 方法的不是函数
throw new TypeError('this is not a function type.')
}
// 表示当前函数 this
const _this = this
// 判断有没有传参进来,若为空则赋值 [] 或直接使用 args
const arg = [...arguments].slice(1)
return function newFn(...newFnArgs) {
// 处理函数使用 new 的情况
if (this instanceof newFn) {
return new _this(...arg, ...newFnArgs)
} else {
return _this.apply(context, [...arg, ...newFnArgs])
}
}
}
function es5Bind() {
//arguments are just Array-like but not actual Array. Check MDN.
let bindFn = this,
bindObj = arguments[0],
bindParams = [].slice.call(arguments, 1) //----> [arg1,arg2..] Array.isArray --> true
return function () {
bindFn.apply(bindObj, bindParams.concat([].slice.call(arguments)))
}
}
function es6Bind(...bindArgs) {
let context = this
return function (...funcArgs) {
context.call(bindArgs[0], ...[...bindArgs.slice(1), ...funcArgs])
// we can use above line using call (OR) below line using apply
//context.apply(bindArgs[0], [...(bindArgs.slice(1)), ...funcArgs]);
}
}
// Function.prototype.es5Bind = es5Bind;
// Function.prototype.es6Bind = es6Bind;
扩展阅读
While each browser has its own source code for implementing Javascript, you can find how many of the native Javascript functions are implemented with the ECMA specifications found here:
- For specs of
apply
, see: 19.2.3.1 - For specs of
bind
, see: 19.2.3.2 - For specs of
call
, see: 19.2.3.3
If you're interested for example, how Node implemented apply, you can dig into their source code on Github here: https://github.com/nodejs/node