【精華】為什么class在前端開發中不常用?

為什么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),尤其是 noImplicitAnystrictPropertyInitialization,TypeScript 會更嚴格地檢查類型。直接修改原型而不聲明類型會更容易觸發錯誤,迫使你顯式定義接口或類型。

5. 對內置對象原型的修改

如果你嘗試修改內置對象的原型(如 Object.prototypeArray.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 為什么不完全禁止?

  1. 兼容 JavaScript
    TypeScript 是 JavaScript 的超集,必須兼容 JS 的所有特性,包括原型機制。如果直接禁止修改原型,會破壞與現有 JS 代碼的兼容性。

  2. 動態性保留
    JavaScript 的動態特性(如運行時修改原型)是其核心優勢之一,TypeScript 不想完全剝奪這種靈活性,而是通過類型系統引導開發者更安全地使用。

  3. 實際用例
    在一些場景中(比如 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"
    
  • 步驟
    1. callapply 復制父構造函數的屬性。
    2. Object.create 設置原型鏈。
    3. 修正 constructor 屬性(可選)。

3. 通過 ES6 的 classextends

  • 作用:使用 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.createclass 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"
    
  • 發生了什么
    1. new Person("Alice") 創建了一個空對象 {}
    2. 把這個空對象的“內部指針”指向 Person.prototype(這就是 __proto__ 的作用,后面講)。
    3. this 綁定到這個新對象上,執行函數體(給對象加了 name 屬性)。
    4. 返回這個新對象,賦值給 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.prototypePerson 的屬性,定義了共享的方法。
  • 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

常見困惑解答

  1. 為什么我看不到 __proto__

    • 它是內部屬性,不是直接定義的。你可以用 Object.getPrototypeOf(obj) 替代 __proto__,更標準:
      console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
      
    • __proto__ 是歷史遺留寫法,不推薦直接用。
  2. 普通函數有 prototype 嗎?

    • 是的,所有函數都有 prototype,但只有用作構造函數時才有用:
      function justFunction() {}
      console.log(justFunction.prototype); // 存在,是個對象
      
  3. 對象有 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"
    
  • 發生了什么

    1. 工廠啟動,制造機創建一個空殼機器人({})。
    2. 機器給這個空殼貼上“Alice”的名字標簽(this.name = name)。
    3. 最后把成品機器人交給你(返回對象)。
  • 關鍵點:這個“制造機”(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() 時:
      1. Alice 發現自己沒這個技能。
      2. 它用導航儀查到藍圖(Person.prototype)。
      3. 在藍圖上找到 sayHello,然后用自己的名字(this.name)執行。
  • 關鍵點

    • __proto__ 是機器人(對象)自己的東西,指向它的“出身藍圖”。
    • 它像一根繩子,把機器人和工廠的藍圖連起來。

工廠的完整運作流程

  1. 老板下單const alice = new Person("Alice")
    • 工廠啟動制造機(Person),輸入原料“Alice”。
  2. 制造機器人
    • 制造機造一個空殼({})。
    • 給空殼裝上名字“Alice”(this.name = name)。
    • 把導航儀(__proto__)調到指向藍圖(Person.prototype)。
  3. 交付:成品機器人 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,包括 callapplybind。它們的核心作用是控制函數中的 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 可能指向 windowundefined(嚴格模式),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 方法(順便介紹)

是什么?
  • applycall 很像,也是立即執行函數并指定 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:借用功能,馬上干活,適合繼承零件(像 RunnerPerson)。
  • bind:定制一個新函數,隨時用,常用于固定 this 或預設參數。
  • 額外福利 apply:和 call 類似,但愛用數組傳參。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/72048.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/72048.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/72048.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【六祎 - Note】SQL備忘錄;DDL,DML,DQL,DCL

SQL備忘錄 from to : 點擊訪問源地址

阿里云物聯網獲取設備屬性api接口:QueryDevicePropertyData

阿里云物聯網接口&#xff1a;QueryDevicePropertyData 說明&#xff1a;調用該接口查詢指定設備或數字孿生節點&#xff0c;在指定時間段內&#xff0c;單個屬性的數據 比如提取上傳到物聯網的溫度數據 api文檔&#xff1a;QueryDevicePropertyData_物聯網平臺_API文檔-阿里…

需求和開發模型

文章目錄 什么是需求&#xff1f;用戶需求軟件需求用戶需求和軟件需求的不同 開發模型什么是“模型”&#xff1f;軟件的生命周期常見的開發模型瀑布模型&#xff08;Waterfall Model&#xff09;螺旋模型增量模型、迭代模型敏捷模型 測試模型V 模型W 模型&#xff08;雙 V 模型…

21-發糖果

n 個孩子站成一排。給你一個整數數組 ratings 表示每個孩子的評分。 你需要按照以下要求&#xff0c;給這些孩子分發糖果&#xff1a; 每個孩子至少分配到 1 個糖果。 相鄰兩個孩子評分更高的孩子會獲得更多的糖果。 請你給每個孩子分發糖果&#xff0c;計算并返回需要準備的 最…

sql深入學習

文章目錄 前言知識學習注釋的兩種形式字符型注入萬能密碼 布爾盲注報錯注入堆疊注入時間盲注二次注入 小技巧 前言 這次學習建立在對數據庫有基本的認識&#xff0c;了解基礎的增刪改查語句&#xff0c;數字型注入和字符型注入的基礎上&#xff0c;進一步深入學習知識&#xf…

利用three.js在Vue項目中展示重構的stl模型文件

一、目的 為了在前端頁面展示3d打印機打印過程 二、前期準備 完整模型的stl文件和模型切割成的n個stl文件 models文件夾下的文件就是切割后的stl文件 三、代碼 <template><div ref"threeContainer" class"three-container"></div><…

【Eureka 緩存機制】

今天簡單介紹一下Eureka server 的緩存機制吧?????? 一、先來個小劇場&#xff1a;服務發現的"拖延癥" 想象你是個外賣小哥&#xff08;客戶端&#xff09;&#xff0c;每次接單都要打電話問調度中心&#xff08;Eureka Server&#xff09;&#xff1a;“現在…

Python--內置模塊和開發規范(下)

2. 開發規范 2.1 單文件應用 文件結構示例 # 文件注釋 import os import jsonDB_PATH "data.json" # 常量放頂部def load_data():"""函數注釋&#xff1a;加載數據"""if os.path.exists(DB_PATH):with open(DB_PATH, "r"…

go設計模式

劉&#xff1a;https://www.bilibili.com/video/BV1kG411g7h4 https://www.bilibili.com/video/BV1jyreYKE8z 1. 單例模式 2. 簡單工廠模式 代碼邏輯&#xff1a; 原始&#xff1a;業務邏輯層 —> 基礎類模塊工廠&#xff1a;業務邏輯層 —> 工廠模塊 —> 基礎類模塊…

搭建數字化生態平臺公司:痛點與蚓鏈解決方案

在數字技術突飛猛進的當下&#xff0c;數字化生態平臺成為眾多企業實現創新發展、拓展業務版圖的 “秘密工具”。今天&#xff0c;咱們就一起來聊聊搭建這類平臺的公司&#xff0c;看看它們有啥獨特之處&#xff0c;又面臨哪些難題。 一、面臨的痛點 &#xff08;一&#xff0…

標記符號“<”和“>”符號被稱為“尖括號”或“角括號”

你提到的“<”和“>”符號被稱為“尖括號”或“角括號”。它們常用于編程語言中表示類型參數&#xff08;如泛型&#xff09;、HTML標簽&#xff08;如<div>&#xff09;、數學中的不等式&#xff08;如< 5&#xff09;等。 好的&#xff0c;我來用通俗的方式解…

云平臺DeepSeek滿血版:引領AI推理革新,開啟智慧新時代

引言&#xff1a;人工智能的未來——云平臺的卓越突破 在當今科技飛速發展的時代&#xff0c;人工智能&#xff08;AI&#xff09;技術正深刻地改變著我們生活與工作方式的方方面面。作為AI領域的創新者與領航者&#xff0c;云平臺始終走在技術前沿&#xff0c;憑借無窮的熱情…

自然語言處理:文本規范化

介紹 大家好&#xff01;很高興又能在這兒和大家分享自然語言處理相關的知識了。在上一篇發布于自然語言處理&#xff1a;初識自然語言處理-CSDN博客為大家初步介紹了自然語言處理的基本概念。而這次&#xff0c;我將進一步深入這個領域&#xff0c;和大家聊聊自然語言處理中一…

HTTP非流式請求 vs HTTP流式請求

文章目錄 HTTP 非流式請求 vs 流式請求一、核心區別 服務端代碼示例&#xff08;Node.js/Express&#xff09;非流式請求處理流式請求處理 客戶端請求示例非流式請求&#xff08;瀏覽器fetch&#xff09;流式請求處理&#xff08;瀏覽器fetch&#xff09; Python客戶端示例&…

C語言機試編程題

編寫版本&#xff1a;vc2022 1.求最大/小值 #include<stdio.h> int main(){int a[50],n;int max, min;printf("請輸入您要輸入幾個數");scanf_s("%d", &n);printf("請輸入您要比較的%d個數\n",n);for (int i 0; i<n; i) {scanf_…

c++ 多個.cpp文件運行

目錄 方法 1&#xff1a;將其他文件中的 main 改為普通函數 方法 2&#xff1a;使用頭文件組織代碼 方法 3&#xff1a;條件編譯&#xff08;僅用于調試或特殊需求&#xff09; 方法 4&#xff1a;創建類或命名空間管理邏輯 在一個C項目中&#xff0c;多個.cpp文件不能同…

基于OFDR的層壓陸相頁巖油儲層中非對稱裂縫群傳播的分布式光纖監測

關鍵詞&#xff1a;OFDR、分布式光纖傳感、裂縫傳播 一. 概述 四川盆地涼高山組優質頁巖油儲層存在復雜的垂直重疊巖性&#xff0c;大陸頁巖油儲層存在發育層理&#xff0c;薄層和天然裂縫&#xff0c;對水平井多級壓裂技術的裂縫網絡形態控制和監測構成挑戰。本研究提出了一…

UniApp 按鈕組件 open-type 屬性詳解:功能、場景與平臺差異

文章目錄 引言一、open-type 基礎概念1.1 核心作用1.2 通用使用模板 二、主流 open-type 值詳解2.1 contact - 客服會話功能說明平臺支持代碼示例 2.2 share - 內容轉發功能說明平臺支持注意事項 2.3 getUserInfo - 獲取用戶信息功能說明平臺支持代碼示例 2.4 getPhoneNumber -…

【大模型】Ubuntu下 fastgpt 的部署和使用

前言 本次安裝的版本為 fastgpt:v4.8.8-fix2。 最新版本fastgpt:v4.8.20-fix2 問答時報錯&#xff0c;本著跑通先使用起來&#xff0c;就沒有死磕下去&#xff0c;后面bug解了再進行記錄。 ? github連接&#xff1a;https://github.com/labring/FastGPT fastgpt 安裝說明&…

【GenBI實戰】python腳本實現基于DeepSeek api的數據查詢和圖表可視化

寫在前面 生成式 BI (GenBI) 正在改變我們與數據交互的方式。它允許用戶使用自然語言提出問題&#xff0c;并自動獲得數據洞察&#xff0c;而無需編寫復雜的 SQL 查詢或手動創建圖表。本文將帶你動手實戰&#xff0c;使用 Python 和 DeepSeek API (或其他類似的大語言模型 API…