魯斯前端布魯斯前端

文章中英模式

布魯斯前端JS面試題目 - JS的new、原型鏈與Class實作解析

深入講解JavaScript中的new關鍵字、原型鏈機制、Class語法,以及私有變數的實作原理與最佳實踐。

影片縮圖

懶得看文章?那就來看影片吧

建構函數 vs Class語法

JavaScript 可通過建構函數或 ES6 Class 語法創建物件,兩者底層機制相同,但語法與使用方式有差異。

建構函數寫法

// 建構函數
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 原型方法 - 所有實例共享一份
Person.prototype.introduce = function() {
  return `我是 ${this.name},今年 ${this.age}`;
};

// 模擬靜態方法
Person.createAnonymous = function () {
  return new Person('Anonymous', 0);
};

// 建立實例
const john = new Person('小明', 30);
console.log(john.introduce());   // "我是 小明,今年 30 歲"

Class寫法(ES6+)

// Class 語法(ES6+)
class Person {
  // 建構函式
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  // 原型方法(共享)
  introduce() {
    return `I'm ${this.name}, ${this.age} years old`;
  }
  
  // 靜態方法 - 直接掛在類別上,不需要實例化就能調用
  static createAnonymous() {
    return new Person('Anonymous', 0);
  }
}

// 直接使用 new 建立實例
const jane = new Person('Jane', 25);

// 靜態方法調用
const anonymous = Person.createAnonymous();

/* 
直接new vs靜態方法建立實例的差異:

1. 直接new:
   - 優點:直觀明確,完全控制參數
   - 缺點:重複邏輯無法封裝,每次都需要提供完整參數

2. 靜態方法:
   - 優點:封裝複雜建構邏輯,提供預設值,實現工廠模式
   - 優點:可實現單例模式或物件池等設計模式
   - 缺點:間接性可能降低程式碼可讀性

實務應用:
- 當物件建立邏輯複雜或需要重複使用特定配置時,靜態方法更合適
- 常見於React.createElement()、Array.from()等API設計
*/

原型鏈解析

__proto__ prototype

JavaScript 物件系統是基於原型的,有兩個關鍵概念:

  • prototype: 是函式的屬性,包含將被共享給實例的方法與屬性
  • __proto__: 是物件的屬性,指向建立該物件的構造函式的 prototype
function Dog(name) {
  this.name = name;
}

Dog.prototype.bark = function() {
  return `${this.name} says woof!`;
};

const fluffy = new Dog('Fluffy');

console.log(fluffy.__proto__ === Dog.prototype);  // true
console.log(Dog.prototype.constructor === Dog);    // true
console.log(fluffy.bark());                       // "Fluffy says woof!"
function Dog(name) {
  this.name = name;
}

Dog.prototype.bark = function() {
  return `${this.name} says woof!`;
};

const fluffy = new Dog('Fluffy');

console.log(fluffy.__proto__ === Dog.prototype);  // true
console.log(Dog.prototype.constructor === Dog);    // true
console.log(fluffy.bark());                       // "Fluffy says woof!"

原型鏈查找機制

當訪問物件屬性時,JavaScript 引擎會:

  1. 1.
    檢查物件自身是否擁有該屬性
  2. 2.
    若無,則查找物件的 __proto__ 指向的原型
  3. 3.
    若仍無,則繼續沿原型鏈向上查找
  4. 4.
    直到找到屬性或到達原型鏈頂端 (Object.prototype.__proto__ === null)
// 原型鏈示例
function Animal() {}
Animal.prototype.eat = function() { return '吃東西...'; };

function Dog() {}
// 設置原型繼承
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;  // 修正 constructor
Dog.prototype.bark = function() { return '汪汪!'; };

const doggy = new Dog();
console.log(doggy.bark());  // "汪汪!" - 在 Dog.prototype 找到
console.log(doggy.eat());   // "吃東西..." - 在 Animal.prototype 找到

替代 __proto__ 的方法

由於 __proto__ 已被棄用,建議使用以下方法:

// 獲取原型
Object.getPrototypeOf(obj);  // 替代 obj.__proto__

