各位靓仔靓女,晚上好!我是今晚的主讲人,咱们今天来聊聊 JavaScript 函数的 [[Call]]
和 [[Construct]]
内部方法,以及 new
操作符这个磨人的小妖精背后的秘密。
什么是内部方法?别慌,不是武功秘籍!
首先,我们需要搞清楚“内部方法”是个什么玩意儿。在 ECMAScript 规范里,内部方法是用双中括号括起来的,比如 [[Call]]
、[[Construct]]
、[[Get]]
等等。这些东西你没办法直接在 JavaScript 代码里调用,它们是引擎内部运作的机制,相当于汽车的发动机,你看不到,但它吭哧吭哧地工作,驱动汽车前进。
函数:能屈能伸的变形金刚
在 JavaScript 里,函数是个非常灵活的角色。它既可以像普通函数一样被调用,也可以作为构造函数,配合 new
操作符来创建对象。这种双重身份就得益于 [[Call]]
和 [[Construct]]
这两个内部方法。
-
[[Call]]
:我是普通函数,请直接调用我!当你像这样调用一个函数:
myFunction()
,引擎就会调用该函数的[[Call]]
内部方法。[[Call]]
的作用就是执行函数体里的代码,然后返回结果。简单来说,[[Call]]
就是让函数像普通函数一样运行的机制。 -
[[Construct]]
:我是构造函数,请用我来创建对象!当你使用
new
操作符来调用一个函数,比如new MyFunction()
,引擎就会调用该函数的[[Construct]]
内部方法。[[Construct]]
的作用是创建一个新的对象,并将该对象作为this
绑定到函数体内部,然后执行函数体里的代码。如果函数没有显式地返回一个对象,[[Construct]]
会默认返回这个新创建的对象。简单来说,[[Construct]]
就是让函数摇身一变,成为构造函数,负责创建对象的机制。
[[Call]]
的详细分解
当一个函数通过 [[Call]]
被调用时,引擎会执行以下步骤(简化版,更完整的描述请参考 ECMAScript 规范):
- 准备环境: 创建一个新的执行上下文 (Execution Context),并将函数的作用域链 (Scope Chain) 设置为该函数创建时的作用域链。
- 确定
this
的值:this
的值取决于调用方式。- 如果是在全局作用域中直接调用,
this
通常指向全局对象(浏览器环境是window
,Node.js 环境是global
)。 - 如果是作为对象的方法调用,
this
指向该对象。 - 可以通过
call
、apply
、bind
等方法来显式指定this
的值。
- 如果是在全局作用域中直接调用,
- 传递参数: 将调用时传入的参数传递给函数。
- 执行函数体: 按照函数体内的代码顺序执行。
- 返回结果: 函数返回的值就是
[[Call]]
的返回值。如果函数没有显式地返回值,则返回undefined
。
代码示例:[[Call]]
的应用
function greet(name) {
console.log(`Hello, ${name}! My this is: ${this}`);
return `Greeting: Hello, ${name}!`;
}
// 直接调用,this 指向全局对象 (window in browser)
greet("Alice"); // 输出: Hello, Alice! My this is: [object Window]
// 返回: "Greeting: Hello, Alice!"
// 使用 call 改变 this 指向
const myObject = { message: "Custom Message" };
const greeting = greet.call(myObject, "Bob"); // 输出: Hello, Bob! My this is: [object Object]
console.log(greeting); // 输出: Greeting: Hello, Bob!
[[Construct]]
的详细分解
当一个函数通过 [[Construct]]
被调用时(也就是使用了 new
操作符),引擎会执行以下步骤(同样是简化版):
- 创建新对象: 创建一个全新的空对象。
- 设置原型链: 将新对象的
[[Prototype]]
内部属性(也就是__proto__
属性,但不能直接访问,需要通过Object.getPrototypeOf()
来访问)指向构造函数的prototype
属性。这意味着新对象可以继承构造函数prototype
上的属性和方法。 - 绑定
this
: 将新创建的对象作为this
绑定到构造函数内部。 - 执行构造函数体: 按照构造函数体内的代码顺序执行。
- 返回结果:
- 如果构造函数显式地返回一个对象,则返回该对象。
- 如果构造函数没有显式地返回任何值(或者返回的是
null
、undefined
、原始类型的值),则返回第 1 步创建的新对象。
代码示例:[[Construct]]
的应用
function Person(name, age) {
this.name = name;
this.age = age;
this.greet = function() {
console.log(`Hi, I'm ${this.name}, and I'm ${this.age} years old.`);
};
}
// 给 Person 的 prototype 添加方法
Person.prototype.introduce = function() {
console.log(`Let me introduce myself. I am ${this.name}.`);
};
// 使用 new 操作符创建对象
const john = new Person("John", 30);
john.greet(); // 输出: Hi, I'm John, and I'm 30 years old.
john.introduce(); // 输出: Let me introduce myself. I am John.
console.log(john instanceof Person); // 输出: true
// 尝试返回一个对象
function SpecialPerson(name, age) {
this.name = name;
this.age = age;
return {special: true}; // 显式返回一个对象
}
const jane = new SpecialPerson("Jane", 25);
console.log(jane); // 输出: { special: true } 注意:返回的是显式返回的对象
console.log(jane.name); // 输出: undefined 因为返回的对象没有name属性
new
操作符的精确执行过程:庖丁解牛
现在,让我们把 new
操作符的执行过程分解得更细致一些,就像庖丁解牛一样,让你彻底明白它背后的原理。
假设我们有以下代码:
function Animal(name) {
this.name = name;
this.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
}
Animal.prototype.species = "Unknown";
const dog = new Animal("Dog");
dog.sayHello(); // 输出: Hello, I'm Dog
console.log(dog.species); // 输出: Unknown
new Animal("Dog")
的执行过程如下:
-
创建一个空对象: 引擎创建一个新的空对象,我们暂且称之为
obj
。obj = {}; // 相当于 let obj = Object.create(null);
-
设置原型链: 将
obj
的[[Prototype]]
内部属性指向Animal.prototype
。obj.__proto__ = Animal.prototype; // (Simplified, directly setting __proto__ for illustration) // 或者用更规范的方式: Object.setPrototypeOf(obj, Animal.prototype);
这意味着
obj
可以访问Animal.prototype
上的属性和方法,比如species
属性。 -
绑定
this
并执行构造函数: 将obj
作为this
绑定到Animal
函数内部,然后执行Animal
函数体。// 相当于:Animal.call(obj, "Dog"); // 在 Animal 函数内部: // this.name = "Dog"; // obj.name = "Dog"; // this.sayHello = function() { ... }; // obj.sayHello = function() { ... };
此时,
obj
拥有了name
和sayHello
属性,以及从Animal.prototype
继承来的species
属性。 -
返回对象: 由于
Animal
函数没有显式地返回一个对象,所以引擎返回obj
。// 如果 Animal 函数里有 return { something: "else" }; 那么返回的就是这个对象,而不是 obj 了。 return obj;
-
赋值: 将返回的
obj
赋值给dog
变量。const dog = obj;
模拟 new
操作符:自己动手,丰衣足食
为了更好地理解 new
操作符的原理,我们可以自己动手实现一个 myNew
函数,来模拟 new
操作符的行为。
function myNew(constructor, ...args) {
// 1. 创建一个空对象
const obj = Object.create(constructor.prototype); // 使用 Object.create() 创建对象,并指定原型
// 2. 绑定 this 并执行构造函数
const result = constructor.apply(obj, args); // 使用 apply 绑定 this 并传递参数
// 3. 返回对象
return (typeof result === 'object' && result !== null) || typeof result === 'function' ? result : obj; // 如果构造函数返回对象,则返回该对象,否则返回新创建的对象
}
// 使用我们自己实现的 myNew 函数
const cat = myNew(Animal, "Cat");
cat.sayHello(); // 输出: Hello, I'm Cat
console.log(cat.species); // 输出: Unknown
function Test(a){
this.a = a;
return 1
}
const t = myNew(Test,1)
console.log(t.a) //1
总结:[[Call]]
vs. [[Construct]]
和 new
操作符
为了更清晰地理解这些概念,我们用一个表格来总结一下:
特性 | [[Call]] |
[[Construct]] |
new 操作符 |
---|---|---|---|
触发方式 | 直接调用函数 (myFunction() ) |
使用 new 操作符调用函数 (new MyFunction() ) |
显式地使用 new 操作符。 |
作用 | 执行函数体,作为普通函数调用。 | 创建对象,并将函数作为构造函数使用。 | 调用函数的 [[Construct]] 内部方法,创建对象,设置原型链,绑定 this ,执行构造函数体,并返回对象(如果构造函数没有显式返回对象)。 |
this 指向 |
取决于调用方式(全局对象、调用者、call /apply /bind ) |
新创建的对象。 | 内部过程保证this 指向新建的对象,并在构造函数执行期间允许对该对象进行属性设置。 |
返回值 | 函数的返回值。 | 如果构造函数显式返回对象,则返回该对象;否则返回新创建的对象。 | 返回新创建的对象(除非构造函数显式返回一个对象)。 |
使用场景 | 调用普通函数。 | 创建对象。 | 实例化对象。 |
注意事项:
-
并非所有函数都可以作为构造函数使用。箭头函数就没有
[[Construct]]
内部方法,所以不能使用new
操作符来调用。const arrowFunction = () => {}; // new arrowFunction(); // TypeError: arrowFunction is not a constructor
-
如果一个函数被设计成只能通过
new
操作符来调用,可以在函数内部判断this
是否为该函数的实例。如果不是,则抛出一个错误。function SafeConstructor(value) { if (!(this instanceof SafeConstructor)) { throw new TypeError("SafeConstructor must be called with new"); } this.value = value; } // const safe = SafeConstructor(1); // TypeError: SafeConstructor must be called with new const safe = new SafeConstructor(1); // 正常
总结的总结:
理解 [[Call]]
和 [[Construct]]
内部方法以及 new
操作符的执行过程,是深入理解 JavaScript 对象创建机制的关键。掌握这些知识,可以帮助你更好地理解 JavaScript 的底层原理,编写更健壮、更可维护的代码。以后面试再问你这些问题,你就把这篇文章甩给他,然后潇洒地走开!
希望今天的分享对大家有所帮助!下次有机会再和大家聊聊 JavaScript 的其他有趣话题。 拜拜!