JavaScript Study Notes: Implementing and Refining call/apply
call and apply are classic JavaScript methods and excellent starting points for understanding implicit this binding, object-property operations, and global scope.
I'll begin with a minimal call, gradually move closer to native behavior, and finally derive an apply implementation, explaining what's incomplete at each step.
1. What Are call and apply?
Their core purpose is to execute a function immediately, temporarily changing its this to a specified object. Their only difference is how arguments are supplied: call accepts separate arguments, while apply accepts an array.
For example:
function greet(greeting, punctuation) {
console.log(greeting + ", " + this.name + punctuation);
}
var obj = { name: "Muyu" };
greet.call(obj, "Hello", "!"); // Hello, Muyu!This does two things:
Temporarily point greet's this to obj.
Immediately execute greet with "Hello", "!" as arguments.
To implement call / apply, we can use JavaScript's implicit binding rule: a method's this points to the object that calls it. In obj.fn(), this inside the method is obj.
A complete call therefore needs at least:
Temporarily attach the function as a property of the target object.
Execute it immediately with the remaining arguments.
Remove the temporary property after obtaining the result.
Handle functions with return values.
Support null, undefined, and primitive values for context.
Ensure the temporary property doesn't overwrite an existing property.
2. Implementation
Basic this Binding
Function.prototype.myCall = function (context) {
// 把调用 myCall 的原函数(this)绑定到 context 的一个新属性上
context.fn = this;
// 通过隐式绑定执行函数
context.fn();
// 用完即焚,保持目标对象整洁
delete context.fn;
};
// ================= 测试用例 =================
function printName() {
console.log(this.name);
}
var obj = { name: "Muyu" };
// 正确用例:成功改变了 this 指向
printName.myCall(obj); // Muyu
// 错误用例:无法传递参数,也没有返回值
function add(a, b) {
return this.x + a + b;
}
var obj2 = { x: 10 };
var res = add.myCall(obj2, 1, 2);
console.log(res); // undefined (而且内部计算也是错的)What this version does:
this is the original function receiving the myCall call, such as printName.
Attach it to context, or obj, as a temporary method named fn.
Call context.fn() to use implicit binding and point the original function's this to context.
After execution, delete the temporary property to leave no trace.
Remaining problems: it cannot accept arguments and doesn't return the original function's result. If context already has a property called fn, we mercilessly overwrite and delete it.
Supporting Arguments and Return Values
For clarity, we'll use ES6 rest parameters and spread syntax. In ES3/ES5, this would commonly have been implemented with eval('context.fn(' + args + ')').
Function.prototype.myCall = function (context, ...args) {
context.fn = this;
// 传入参数,并保存原函数的执行结果
var result = context.fn(...args);
delete context.fn;
// 返回原函数的执行结果
return result;
};
// ================= 测试用例 =================
function add(a, b) {
return this.x + a + b;
}
var obj = { x: 10, fn: "我是原有的属性" };
// 正确用例:参数传递成功,且成功拿到了返回值
var res = add.myCall(obj, 1, 2);
console.log(res); // 13
// 错误用例 1:原对象的 fn 属性被覆盖并删除了
console.log(obj.fn); // undefined
// 错误用例 2:当传入的 context 为 null 时,会抛出异常
try {
add.myCall(null, 1, 2);
} catch (error) {
console.log(error.message); // Cannot set properties of null (setting 'fn')
}What this version does:
Use ...args to collect additional arguments.
Pass them through context.fn(...args) and capture the return value in result.
Return result after deleting the temporary property.
Remaining problems: edge cases aren't handled. Native call defaults this to the global object—window in browsers, global in Node—when the first argument is null or undefined. An existing fn property also causes a serious collision and data loss. Finally, attaching a property directly fails when context is a primitive such as a number or string.
Fixing Edge Cases and Property Collisions: A More Complete call and apply
// 手撕 call
Function.prototype.myCall = function (context, ...args) {
// 1. 处理 null/undefined,默认指向全局对象
// 2. 使用 Object() 包装基础类型(如数字、字符串),使其能挂载属性
var ctx = context === null || context === undefined
? globalThis
: Object(context);
// 使用 Symbol 生成唯一键,绝对不会覆盖对象原有的属性
var fnSymbol = Symbol("fn");
ctx[fnSymbol] = this;
var result = ctx[fnSymbol](...args);
delete ctx[fnSymbol];
return result;
};
// 手撕 apply(逻辑与 call 完全一致,仅接收参数的方式改为数组)
Function.prototype.myApply = function (context, argsArray) {
var ctx = context === null || context === undefined
? globalThis
: Object(context);
var fnSymbol = Symbol("fn");
ctx[fnSymbol] = this;
// 容错处理:如果不传 argsArray,默认传入空数组展开
var result = ctx[fnSymbol](...(argsArray || []));
delete ctx[fnSymbol];
return result;
};
// ================= 测试用例 =================
function testContext(a, b) {
return [this.value, a, b];
}
var obj = { value: "myObject", fn: "不要覆盖我" };
var value = "Global Value"; // 在浏览器全局下模拟
// 正确用例 1:修复了属性冲突,原有的 fn 属性完好无损
testContext.myCall(obj, 1, 2);
console.log(obj.fn); // 不要覆盖我
// 正确用例 2:兼容 null 和 undefined 指向全局对象
// 注:在 Node 环境下 globalThis 没有 value 属性,结果为 undefined;浏览器环境下相当于 window.value
console.log(testContext.myCall(null, "a", "b")); // ['Global Value', 'a', 'b'] (浏览器环境下)
// 正确用例 3:基础类型包装 (this 会变成一个 Number 对象)
function checkType() { return typeof this; }
console.log(checkType.myCall(123)); // "object"
// 正确用例 4:myApply 测试
console.log(testContext.myApply(obj, [1, 2])); // ['myObject', 1, 2]
console.log(testContext.myApply(obj)); // ['myObject', undefined, undefined]What this version does:
Handle null and undefined with globalThis, the modern standard way to access the global object in both browsers and Node.
Since directly doing 1.fn = this isn't valid, box primitives with Object(context).
Use ES6 Symbol to create a unique property key, eliminating collisions with existing properties.
Implement apply too, by handling arguments supplied as an array.
Remaining problems: none. This is a very robust polyfill that perfectly simulates native behavior.
3. Summary
Refining call and apply makes extensive use of these core JavaScript mechanisms:
Implicit binding: the obj.method() call form makes a standalone function execute as a method of the target object, changing its this.
Unique identifiers (Symbol): a practical example of Symbol's classic use as a unique property key that avoids name collisions.
Global-scope fallback: understanding how JavaScript in non-strict mode falls back to the global object (window / globalThis) when context is null or undefined.
Wrapper objects: understanding the implicit Object() boxing the engine performs when calling object methods on primitives such as String, Number, and Boolean.
@startuml
skinparam ArrowColor #666666
skinparam DefaultTextAlignment center
skinparam NoteBackgroundColor #FEFEFE
skinparam NoteBorderColor #CCCCCC
title Handwritten myCall vs. Native call
start
:fn.call(obj) / fn.myCall(obj);
fork
partition "Application-level Simulation (myCall)\nSide effects and object mutation" #FFF0F5 {
:Handle edge cases (null/undefined and primitive boxing);
:Create a unique identifier (Symbol);
:**Mutate the target object** ""obj[Symbol] = fn; ""(Attach the function to the object);
note left
**Key Limitation**
The object is modified at this point.
A new property is inserted,
changing its memory structure.
end note
:**Trigger implicit binding** ""obj[Symbol](...args);"" (Use method invocation to change this);
:**Clean up** ""delete obj[Symbol];"" (Remove the property after use);
}
fork again
partition "JS Engine (Native call)\nNo object mutation, internal binding changes" #E0FFFF {
:The engine creates a **new execution context** for fn;
:Initialize the lexical and variable environments;
:**Set the internal binding** so the context's **ThisBinding** points to obj;
note right
**Key Advantage**
The target object obj **remains unchanged**!
No new property and no delete operation.
Safe, efficient, and complete.
end note
:Execute the function's machine code/bytecode with the updated ThisBinding;
:Pop the completed call from the stack and discard its execution context;
}
end fork
:Return the function result;
stop
@endumlDiagram unavailable. Use Show code to inspect the source.