// 設置原型
Object.setPrototypeOf(obj, prototype);  // 替代 obj.__proto__ = prototype

// 創建帶有指定原型的新物件
Object.create(prototype);  // 比 new 更直接

私有變數實作原理

1. 閉包實現私有變數

function Counter() {
  // 私有變數
  let count = 0;
  
  // 公開方法
  this.increment = function() {
    count++;
    return count;
  };
  
  this.decrement = function() {
    count--;
    return count;
  };
  
  this.getValue = function() {
    return count;
  };
}

const counter = new Counter();
console.log(counter.getValue());   // 0
counter.increment();
console.log(counter.getValue());   // 1
console.log(counter.count);        // undefined (無法直接訪問)

2. ES2022 私有屬性 (#)

class BankAccount {
  #balance = 0;  // 私有屬性
  
  constructor(initialBalance) {
    if (initialBalance > 0) {
      this.#balance = initialBalance;
    }
  }
  
  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
      return true;
    }
    return false;
  }
  
  getBalance() {
    return this.#balance;
  }
}

const account = new BankAccount(100);
console.log(account.getBalance());  // 100
// console.log(account.#balance);   // SyntaxError: Private field

🔥 常見面試題目

(一)new 關鍵字做了什麼?請描述其執行過程。

解答:當使用 new 操作符調用函式時,會執行以下步驟:

  1. 1.
    創建一個新的空物件
  2. 2.
    將該物件的 __proto__ 設置為函式的 prototype 屬性
  3. 3.
    將函式內的 this 綁定到新建的物件
  4. 4.
    執行函式中的代碼(通常會初始化物件屬性)
  5. 5.
    如果函式沒有返回對象,則返回新創建的物件
function myNew(Constructor, ...args) {
  // 1. 創建一個新物件,並將其原型指向構造函數的 prototype
  const obj = Object.create(Constructor.prototype);

  // 2. 執行構造函數,並將 this 指向新創建的物件
  const result = Constructor.apply(obj, args);

  // 3. 如果構造函數返回一個物件,則返回該物件;否則返回新創建的物件
  return result instanceof Object ? result : obj;
}

// 使用示例
function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`你好,我是 ${this.name}`);
};

const person = myNew(Person, '小明');
person.sayHello();  // 輸出:你好,我是 小明

(二)prototype 和 __proto__ 有什麼區別?

解答:

  • prototype 是函式特有的屬性,指向一個物件,該物件包含會被實例繼承的屬性和方法
  • __proto__ 是所有物件都有的屬性,指向該物件的構造函式的 prototype
  • 當用 new Foo() 創建物件時,這個物件的 __proto__ 會被設為 Foo.prototype
  • 所以 instance.__proto__ === Constructor.prototype 恒成立

(三)ES6 的 Class 和傳統建構函數有何不同?

解答:

ES6 Class 本質上仍是原型繼承的語法糖,底層機制沒有改變。

比較項目Class 寫法Function 寫法
語法更清晰直觀,OOP風格較為分散,需分別定義原型方法
提升行為不會被提升函式聲明會被提升
繼承實現使用 extends 關鍵字簡潔需手動設置原型鏈,較複雜
靜態方法使用 static 關鍵字定義直接賦值給構造函式
私有屬性支持 # 語法定義真正私有屬性需使用閉包或其他技巧模擬
兼容性ES6+,需考慮舊瀏覽器兼容全面支持,兼容性好

代碼比較:

Class 寫法

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  hello() {
    return `你好,我是 ${this.name}`;
  }
  
  static create(name, age) {
    return new Person(name, age);
  }
  
  #privateField = '私有';
  
  getPrivate() {
    return this.#privateField;
  }
}

Function 寫法

function Person(name, age) {
  this.name = name;
  this.age = age;
  
  // Private variable using closure
  var privateField = 'private';
  this.getPrivate = function() {
    return privateField;
  };
}

// Prototype method
Person.prototype.hello = function() {
  return `Hello, my name is ${this.name}`;
};

// Static method
Person.create = function(name, age) {
  return new Person(name, age);
};