手写 call
, apply
和 bind
实现函数原型方法 call
, apply
和 bind
call
的实现
- 第一个参数为
null
或者undefined
时,this
指向全局对象window
,值为原始值的指向该原始值的自动包装对象,如String
、Number
、Boolean
- 为了避免函数名与上下文(
context
)的属性发生冲突,使用Symbol
类型作为唯一值 - 将函数作为传入的上下文(
context
)属性执行 - 函数执行完成后删除该属性,不然会对传入对象造成污染
- 返回执行结果
点我查看详细
方法 1
js
// 函数原型方法 `call` 的实现
// 将要改变 this 指向的方法挂到目标上执行并返回
// 注意:手写模拟实现,是否可以不使用高级语法 如 ... 运算符,改造如下
// [...arguments].slice(1) 改为 [].slice.call(arguments, 1),这里有误,不应该内部再使用 call
// Symbol() 可以用随机数 Math.random().toString() 同时避免已存在
// 在调用时,去除 ... 难以处理参数格式 (想到的办法,就是拼接 new Function 字符串了)
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
}
// context.fn = this 改为如下
// 将当前被调用的方法定义在 context.tempFn 上。(为了能以对象调用形式绑定this)
// FIXED: 避免函数名与上下文属性冲突,改为使用 Symbol
let tempFn = Symbol()
// 思考下为什么要这样做?arguments[0].fn = this
context[tempFn] = this
// 以对象调用形式调用tempFn, 此时this指向 context 也就是传入的需要绑定的this指向
// FIXED: 既然是实现 call, 不应该内部再消费 call
// const arg = [].slice.call(arguments, 1)
const args = [...arguments].slice(1)
// 执行保存的函数, 这个时候作用域就是在执行方的对象的作用域下执行,这会改变的this的指向
// 如果 args 为空数组时,这里就是无参数,无需判断 args 长度
const result = context[tempFn](...args)
// FIXED: 删除该方法,不然会对传入对象造成污染(被添加该方法)
delete context[tempFn]
return result
}
方法 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]()
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