前言:以下记录call,apply方法,本文将以简单且清晰逻辑的带你一步步理解如何手写这些借用函数。不难请往下看
call
首先Fn.call(context, a,b,c,d)
简单理解一下就是将函数Fn
挂载到context对象
上,并且通过context对象
调用函数Fn
并且传入参数后执行得到的结果。
1. 第一版本call
Function.prototype.myCall_1 = function() {
let context = arguments[0] // 取上下文
let arg = Array.from(arguments).slice(1) // 取入参
const randomKey = 'key_' + Math.random() // 创建一个唯一key
context[randomKey] = this // 将 Fn 挂载到 context 上
const res = context[randomKey](...arg) // 执行得到结果
return res // 返回结果
}
复制代码
试一下
var obj = {name:1,age:2}
var name = 'Leo', age = 18
function Fn(height) {
console.log('name:', this.name, 'age:', this.age,'height:',height)
}
Fn() // name: Leo age: 18 height: undefined
Fn.myCall_1(obj, '80cm') // name: 1 age: 2 height: 80cm
复制代码
第一版本的完成了其基本实现call
的模拟
关于第一版存在如下一些问题
myCall_1
的randomKey
是否可以让其做到真正唯一?- 每一次执行
myCall_1
都会在context
上挂在一个额外属性 - 关于
context
传参问题
2. 第二版本call
问题1:
Function.prototype.myCall_2 = function() {
let context = arguments[0] // 取上下文
let arg = Array.from(arguments).slice(1) // 取入参
const randomKey = Symbol('call') // --------问题1------->通过Symbol声明一个唯一的key值
context[randomKey] = this // 将 Fn 挂载到 context 上
const res = context[randomKey](...arg) // 执行得到结果
return res // 返回结果
}
测试一下
Fn.myCall_2(obj, '80cm') // name: 1 age: 2 height: 80cm
obj // {name: 1, age: 2, Symbol(call): ƒ}
复制代码
如有不太理解的可以去复习一下Symbol
问题2:
Function.prototype.myCall_2 = function() {
let context = arguments[0] // 取上下文
let arg = Array.from(arguments).slice(1) // 取入参
const randomKey = Symbol('call') // --------问题1------->通过Symbol声明一个唯一的key值
context[randomKey] = this // 将 Fn 挂载到 context 上
const res = context[randomKey](...arg) // 执行得到结果
delete context[randomKey] // --------问题2------->执行完毕后从 context 上删除该键值对
return res // 返回结果
}
测试一下
Fn.myCall_2(obj, '80cm') // name: 1 age: 2 height: 80cm
obj // {name: 1, age: 2} 多余的键值对已经去除
复制代码
关于问题3先看看原生call
是如何处理的
非严格模式下
Fn.call(null, '80cm') // name: Leo age: 18 height: 80cm
Fn.call(undefined, '80cm') // name: Leo age: 18 height: 80cm
Fn.call(false, '80cm') // name: undefined age: undefined height: 80cm
Fn.call(true, '80cm') // name: undefined age: undefined height: 80cm
Fn.call('', '80cm') // name: undefined age: undefined height: 80cm
Fn.call(1, '80cm') // name: undefined age: undefined height: 80cm
Fn.call(function aa(){}, '80cm') // name: aa age: undefined height: 80cm
Fn.call(this, '80cm') // name: Leo age: 18 height: 80cm
Fn.call('80cm') // name: undefined age: undefined height: undefined
复制代码
关于问题3先看看myCall_2
是如何处理的
非严格模式下
Fn.myCall_2(null, '80cm') // Uncaught TypeError: Cannot set properties of null (setting 'Symbol(call)')
Fn.myCall_2(undefined, '80cm') // Cannot set properties of undefined (setting 'Symbol(call)')
Fn.myCall_2(false, '80cm') // Uncaught TypeError: context[randomKey] is not a function
Fn.myCall_2(true, '80cm') // Uncaught TypeError: context[randomKey] is not a function
Fn.myCall_2('', '80cm') // Uncaught TypeError: context[randomKey] is not a function
Fn.myCall_2(1, '80cm') // Uncaught TypeError: context[randomKey] is not a function
Fn.myCall_2(function aa(){}, '80cm') // name: aa age: undefined height: 80cm
Fn.myCall_2(this, '80cm') // name: Leo age: 18 height: 80cm
Fn.myCall_2('80cm') // Uncaught TypeError: context[randomKey] is not a function
复制代码
对比之下看到关于myCall_2
的一些问题
- 当
context
为null/undefined/this
的时候context
被指向全局作用域 - 当
context
为false/true/''/1/
的时候context
被貌似被指向一个未知对象(可以用包装对象处理) - 当
context
为function
时看现象相当于一个有name键值对的对象,但是我认为应该没有做什么处理直接把函数当作对象拿来用(这里有疑问的欢迎讨论) - 当
context
不传值的时候其表现与2相似(将’80cm’字符串当作this)
综合以上比对开始优化问题3
3. 第三版本call
问题3:
Function.prototype.myCall_3 = function() {
let context = arguments[0] // 取上下文
let arg = Array.from(arguments).slice(1) // 取入参
// 问题3
if(context === null || context === undefined) {
context = window // 改变指向全局
} else {
context = Object(context) // 变成一个包装对象处理
}
const randomKey = Symbol('call') // --------问题1------->通过Symbol声明一个唯一的key值
context[randomKey] = this // 将 Fn 挂载到 context 上
const res = context[randomKey](...arg) // 执行得到结果
delete context[randomKey] // --------问题2------->执行完毕后从 context 上删除该键值对
return res // 返回结果
}
测试一下
Fn.myCall_3(null, '80cm') // name: Leo age: 18 height: 80cm
Fn.myCall_3(undefined, '80cm') // name: Leo age: 18 height: 80cm
Fn.myCall_3(false, '80cm') // name: undefined age: undefined height: 80cm
Fn.myCall_3(true, '80cm') // name: undefined age: undefined height: 80cm
Fn.myCall_3('', '80cm') // name: undefined age: undefined height: 80cm
Fn.myCall_3(1, '80cm') // name: undefined age: undefined height: 80cm
Fn.myCall_3(function aa(){}, '80cm') // name: aa age: undefined height: 80cm
Fn.myCall_3(this, '80cm') // name: Leo age: 18 height: 80cm
Fn.myCall_3('80cm') // name: undefined age: undefined height: undefined
可以看到这里的 myCall_3 测试结果与原生 call 保持一直
复制代码
对于一些疑问
- 关于
call
函数的this校验问题:其实Function.prototype.myCall_3
这段代码就可以代为处理this
的校验问题,可以思考一下 - 严格模式问题:在严格模式下由于不再指向全局作用域即:
window
为undefined
所以this/null/undefined
均出现报错,其余表现一致
至此call
的模拟实现基本完成
apply
对于apply
而言其唯一区别在于入格式的区别,不再是传入数列参数
。而是一个数组
1. 第一版本apply
Function.prototype.myApply_1 = function() {
let context = arguments[0] // 取上下文
let arg = arguments[1] // 取入参
// 问题3
if(context === null || context === undefined) {
context = window // 改变指向全局
} else {
context = Object(context) // 变成一个包装对象处理
}
const randomKey = Symbol('apply') // --------问题1------->通过Symbol声明一个唯一的key值
context[randomKey] = this // 将 Fn 挂载到 context 上
const res = context[randomKey](...arg) // 执行得到结果
delete context[randomKey] // --------问题2------->执行完毕后从 context 上删除该键值对
return res // 返回结果
}
测试一下
Fn.myApply_1('', ['80cm']) // name: undefined age: undefined height: 80cm
Fn.myApply_1(obj, ['80cm']) // name: 1 age: 2 height: 80cm
Fn.myApply_1(null, ['80cm']) // name: Leo age: 18 height: 80cm
Fn.myApply_1(function aa(){}, ['80cm']) // name: aa age: undefined height: 80cm
Fn.myApply_1(false, ['80cm']) // name: undefined age: undefined height: 80cm
Fn.myApply_1(this, ['80cm']) // name: Leo age: 18 height: 80cm
复制代码
其实从第一版本中可以看出这里myApply_1
的输出结果已经和原生apply
高度一致了,这里只需要对数组进行校验即可
既然call
都实现了那么就用myCall_3
来协助实现apply
就好了
2. 第二版本apply
Function.prototype.myApply_2 = function() {
let context = arguments[0] // 取上下文
let arg = arguments[1] // 取入参
let type = Object.prototype.toString.myCall_3(arg) // 找出类型
if(type.slice(8, type.length - 1) !== 'Array') throw new TypeError('CreateListFromArrayLike called on non-object');
// 问题3
if(context === null || context === undefined) {
context = window // 改变指向全局
} else {
context = Object(context) // 变成一个包装对象处理
}
const randomKey = Symbol('apply') // --------问题1------->通过Symbol声明一个唯一的key值
context[randomKey] = this // 将 Fn 挂载到 context 上
const res = context[randomKey](...arg) // 执行得到结果
delete context[randomKey] // --------问题2------->执行完毕后从 context 上删除该键值对
return res // 返回结果
}
测试一下
Fn.myApply_2(obj, {}) // Uncaught TypeError: CreateListFromArrayLike called on non-object
Fn.myApply_2(obj, ['80cm']) // name: 1 age: 2 height: 80cm
复制代码
此外发现对于原生apply
类数组是可以进行正常使用的。接下来继续测试
- Fn.apply(obj, {}) // name: 1 age: 2 height: undefined
- Fn.apply(obj, null) // name: 1 age: 2 height: undefined
- Fn.apply(obj, function(){}) // name: 1 age: 2 height: undefined
- Fn.apply(obj, undefined) // name: 1 age: 2 height: undefined
- Fn.apply(obj) // name: 1 age: 2 height: undefined
- Fn.apply(obj, true) // Uncaught TypeError: CreateListFromArrayLike called on non-object
- Fn.apply(obj, ”) // Uncaught TypeError: CreateListFromArrayLike called on non-object
- Fn.apply(obj, 1) // Uncaught TypeError: CreateListFromArrayLike called on non-object
- Fn.apply(obj, {length:1,0:’80cm’}) // name: 1 age: 2 height: 80cm
按照第二版本myApply_2
的代码以上测试全部报错,综合以上比对开始着手优化
3. 第三版本apply
Function.prototype.myApply_3 = function() {
let context = arguments[0] // 取上下文
let arg = arguments[1] // 取入参
let typeAll = Object.prototype.toString.myCall_3(arg) // 找出类型
const type = typeAll.slice(8, typeAll.length - 1) // 字符串切割
if(type === 'Boolen' || type === 'String' || type === 'Number') {
// 根据测试结果以上三种类型值直接报错
throw new TypeError('CreateListFromArrayLike called on non-object');
} else if (type === 'Null' || type === 'Undefined' ||type === 'Function' || (type === 'Object' && Object.keys(arg).length === 0)) {
// {}/null/undefined/function
arg = []
} else if (type === 'Object' && Object.keys(arg).length !== 0) {
// 类数组
arg = Array.from(arg)
}
// 问题3
if(context === null || context === undefined) {
context = window // 改变指向全局
} else {
context = Object(context) // 变成一个包装对象处理
}
const randomKey = Symbol('apply') // --------问题1------->通过Symbol声明一个唯一的key值
context[randomKey] = this // 将 Fn 挂载到 context 上
const res = context[randomKey](...arg) // 执行得到结果
delete context[randomKey] // --------问题2------->执行完毕后从 context 上删除该键值对
return res // 返回结果
}
测试一下
Fn.myApply_3(obj, {}) // name: 1 age: 2 height: undefined
Fn.myApply_3(obj, null) // name: 1 age: 2 height: undefined
Fn.myApply_3(obj, function(){}) // name: 1 age: 2 height: undefined
Fn.myApply_3(obj, undefined) // name: 1 age: 2 height: undefined
Fn.myApply_3(obj) // name: 1 age: 2 height: undefined
Fn.myApply_3(obj, true) // Uncaught TypeError: CreateListFromArrayLike called on non-object
Fn.myApply_3(obj, '') // Uncaught TypeError: CreateListFromArrayLike called on non-object
Fn.myApply_3(obj, 1) // Uncaught TypeError: CreateListFromArrayLike called on non-object
Fn.myApply_3(obj, {length:1,0:'80cm'}) // name: 1 age: 2 height: 80cm
可以看到这里的 myApply_3 测试结果与原生 apply 保持一直
复制代码
至此apply
的模拟实现基本完成
最后
原创不易希望大家多多支持,欢迎拍砖!
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END