Skip to content

myCall

题目描述

实现一个 myCall 方法来模拟 Function.prototype.call 的功能:

  • 可以修改函数的 this 指向
  • 支持传递任意数量的参数
  • 需要遵循 ECMAScript 规范的非严格模式逻辑
  • thisArgnullundefined 时指向 window
  • 原始值会被包装成对象

本题考查 this 绑定机制函数调用原理ECMAScript 规范理解

核心知识点

1. Function.prototype.call 原理

  • 作用: 调用一个函数,同时指定函数运行时的 this
  • 语法: func.call(thisArg, arg1, arg2, ...)
  • 特点: 立即执行函数,不同于 bind 的延迟执行

2. this 绑定规则(非严格模式)

  • null/undefined: 绑定到全局对象 (window)
  • 原始值: 自动装箱为包装对象
  • 对象: 直接使用该对象作为 this
  • 函数: 作为对象使用

3. Symbol 的巧妙运用

  • 避免属性冲突: 使用 Symbol 作为临时属性名
  • 保证唯一性: 每次调用生成新的 Symbol
  • 安全清理: 调用完成后删除临时属性

代码实现

javascript
Function.prototype.mycall = function (thisArg, ...args) {
  const key = Symbol(1)
  if (thisArg === void 0 || thisArg === null)
    thisArg = window
  else if (typeof thisArg !== 'object')
    thisArg = new Object(thisArg)

  thisArg[key] = this
  const result = thisArg[key](...args)
  delete thisArg[key]

  return result
}

关键技术点

1. thisArg 处理的细节

javascript
// 严格区分各种情况
if (thisArg === void 0 || thisArg === null) {
  // undefined 和 null 的处理
  thisArg = window
}
else if (typeof thisArg !== 'object') {
  // 原始值的自动装箱
  thisArg = new Object(thisArg)
}

// 具体的装箱示例
new Object(123) // Number {123}
new Object('abc') // String {'abc'}
new Object(true) // Boolean {true}
new Object(Symbol('s')) // Symbol {Symbol(s)}

2. Symbol 防冲突机制

javascript
// 为什么使用 Symbol?
const key = Symbol() // 保证属性名唯一
thisArg[key] = this // 不会覆盖已有属性

// 对比字符串属性名的风险
thisArg.tempMethod = this // 可能覆盖已有属性

3. 参数传递技巧

javascript
// 使用展开运算符处理任意数量参数
function myCall(thisArg, ...args) {
  // args 是一个数组,包含所有剩余参数
  const result = thisArg[key](...args) // 展开传递
  return result
}

4. 常见陷阱和坑点

  • 忘记处理 null/undefined: 导致类型错误
  • 不使用 Symbol: 可能覆盖对象原有属性
  • 忘记清理临时属性: 造成内存泄漏和意外行为
  • 严格模式处理: 题目要求非严格模式的实现
  • 返回值遗漏: 必须返回函数执行结果

使用示例

javascript
// 基本用法
function greet(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`
}

const person = { name: 'Alice' }

// 原生 call
console.log(greet.call(person, 'Hello', '!')) // "Hello, Alice!"

// 自实现 myCall
console.log(greet.mycall(person, 'Hello', '!')) // "Hello, Alice!"

// 测试 thisArg 为 null
function showThis() {
  console.log(this === window) // 非严格模式下为 true
}
showThis.mycall(null)

// 测试原始值自动装箱
function checkType() {
  console.log(typeof this) // object
  console.log(this.valueOf()) // 123
}
checkType.mycall(123)

// 测试复杂场景
const calculator = {
  base: 10,
  add(a, b) {
    return this.base + a + b
  },
}

const newBase = { base: 20 }
console.log(calculator.add.mycall(newBase, 5, 3)) // 28

// 测试箭头函数(注意:箭头函数不能改变 this)
function arrowFunc() {
  console.log(this === window) // 始终为 true(在全局作用域)
}
arrowFunc.mycall(person) // this 不会改变

扩展思考

1. apply 方法实现

javascript
Function.prototype.myApply = function (thisArg, argsArray) {
  const key = Symbol()

  if (thisArg === void 0 || thisArg === null) {
    thisArg = window
  }
  else if (typeof thisArg !== 'object') {
    thisArg = new Object(thisArg)
  }

  thisArg[key] = this

  // 处理参数数组
  let result
  if (argsArray === void 0 || argsArray === null) {
    result = thisArg[key]()
  }
  else {
    // 确保 argsArray 是类数组对象
    result = thisArg[key](...Array.from(argsArray))
  }

  delete thisArg[key]
  return result
}

2. bind 方法实现

javascript
Function.prototype.myBind = function (thisArg, ...bindArgs) {
  const fn = this

  return function boundFunction(...callArgs) {
    // 判断是否是 new 调用
    if (this instanceof boundFunction) {
      // new 调用时,this 指向新创建的对象
      return new fn(...bindArgs, ...callArgs)
    }
    else {
      // 普通调用,使用 myCall
      return fn.mycall(thisArg, ...bindArgs, ...callArgs)
    }
  }
}

3. 严格模式版本

javascript
Function.prototype.myCallStrict = function(thisArg, ...args) {
  'use strict'
  const key = Symbol()

  // 严格模式下不进行 thisArg 转换
  if (thisArg === void 0 || thisArg === null) {
    // 严格模式下保持 null/undefined
    thisArg = thisArg
  }
  // 不进行原始值装箱

  thisArg[key] = this
  const result = thisArg[key](...args)
  delete thisArg[key]
  return result
}

4. 性能优化版本

javascript
// 避免重复创建 Symbol
const tempSymbol = Symbol('myCall')

Function.prototype.myCallOptimized = function (thisArg, ...args) {
  if (thisArg === void 0 || thisArg === null) {
    thisArg = window
  }
  else if (typeof thisArg !== 'object') {
    thisArg = new Object(thisArg)
  }

  // 检查是否已有该属性
  const hasOwnProperty = thisArg.hasOwnProperty(tempSymbol)
  const oldValue = thisArg[tempSymbol]

  thisArg[tempSymbol] = this
  const result = thisArg[tempSymbol](...args)

  // 恢复原状态
  if (hasOwnProperty) {
    thisArg[tempSymbol] = oldValue
  }
  else {
    delete thisArg[tempSymbol]
  }

  return result
}

内容基于 MIT 许可 | 保持节奏 · 持续积累