为什么class在前端开发中不常用?
- js是一种基于原型的语言。
- 它的对象继承是通过 原型链(prototype chain)实现的,每个对象都有一个 proto 属性指向它的原型。(大多数传统面向对象语言(如 Java、C++、Python、Ruby)是基于类的。在这些语言中,对象是通过类(class)定义的模板实例化出来的,继承是通过类之间的关系实现的。)
- ES6 的 class 只是语法糖,底层还是基于原型。
class Student extends Person {constructor(name, age, grade) {super(name, age);this.grade = grade;}study() {return `${this.name} is studying`;}
}//实际上等价于:function Person(name, age) {this.name = name;this.age = age;
}
Person.prototype.sayHello = function() {return `Hello, my name is ${this.name}`;
};
为什么不像其他语言基于类?
这是一个非常有趣的问题!JavaScript 为什么选择基于原型而不是基于类,背后有历史、技术和设计哲学的原因。让我们一步步拆解。
1. JavaScript 的诞生背景
JavaScript 是由 Brendan Eich 在 1995 年为网景公司(Netscape)开发的,最初的目标是为网页添加简单的动态功能。当时的时间非常紧迫(据说只有 10 天设计和实现),因此语言设计上需要快速成型,同时保持简单和灵活。
在那个年代,面向对象编程(OOP)已经很流行,尤其是基于类的语言(如 C++ 和 Java)。网景公司的高层希望 JavaScript 能吸引 Java 开发者,所以表面上要求它“看起来像 Java”。但 Brendan Eich 本人更喜欢一种更动态、简洁的语言——他受到 Self 语言(一种基于原型的语言)的启发,最终选择了原型机制。
2. 为什么选择原型而不是类?
(1) 简单性和灵活性
-
原型更简单
基于类的语言需要定义类的蓝图(模板),然后通过实例化生成对象。这种机制在复杂系统中很强大,但在简单的脚本语言中显得繁琐。原型机制直接让对象可以“复制”或“继承”另一个对象的行为,不需要额外的类定义。例如:javascript">const person = {sayHello() {console.log("Hello");} }; const alice = Object.create(person); alice.sayHello(); // "Hello"
这比定义一个类、再实例化要直截了当。
-
动态性
原型允许运行时修改对象的结构。你可以随时给原型添加方法,所有基于这个原型的对象都会自动获得新行为:javascript">person.greet = function() {console.log("Hi there"); }; alice.greet(); // "Hi there"
而基于类的语言通常是静态的,类定义一旦确定就很难改动。
(2) 适合脚本语言的轻量需求
JavaScript 最初是为网页设计的,主要用来操作 DOM、处理事件等简单任务。基于类的系统更适合大型、结构化的软件(如操作系统或企业级应用),但对于一个嵌入浏览器的小型脚本语言来说,原型机制更轻量、更灵活,不需要复杂的编译步骤。
(3) Self 语言的影响
Brendan Eich 深受 Self 语言启发。Self 是一种基于原型的语言,核心理念是“一切皆对象”,没有类的概念。对象直接通过克隆和修改原型来创建新对象。这种设计避免了类的抽象层,让语言更简洁。JavaScript 借鉴了这种思想,虽然加上了构造函数(function
)来模拟类的实例化,但底层依然是原型。
3. 为什么不像其他语言基于类?
(1) 历史包袱和竞争压力
- 在 1995 年,网景和微软(IE 的 JScript)在浏览器大战中竞争激烈。JavaScript 需要快速推出并赢得开发者支持。如果完全照搬 Java 的基于类设计,会增加语言的复杂度和实现成本,可能拖慢开发进度。
- 同时,JavaScript 要吸引的不只是 Java 程序员,还有写脚本的非专业开发者(比如网页设计师)。原型机制加上动态特性,让它更容易上手。
(2) 与其他语言的目标不同
- Java、C++ 等语言是为构建大型、静态类型系统设计的,基于类的机制适合严格的类型检查和复杂的继承体系。
- JavaScript 的目标是动态性和交互性,运行在浏览器中,面对的是不可预测的网页环境。原型机制让它能快速适应变化,比如动态添加方法或属性。
(3) 哲学差异
基于类的语言(尤其是强类型语言)强调“提前规划”和“结构化”,而 JavaScript 的原型机制更倾向于“边做边改”和“自由度”。这反映了 Brendan Eich 想创造一种更自由、实验性语言的初衷。
4. 原型带来的独特优势
-
灵活继承
你可以直接操作原型链,实现多样的继承方式,而不像基于类的语言受限于单一继承(Single Inheritance)或复杂的多重继承。javascript">const obj1 = { a: 1 }; const obj2 = Object.create(obj1); obj2.b = 2; console.log(obj2.a); // 1 (从原型继承)
-
运行时扩展
原型允许你在运行时动态修改对象的行为,这在浏览器环境中非常有用,比如 polyfill(填补浏览器功能的缺失)。 -
简洁的实现
原型的实现比类更轻量,占用内存更少,适合早期的资源有限的浏览器环境。
5. 为什么后来加了 class
?
尽管原型很强大,但开发者(尤其是来自 Java、C++ 背景的人)觉得它难以理解,而且代码组织不够直观。到了 ES6(2015 年),JavaScript 引入了 class
语法,主要是为了:
- 迎合主流开发者的习惯。
- 提供更清晰的面向对象编程方式。
但要注意,class
只是语法糖,底层仍然是原型机制。例如:
javascript">class Person {constructor(name) {this.name = name;}
}
实际上等价于:
javascript">function Person(name) {this.name = name;
}
总结:为什么 JS 基于原型?
- 历史原因:快速开发、受到 Self 语言启发、迎合 Java 外观但保持简单。
- 技术原因:原型更轻量、灵活,适合动态脚本语言的需求。
- 哲学原因:强调自由度和运行时可变性,而不是静态结构。
如果 JavaScript 当初选择基于类,可能更像 Java,失去今天的灵活性和独特性,但也可能让初学者更难接受它的动态特性。原型机制是 JavaScript 的“灵魂”,也是它能适应从简单脚本到现代复杂应用的关键。
原型机制的弱点(如灵活性带来的不可预测性)被怎么解决了?
- 渲染引擎的优化
- TS
- ES6 提供了class
在 TypeScript 中,直接修改原型(比如 xxx.prototype.somefunc = newFunc
)并没有被完全禁止,但它的行为和可行性受到了一些限制和约束。这主要取决于上下文、类型检查的严格程度以及代码的组织方式。让我详细解释一下。
TypeScript已经不允许直接这样修改原型了吗?
TypeScript 对原型修改的态度
TypeScript 的核心目标是增强 JavaScript 的类型安全性和可维护性。它通过静态类型检查来减少运行时错误,但它仍然是 JavaScript 的超集,底层运行时行为没有改变。因此,TypeScript 允许你修改原型,但会通过类型系统和配置项对你施加一些限制。
1. 默认情况下是允许的,但类型不安全
你仍然可以在 TypeScript 中直接修改原型,比如:
function Person(name: string) {this.name = name;
}Person.prototype.sayHello = function() {console.log(`Hello, ${this.name}`);
};const p = new Person("Alice");
p.sayHello(); // 输出 "Hello, Alice"
这段代码在 TypeScript 中是合法的,也能正常运行。因为 TypeScript 不会阻止你操作 prototype
,它只是试图为这种操作提供类型支持。
但是,TypeScript 的类型系统可能无法自动推断出你添加的 sayHello
方法,除非你显式声明它的类型。这会导致类型检查时的警告或错误。
2. 类型声明缺失的问题
如果你不告诉 TypeScript 你修改了原型,它会认为 sayHello
不存在:
const p = new Person("Alice");
p.sayHello(); // TS 错误: Property 'sayHello' does not exist on type 'Person'
要解决这个问题,你需要在 Person
的类型定义中显式声明这个方法,比如通过接口或类型扩展:
interface Person {name: string;sayHello(): void;
}function Person(this: Person, name: string) {this.name = name;
}Person.prototype.sayHello = function() {console.log(`Hello, ${this.name}`);
};const p = new Person("Alice");
p.sayHello(); // 正常运行,且类型安全
3. 使用 class
时更严格
如果你使用 TypeScript 的 class
语法(推荐的方式),直接修改原型会受到更多限制。因为 class
的方法和属性是静态定义的,TypeScript 会假设类的结构是固定的:
class Person {constructor(public name: string) {}
}Person.prototype.sayHello = function() {console.log(`Hello, ${this.name}`);
}; // 可以运行,但不推荐const p = new Person("Alice");
p.sayHello(); // TS 错误: Property 'sayHello' does not exist on type 'Person'
在这种情况下,TypeScript 不会自动识别 sayHello
,因为它不在类的定义中。你需要通过接口扩展来声明:
interface Person {sayHello(): void;
}class Person {constructor(public name: string) {}
}Person.prototype.sayHello = function() {console.log(`Hello, ${this.name}`);
};const p = new Person("Alice");
p.sayHello(); // 现在类型安全
4. 严格模式下的限制
如果你在 tsconfig.json
中启用了严格模式(strict: true
),尤其是 noImplicitAny
和 strictPropertyInitialization
,TypeScript 会更严格地检查类型。直接修改原型而不声明类型会更容易触发错误,迫使你显式定义接口或类型。
5. 对内置对象原型的修改
如果你尝试修改内置对象的原型(如 Object.prototype
或 Array.prototype
),TypeScript 不会阻止运行时行为,但会要求你通过声明合并(Declaration Merging)来扩展类型:
interface Array<T> {myCustomMethod(): void;
}Array.prototype.myCustomMethod = function() {console.log("Custom method on array");
};const arr = [1, 2, 3];
arr.myCustomMethod(); // 正常运行,且类型安全
不这样做的话,TypeScript 会报错,因为它不知道 myCustomMethod
。
TypeScript 为什么不完全禁止?
-
兼容 JavaScript
TypeScript 是 JavaScript 的超集,必须兼容 JS 的所有特性,包括原型机制。如果直接禁止修改原型,会破坏与现有 JS 代码的兼容性。 -
动态性保留
JavaScript 的动态特性(如运行时修改原型)是其核心优势之一,TypeScript 不想完全剥夺这种灵活性,而是通过类型系统引导开发者更安全地使用。 -
实际用例
在一些场景中(比如 polyfill 或库开发),修改原型仍然是合法且必要的。TypeScript 允许这种操作,但要求你显式声明类型。
现代 TypeScript 的推荐实践
尽管你可以修改原型,TypeScript 更鼓励使用 class
或模块化的方式来组织代码,而不是直接操作 prototype
:
- 用
class
替代:class Person {constructor(public name: string) {}sayHello() {console.log(`Hello, ${this.name}`);} }
- 用模块和函数替代:
const createPerson = (name: string) => ({name,sayHello: () => console.log(`Hello, ${name}`) });
直接修改原型被视为“老派”或“不安全”的做法,尤其在大型项目中,可能导致:
- 类型不一致。
- 维护困难(团队成员难以追踪原型修改)。
- 与静态类型系统的理念冲突。
结论
- TypeScript 没有禁止修改原型,但通过类型系统和严格检查,间接限制了这种行为。如果你想这样做,需要显式声明类型,否则会报错。
- 在现代开发中不推荐,因为
class
和函数式编程提供了更清晰、更安全的替代方案。 - 如果你坚持用
xxx.prototype.somefunc = newFunc
,是可以的,但要配合接口或类型声明,确保类型安全。
js中“复制”或“继承”的方法有哪些?
在 JavaScript 中,基于原型的“复制”或“继承”机制是其核心特性之一。由于 JavaScript 是基于原型的语言,它没有传统基于类语言中的“类继承”,而是通过原型链实现类似继承的功能。以下是 JavaScript 中实现“复制”或“继承”的常用方法,涵盖了从低级操作到高级语法的各种方式:
1. 通过 Object.create()
- 作用:创建一个新对象,并将其原型设置为指定对象,实现继承。
- 特点:直接操作原型链,简单且灵活。
- 示例:
javascript">const parent = {sayHello() {console.log("Hello from parent");} };const child = Object.create(parent); child.sayHello(); // "Hello from parent"
- 注意:
child
继承了parent
的属性和方法,但自身是空的,可以添加新属性。
2. 通过构造函数和 prototype
- 作用:利用构造函数和原型链实现继承,模拟类的行为。
- 特点:传统方式,广泛用于 ES5 及之前。
- 示例:
javascript">function Parent(name) {this.name = name; } Parent.prototype.sayHello = function() {console.log(`Hello, ${this.name}`); };function Child(name) {Parent.call(this, name); // 复制 Parent 的实例属性 } Child.prototype = Object.create(Parent.prototype); // 继承原型方法 Child.prototype.constructor = Child; // 修正 constructorconst child = new Child("Alice"); child.sayHello(); // "Hello, Alice"
- 步骤:
- 用
call
或apply
复制父构造函数的属性。 - 用
Object.create
设置原型链。 - 修正
constructor
属性(可选)。
- 用
3. 通过 ES6 的 class
和 extends
- 作用:使用 ES6 的类语法实现继承(底层仍是原型)。
- 特点:语法糖,更直观,适合现代开发。
- 示例:
javascript">class Parent {constructor(name) {this.name = name;}sayHello() {console.log(`Hello, ${this.name}`);} }class Child extends Parent {constructor(name) {super(name); // 调用父类的构造函数} }const child = new Child("Alice"); child.sayHello(); // "Hello, Alice"
- 注意:
extends
底层是通过原型链实现的,等价于构造函数方式。
4. 通过对象字面量和扩展运算符(浅复制)
- 作用:复制对象的属性(不涉及原型链),实现简单的“复制”。
- 特点:不完全是继承,更像是属性拷贝,适用于简单场景。
- 示例:
javascript">const parent = {name: "Alice",sayHello() {console.log("Hello");} };const child = { ...parent }; child.name = "Bob"; child.sayHello(); // "Hello" console.log(parent.name); // "Alice"(互不影响)
- 限制:只复制自身属性,不复制原型上的方法,且是浅复制。
5. 通过 Object.assign()
- 作用:将一个或多个源对象的可枚举属性复制到目标对象。
- 特点:浅复制,常用于合并对象。
- 示例:
javascript">const parent = {name: "Alice",sayHello() {console.log("Hello");} };const child = Object.assign({}, parent); child.name = "Bob"; child.sayHello(); // "Hello"
- 限制:和扩展运算符类似,不复制原型链。
6. 通过 __proto__
(不推荐)
- 作用:直接设置对象的
__proto__
属性,指定原型。 - 特点:低级操作,灵活但不推荐(性能差且已被废弃)。
- 示例:
javascript">const parent = {sayHello() {console.log("Hello");} };const child = { __proto__: parent }; child.sayHello(); // "Hello"
- 警告:
__proto__
是非标准特性,可能不支持,且不建议在生产代码中使用。
7. 通过混入(Mixin)
- 作用:将多个对象的功能“混合”到一个对象中。
- 特点:不依赖原型链,灵活实现多继承。
- 示例:
javascript">const canSpeak = {sayHello() {console.log("Hello");} }; const canWalk = {walk() {console.log("Walking");} };const person = Object.assign({}, canSpeak, canWalk); person.sayHello(); // "Hello" person.walk(); // "Walking"
- 注意:属性冲突时,后者覆盖前者。
8. 深复制(特殊情况)
- 作用:完全复制对象,包括嵌套属性,不涉及原型继承。
- 特点:用于独立复制整个对象。
- 示例:
javascript">const parent = {info: { name: "Alice" },sayHello() {console.log("Hello");} };const child = JSON.parse(JSON.stringify(parent)); child.info.name = "Bob"; console.log(parent.info.name); // "Alice"(互不影响)
- 限制:
JSON
方法不支持函数和原型,且只适用于可序列化的数据。也可以用库(如 Lodash 的_.cloneDeep
)。
方法对比
方法 | 类型 | 是否涉及原型 | 复制深度 | 适用场景 |
---|---|---|---|---|
Object.create | 继承 | 是 | - | 原型链继承 |
构造函数+原型 | 继承 | 是 | - | 模拟类继承(ES5) |
class extends | 继承 | 是 | - | 现代类继承(ES6+) |
扩展运算符 | 复制 | 否 | 浅复制 | 简单对象复制 |
Object.assign | 复制 | 否 | 浅复制 | 属性合并 |
__proto__ | 继承 | 是 | - | 低级原型操作(不推荐) |
Mixin | 复制/混合 | 否 | 浅复制 | 多功能组合 |
深复制(JSON) | 复制 | 否 | 深复制 | 完全独立对象 |
总结
- 继承(基于原型):
Object.create
、class extends
、构造函数+原型是主要方式,适合需要原型链的场景。 - 复制(不基于原型):扩展运算符、
Object.assign
、深复制适用于属性拷贝,不涉及继承。 - 混合方式:Mixin 用于组合多个对象的功能。
在现代 JavaScript 中,推荐使用 class extends
(清晰且符合 OOP 习惯)或函数式方法(简单场景),避免直接操作低级原型(如 __proto__
)。你有具体的需求吗?我可以帮你选个最适合的方法实现!
.__proto__和.prototype有什么区别?我已经完全混乱了,我也不太理解构造函数
别担心!我完全理解这些概念一开始可能会让人头脑一片迷雾,尤其是 __proto__
和 prototype
,还有构造函数的运作方式。我会用最简单的方式一步步解释,尽量让你理清思路。如果有任何地方还是不明白,可以随时打断我!
先从基础开始:什么是构造函数?
咱们先把构造函数弄清楚,因为它是 __proto__
和 prototype
的基础。
构造函数的简单理解
构造函数就是一个普通的函数,但它的目的是通过 new
关键字来创建对象。它像一个“模板”,告诉你怎么造出一个新对象。
- 例子:
javascript">function Person(name) {this.name = name; // 设置新对象的属性 }const alice = new Person("Alice"); // 相当于:/*1. const alice = {};2. alice.__proto__ = Person.prototype;3. Person.call(alice, "Alice");4. 返回 alice*/ console.log(alice.name); // "Alice"
- 发生了什么:
new Person("Alice")
创建了一个空对象{}
。- 把这个空对象的“内部指针”指向
Person.prototype
(这就是__proto__
的作用,后面讲)。 - 把
this
绑定到这个新对象上,执行函数体(给对象加了name
属性)。 - 返回这个新对象,赋值给
alice
。
关键点
- 构造函数本身只是个函数,
new
让它变成“造对象”的工具。 - 它会自动返回一个新对象(除非你手动返回其他东西)。
__proto__
和 prototype
的区别
现在我们进入正题!这两个东西名字很像,但作用完全不同。
1. prototype
(构造函数的属性)
-
是什么:
prototype
是构造函数的一个属性(一个对象),它定义了所有通过这个构造函数创建的实例可以共享的属性和方法。 -
谁有它:只有函数(尤其是打算用作构造函数的函数)有
prototype
属性。 -
作用:当你用
new
创建对象时,新对象的“原型”会指向这个prototype
。 -
例子:
javascript">function Person(name) {this.name = name; } Person.prototype.sayHello = function() {console.log("Hello, " + this.name); };const alice = new Person("Alice"); alice.sayHello(); // "Hello, Alice"
- 这里
Person.prototype
是一个对象,里面放了sayHello
方法。 alice
能调用sayHello
,因为它的“原型”指向了Person.prototype
。
- 这里
-
小结:
prototype
是构造函数的“蓝图”,决定了实例能继承什么。
2. __proto__
(对象的内部属性)
-
是什么:
__proto__
是每个对象都有的一个隐藏属性(内部指针),它指向这个对象的原型(也就是它继承的那个对象)。 -
谁有它:所有对象(包括普通对象、数组、函数等)都有
__proto__
。 -
作用:当你访问一个对象的属性或方法时,如果对象本身没有,JS 会通过
__proto__
去原型上找。 -
例子(接上面的代码):
javascript">console.log(alice.__proto__ === Person.prototype); // true
alice.__proto__
指向Person.prototype
,所以alice
能用sayHello
。- 如果你在
alice
上找不到某个属性,JS 会顺着__proto__
去Person.prototype
找。
-
小结:
__proto__
是对象和它的原型之间的“连接线”。
用图来理清关系
假设有以下代码:
javascript">function Person(name) {this.name = name;
}
Person.prototype.sayHello = function() {console.log("Hello, " + this.name);
};
const alice = new Person("Alice");
- 关系图:
Person (函数)|| .prototype (对象)| ├── sayHello: function| └── constructor: Person| alice (对象)|| .__proto__ 指向 Person.prototype|└── name: "Alice"
Person.prototype
是Person
的属性,定义了共享的方法。alice.__proto__
是alice
的内部指针,指向Person.prototype
。
再加点例子巩固
示例 1:访问属性
javascript">function Dog(name) {this.name = name;
}
Dog.prototype.bark = function() {console.log("Woof!");
};const myDog = new Dog("Buddy");
console.log(myDog.name); // "Buddy"(直接在 myDog 上)
console.log(myDog.bark()); // "Woof!"(从 myDog.__proto__ 找到)
console.log(myDog.__proto__ === Dog.prototype); // true
示例 2:原型链
javascript">const animal = {eat: function() {console.log("Eating...");}
};function Cat(name) {this.name = name;
}
Cat.prototype = animal; // 设置原型(继承 animal)const myCat = new Cat("Whiskers");
myCat.eat(); // "Eating..."(从 myCat.__proto__ 找到)
console.log(myCat.__proto__ === animal); // true
常见困惑解答
-
为什么我看不到
__proto__
?- 它是内部属性,不是直接定义的。你可以用
Object.getPrototypeOf(obj)
替代__proto__
,更标准:javascript">console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
__proto__
是历史遗留写法,不推荐直接用。
- 它是内部属性,不是直接定义的。你可以用
-
普通函数有
prototype
吗?- 是的,所有函数都有
prototype
,但只有用作构造函数时才有用:javascript">function justFunction() {} console.log(justFunction.prototype); // 存在,是个对象
- 是的,所有函数都有
-
对象有
prototype
吗?- 没有!普通对象只有
__proto__
,没有prototype
。只有函数才有prototype
。
- 没有!普通对象只有
总结:proto vs prototype
属性 | 谁拥有它 | 作用 | 示例值 |
---|---|---|---|
prototype | 函数(构造函数) | 定义实例可以继承的属性和方法 | Person.prototype |
__proto__ | 所有对象 | 指向对象的原型(连接原型链) | alice.__proto__ |
prototype
:是构造函数的“工具箱”,给所有实例共享用。__proto__
:是实例的“导航箭头”,指着它的原型。
再回到构造函数
构造函数的核心就是通过 new
:
- 创建一个新对象。
- 把新对象的
__proto__
设为构造函数的prototype
。 - 初始化对象(用
this
)。 - 返回对象。
如果你还是觉得晕,我可以用更生活化的比喻(比如工厂和工人)再讲一遍,或者你告诉我具体哪里卡住了,我针对性解释!怎么样?有没有稍微清晰一点了?呜呜不要紧,我们慢慢来!
形象生动讲解
好的!这次我用一个更生活化、更形象的比喻来讲解 __proto__
和 prototype
,还有构造函数的运作方式。想象我们在一个“魔法玩具工厂”里,目标是制造会说话的小机器人玩具。让我们一步步走进这个场景,把这些概念变得生动起来!
场景设定:魔法玩具工厂
- 你是工厂的老板,想批量生产会说话的小机器人。
- 每个机器人都有自己的名字(个性化属性),还能说“Hello”(共享技能)。
- 工厂里有一些工具和角色,我们用它们来解释构造函数、
prototype
和__proto__
。
1. 构造函数:工厂的“机器人制造机”
-
比喻:构造函数是一个“机器人制造机”。你把原材料(比如名字)放进去,按下“启动”(
new
),它就吐出一个新的机器人。 -
形象化:
- 机器的名字叫
Person
。 - 你输入“Alice”作为原料,按下“启动”,机器就造出一个名叫 Alice 的机器人。
- 机器的名字叫
-
代码:
javascript">function Person(name) {this.name = name; // 给新机器人贴上名字标签 }const alice = new Person("Alice"); // 按下“启动”按钮 console.log(alice.name); // "Alice"
-
发生了什么:
- 工厂启动,制造机创建一个空壳机器人(
{}
)。 - 机器给这个空壳贴上“Alice”的名字标签(
this.name = name
)。 - 最后把成品机器人交给你(返回对象)。
- 工厂启动,制造机创建一个空壳机器人(
-
关键点:这个“制造机”(
Person
)本身不会说话,但它知道怎么给机器人装上零件。它还有个秘密武器——“技能蓝图”(这就是prototype
)。
2. prototype
:技能蓝图
-
比喻:
prototype
是工厂里的“技能蓝图”,一张写着“所有机器人都会的技能”的图纸。制造机(Person
)会把这张图纸交给每一个新机器人,让它们学会蓝图上的技能。 -
形象化:
- 你在蓝图上写下:“所有机器人都会说 Hello”。
- 每个机器人出厂时,都会带上这张蓝图的“使用权”。
-
代码:
javascript">Person.prototype.sayHello = function() {console.log("Hello, " + this.name); };const alice = new Person("Alice"); alice.sayHello(); // "Hello, Alice"
-
发生了什么:
Person.prototype
是制造机的一个特殊抽屉,里面放着蓝图。- 蓝图上写着
sayHello
的技能(一个函数)。 - 每个机器人(比如
alice
)出厂时,制造机会偷偷告诉它:“如果你需要技能,去我的蓝图上看。”
-
关键点:
prototype
是制造机(构造函数)的财产,不是机器人自己的东西。- 所有用
Person
造出来的机器人都共享同一张蓝图,节省工厂资源。
3. __proto__
:机器人的“技能导航仪”
-
比喻:
__proto__
是每个机器人身上装的一个小导航仪,告诉它:“如果你自己不会什么技能,就去蓝图(prototype
)那儿找。” -
形象化:
- 小机器人 Alice 想要说“Hello”,但它自己没装这个技能。
- 它打开导航仪(
__proto__
),导航仪指向工厂的蓝图(Person.prototype
)。 - 在蓝图上找到
sayHello
,然后用自己的名字喊出来。
-
代码:
javascript">console.log(alice.__proto__ === Person.prototype); // true alice.sayHello(); // "Hello, Alice"
-
发生了什么:
alice
是工厂造出来的机器人,它的__proto__
导航仪指向Person.prototype
。- 当你叫
alice.sayHello()
时:- Alice 发现自己没这个技能。
- 它用导航仪查到蓝图(
Person.prototype
)。 - 在蓝图上找到
sayHello
,然后用自己的名字(this.name
)执行。
-
关键点:
__proto__
是机器人(对象)自己的东西,指向它的“出身蓝图”。- 它像一根绳子,把机器人和工厂的蓝图连起来。
工厂的完整运作流程
- 老板下单:
const alice = new Person("Alice")
。- 工厂启动制造机(
Person
),输入原料“Alice”。
- 工厂启动制造机(
- 制造机器人:
- 制造机造一个空壳(
{}
)。 - 给空壳装上名字“Alice”(
this.name = name
)。 - 把导航仪(
__proto__
)调到指向蓝图(Person.prototype
)。
- 制造机造一个空壳(
- 交付:成品机器人
alice
出厂,能用蓝图上的技能(sayHello
)。
- 结果:
alice.name
是它自己的零件(“Alice”)。alice.sayHello
是通过导航仪从蓝图借来的技能。
再加个场景:继承
-
比喻:工厂升级了,想造一种会跑的机器人(
Runner
),它继承了普通机器人的技能。 -
代码:
javascript">function Runner(name) {Person.call(this, name); // 借用 Person 的零件 } Runner.prototype = Object.create(Person.prototype); // 复制蓝图 Runner.prototype.run = function() {console.log(this.name + " is running"); };const bob = new Runner("Bob"); bob.sayHello(); // "Hello, Bob"(从 Person 的蓝图) bob.run(); // "Bob is running"(自己的蓝图)
-
形象化:
Runner
是新款制造机,它先借用Person
的零件(名字)。- 它复制了
Person
的蓝图(Object.create
),然后加了自己的技能“跑步”。 bob
的导航仪(__proto__
)指向Runner.prototype
,而Runner.prototype
的导航仪又指向Person.prototype
。
总结:角色分工
- 构造函数(
Person
):工厂的制造机,负责造机器人。 prototype
:制造机的技能蓝图,写着共享技能(sayHello
)。__proto__
:每个机器人的导航仪,指向它的蓝图。
生活化对比
概念 | 工厂比喻 | 作用 |
---|---|---|
构造函数 | 制造机 | 造出新机器人,装上零件 |
prototype | 技能蓝图 | 定义所有机器人能共享的技能 |
__proto__ | 导航仪 | 告诉机器人去哪找技能 |
检查一下清楚了吗?
- 想象
alice
是一个小机器人,它的名字是“Alice”,它不会说话,但能通过导航仪(__proto__
)找到工厂蓝图(Person.prototype
)上的sayHello
。 - 如果你问
alice
:“你是谁造的?”它会说:“Person
制造机造的,我的技能蓝图在它那儿!”
还有哪里觉得模糊吗?或者想让我再举个例子(比如造个会跳舞的机器人)?这次有没有更形象一点呀?
Person.call(this, name) 什么意思?
- Person.call(this, name);其实就是运行这个Person机器,但是用我给你的“this”来生产对吧。
- 对!完全正确!
Person.call(this, name) 的作用
- 比喻:Person.call(this, name) 就像你拿起电话,打给“Person 制造机”,说:“嘿,我这儿有个空壳机器人(this),你帮我给它装上名字零件(name),但别按你的完整流程走,我只借你装零件的步骤!”
- 形象化:
“Runner 制造机”启动,造出一个空壳机器人(比如 bob)。
它把这个空壳(this)交给“Person 制造机”,说:“给它装上名字‘Bob’。”
“Person 制造机”执行它的逻辑(this.name = name),把名字装到空壳上,然后完事。
call 、bind、apply
好的!让我来详细介绍一下 call
方法,顺便带上你提到的 bind
方法,以及它们跟 apply
的关系(因为这三兄弟经常一起出现)。我会尽量用生动的方式解释,结合我们之前的“魔法玩具工厂”比喻,让你轻松理解它们的用途和区别!
背景:函数的“魔法按钮”
在 JavaScript 中,函数不仅仅是代码块,它们还有一些魔法按钮(方法),可以改变函数的执行方式。这些方法绑定在所有函数上,来自 Function.prototype
,包括 call
、apply
和 bind
。它们的核心作用是控制函数中的 this
和参数。
1. call
方法
是什么?
call
是一个函数方法,可以让你手动指定函数中的this
指向,并且立即执行这个函数。- 语法:
function.call(thisArg, arg1, arg2, ...)
thisArg
:你想让this
指向的对象。arg1, arg2, ...
:传递给函数的参数(逐个列出)。
比喻:工厂的“电话借用服务”
- 想象你在“Runner 制造机”旁边,有个电话。你拨通“Person 制造机”(
Person
函数)的号码,说:“我这儿有个空壳机器人(this
),你帮我执行你的逻辑,把名字装上吧!” - “Person 制造机”接到电话,用你的空壳(指定的
this
)执行它的代码。
示例
javascript">function Person(name) {this.name = name;console.log("Person says: My name is " + this.name);
}function Runner(name) {Person.call(this, name); // 借用 Person 的逻辑
}const bob = new Runner("Bob"); // "Person says: My name is Bob"
console.log(bob.name); // "Bob"
- 发生了什么:
Runner
造了个空壳(this
是{}
)。- 用
call
打电话给Person
,说:“用我的空壳,装上名字‘Bob’。” Person
执行,把this.name = "Bob"
装到bob
上。
用处
- 借用功能:像上面这样,复用其他函数的逻辑。
- 控制
this
:在普通调用中,this
可能指向window
或undefined
(严格模式),call
让你指定它。
2. bind
方法
是什么?
bind
也是函数方法,但它不会立即执行函数,而是返回一个新函数,这个新函数的this
被永久绑定到你指定的对象。- 语法:
function.bind(thisArg, arg1, arg2, ...)
- 返回一个新函数,
this
固定为thisArg
。 - 可以预先绑定部分参数(柯里化)。
- 返回一个新函数,
比喻:工厂的“遥控器定制”
- 想象你不想每次都打电话给“Person 制造机”,而是定制一个遥控器(新函数)。这个遥控器已经设定好:无论在哪按下按钮,
this
永远指向你指定的机器人。 - 你拿着遥控器,按下按钮时才会执行。
示例
javascript">function Person(name) {this.name = name;console.log("Hello, " + this.name);
}const robot = { name: "Robo" };
const boundPerson = Person.bind(robot); // 返回新函数,this 绑定到 robot
boundPerson("Alice"); // "Hello, Robo"(name 参数被忽略,因为 this 已绑定)
- 发生了什么:
bind
创建了一个新函数boundPerson
,它的this
永久指向robot
。- 调用时,
this.name
用的是robot.name
,传入的 “Alice” 没起作用(除非函数逻辑用参数)。
用处
- 固定
this
:比如事件监听器中,防止this
乱跑:javascript">const obj = {name: "Obj",say: function() {console.log(this.name);} }; const boundSay = obj.say.bind(obj); setTimeout(boundSay, 1000); // "Obj"(不绑定的话,this 可能是 window)
- 预设参数:提前锁定部分参数:
javascript">function add(a, b) {return a + b; } const add5 = add.bind(null, 5); // this 无关,绑定 a=5 console.log(add5(3)); // 8
3. apply
方法(顺便介绍)
是什么?
apply
和call
很像,也是立即执行函数并指定this
,但参数以数组形式传递。- 语法:
function.apply(thisArg, [args])
thisArg
:指定的this
。[args]
:参数数组。
比喻:工厂的“批量订单”
- 你还是打电话给“Person 制造机”,但这次不是一个个零件报过去,而是递过去一张清单(数组),说:“按这个清单装零件!”
示例
javascript">function Person(name, age) {this.name = name;this.age = age;
}function Runner(name, age) {Person.apply(this, [name, age]); // 用数组传递参数
}const bob = new Runner("Bob", 25);
console.log(bob.name); // "Bob"
console.log(bob.age); // 25
- 发生了什么:
apply
把[name, age]
展开,像this.name = name; this.age = age
一样执行。
用处
- 处理数组参数:比如找最大值:
javascript">const numbers = [1, 5, 3, 8]; console.log(Math.max.apply(null, numbers)); // 8
三兄弟对比
方法 | 执行时机 | 参数形式 | 返回值 | 比喻 |
---|---|---|---|---|
call | 立即执行 | 逐个参数 | 函数执行结果 | 电话借用服务 |
apply | 立即执行 | 参数数组 | 函数执行结果 | 批量订单清单 |
bind | 不执行,返回新函数 | 逐个参数(可预设) | 新函数 | 定制遥控器 |
图解工厂场景
call
:你喊:“Person,帮我装名字‘Bob’,现在就干!”(马上装好)apply
:你递清单:“Person,按 [‘Bob’, 25] 装零件,现在开工!”(马上装好)bind
:你定制遥控器:“Person,锁定这个机器人,以后我按按钮你就装名字。”(等你按)
回到你的问题
Person.call
:我们之前用它让“Runner 制造机”借用“Person”的装名字功能,直接把名字装到bob
上。bind
:如果你用Person.bind(this)(name)
,会返回一个绑定了this
的新函数,但不会立即执行,得再调用一次。
对比代码
javascript">function Person(name) {this.name = name;
}function Runner(name) {Person.call(this, name); // 立即装名字
}function RunnerBind(name) {const bound = Person.bind(this); // 只定制,不执行bound(name); // 手动再按一下按钮
}const bob = new Runner("Bob"); // "Bob"(call 直接生效)
const bob2 = new RunnerBind("Bob"); // "Bob"(bind 需两步)
总结
call
:借用功能,马上干活,适合继承零件(像Runner
借Person
)。bind
:定制一个新函数,随时用,常用于固定this
或预设参数。- 额外福利
apply
:和call
类似,但爱用数组传参。