手写模拟实现:call/apply

前言:以下记录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的模拟
关于第一版存在如下一些问题

  1. myCall_1randomKey是否可以让其做到真正唯一?
  2. 每一次执行myCall_1都会在context上挂在一个额外属性
  3. 关于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的一些问题

  1. contextnull/undefined/this的时候context被指向全局作用域
  2. contextfalse/true/''/1/的时候context被貌似被指向一个未知对象(可以用包装对象处理)
  3. contextfunction时看现象相当于一个有name键值对的对象,但是我认为应该没有做什么处理直接把函数当作对象拿来用(这里有疑问的欢迎讨论
  4. 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 保持一直
复制代码

对于一些疑问

  1. 关于call函数的this校验问题:其实Function.prototype.myCall_3这段代码就可以代为处理this的校验问题,可以思考一下
  2. 严格模式问题:在严格模式下由于不再指向全局作用域即:windowundefined所以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
喜欢就支持一下吧
点赞0 分享