為什么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) 簡單性和靈活性
-
原型更簡單
基于類的語言需要定義類的藍圖(模板),然后通過實例化生成對象。這種機制在復雜系統中很強大,但在簡單的腳本語言中顯得繁瑣。原型機制直接讓對象可以“復制”或“繼承”另一個對象的行為,不需要額外的類定義。例如:const person = {sayHello() {console.log("Hello");} }; const alice = Object.create(person); alice.sayHello(); // "Hello"
這比定義一個類、再實例化要直截了當。
-
動態性
原型允許運行時修改對象的結構。你可以隨時給原型添加方法,所有基于這個原型的對象都會自動獲得新行為: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)或復雜的多重繼承。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
只是語法糖,底層仍然是原型機制。例如:
class Person {constructor(name) {this.name = name;}
}
實際上等價于:
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()
- 作用:創建一個新對象,并將其原型設置為指定對象,實現繼承。
- 特點:直接操作原型鏈,簡單且靈活。
- 示例:
const parent = {sayHello() {console.log("Hello from parent");} };const child = Object.create(parent); child.sayHello(); // "Hello from parent"
- 注意:
child
繼承了parent
的屬性和方法,但自身是空的,可以添加新屬性。
2. 通過構造函數和 prototype
- 作用:利用構造函數和原型鏈實現繼承,模擬類的行為。
- 特點:傳統方式,廣泛用于 ES5 及之前。
- 示例:
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 的類語法實現繼承(底層仍是原型)。
- 特點:語法糖,更直觀,適合現代開發。
- 示例:
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. 通過對象字面量和擴展運算符(淺復制)
- 作用:復制對象的屬性(不涉及原型鏈),實現簡單的“復制”。
- 特點:不完全是繼承,更像是屬性拷貝,適用于簡單場景。
- 示例:
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()
- 作用:將一個或多個源對象的可枚舉屬性復制到目標對象。
- 特點:淺復制,常用于合并對象。
- 示例:
const parent = {name: "Alice",sayHello() {console.log("Hello");} };const child = Object.assign({}, parent); child.name = "Bob"; child.sayHello(); // "Hello"
- 限制:和擴展運算符類似,不復制原型鏈。
6. 通過 __proto__
(不推薦)
- 作用:直接設置對象的
__proto__
屬性,指定原型。 - 特點:低級操作,靈活但不推薦(性能差且已被廢棄)。
- 示例:
const parent = {sayHello() {console.log("Hello");} };const child = { __proto__: parent }; child.sayHello(); // "Hello"
- 警告:
__proto__
是非標準特性,可能不支持,且不建議在生產代碼中使用。
7. 通過混入(Mixin)
- 作用:將多個對象的功能“混合”到一個對象中。
- 特點:不依賴原型鏈,靈活實現多繼承。
- 示例:
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. 深復制(特殊情況)
- 作用:完全復制對象,包括嵌套屬性,不涉及原型繼承。
- 特點:用于獨立復制整個對象。
- 示例:
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
關鍵字來創建對象。它像一個“模板”,告訴你怎么造出一個新對象。
- 例子:
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
。 -
例子:
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__
去原型上找。 -
例子(接上面的代碼):
console.log(alice.__proto__ === Person.prototype); // true
alice.__proto__
指向Person.prototype
,所以alice
能用sayHello
。- 如果你在
alice
上找不到某個屬性,JS 會順著__proto__
去Person.prototype
找。
-
小結:
__proto__
是對象和它的原型之間的“連接線”。
用圖來理清關系
假設有以下代碼:
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:訪問屬性
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:原型鏈
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__
,更標準:console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
__proto__
是歷史遺留寫法,不推薦直接用。
- 它是內部屬性,不是直接定義的。你可以用
-
普通函數有
prototype
嗎?- 是的,所有函數都有
prototype
,但只有用作構造函數時才有用: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 的機器人。
- 機器的名字叫
-
代碼:
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”。
- 每個機器人出廠時,都會帶上這張藍圖的“使用權”。
-
代碼:
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
,然后用自己的名字喊出來。
-
代碼:
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
),它繼承了普通機器人的技能。 -
代碼:
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
)執行它的代碼。
示例
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
永遠指向你指定的機器人。 - 你拿著遙控器,按下按鈕時才會執行。
示例
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
亂跑:const obj = {name: "Obj",say: function() {console.log(this.name);} }; const boundSay = obj.say.bind(obj); setTimeout(boundSay, 1000); // "Obj"(不綁定的話,this 可能是 window)
- 預設參數:提前鎖定部分參數:
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 制造機”,但這次不是一個個零件報過去,而是遞過去一張清單(數組),說:“按這個清單裝零件!”
示例
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
一樣執行。
用處
- 處理數組參數:比如找最大值:
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
的新函數,但不會立即執行,得再調用一次。
對比代碼
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
類似,但愛用數組傳參。