鲁斯前端布鲁斯前端

文章中英模式

布鲁斯前端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);
};