JavaScript高级程序设计笔记 8

第8章 对象、类与面向对象编程 — 快速复习笔记

1. 理解对象

1.1 属性类型

  • 数据属性
    • [[Configurable]]:能否删除/修改属性特性(默认 true)
    • [[Enumerable]]:能否 for-in 遍历(默认 true)
    • [[Writable]]:能否修改值(默认 true)
    • [[Value]]:实际存储的值(默认 undefined)
  • 访问器属性(getter/setter):
    • [[Configurable]][[Enumerable]] 同上
    • [[Get]]:读取时调用(默认 undefined)
    • [[Set]]:写入时调用(默认 undefined)

1.2 操作属性特性

  • Object.defineProperty(obj, prop, descriptor):定义单个属性
  • Object.defineProperties(obj, props):定义多个属性
  • Object.getOwnPropertyDescriptor(obj, prop):获取属性描述符
  • Object.getOwnPropertyDescriptors(obj):获取所有属性描述符(ES2017)
let person = {};
Object.defineProperty(person, "name", {
  writable: false,
  value: "Nicholas"
});
console.log(person.name); // "Nicholas"
person.name = "Greg";     // 非严格模式静默失败,严格模式抛出错误

2. 创建对象

2.1 工厂模式

function createPerson(name, age) {
  let o = new Object();
  o.name = name;
  o.age = age;
  o.sayName = function() { console.log(this.name); };
  return o;
}
let p1 = createPerson("Nicholas", 29);
  • 问题:无法识别对象类型(都是 Object 实例)。

2.2 构造函数模式

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayName = function() { console.log(this.name); };
}
let p1 = new Person("Nicholas", 29);
  • new 操作符过程
    1. 创建新对象
    2. 将构造函数的作用域赋给新对象(this 指向新对象)
    3. 执行构造函数代码
    4. 返回新对象
  • 问题:每个实例都有自己的方法副本(无法共享)。

2.3 原型模式

function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.sayName = function() { console.log(this.name); };
let p1 = new Person();
let p2 = new Person();
console.log(p1.sayName === p2.sayName); // true
  • 原型对象:每个函数都有 prototype 属性指向原型对象,实例有 __proto__(或 Object.getPrototypeOf())指向构造函数的原型。
  • 原型链查找:实例属性 → 原型属性 → 更上层原型,直到 Object.prototype
  • in 操作符:无论实例属性还是原型属性,只要存在就返回 true。
  • hasOwnProperty():仅当属性存在于实例本身时返回 true。
  • 问题:原型上的引用类型属性会被所有实例共享(如 Person.prototype.friends = ["a","b"],修改会影响所有实例)。

2.4 组合使用构造函数和原型模式(最常用)

function Person(name, age) {
  this.name = name;      // 实例独有
  this.age = age;
  this.friends = [];
}
Person.prototype.sayName = function() {  // 共享方法
  console.log(this.name);
};

2.5 动态原型模式

  • 在构造函数中动态检查并添加原型方法(仅在需要时创建)。
function Person(name) {
  this.name = name;
  if (typeof this.sayName !== "function") {
    Person.prototype.sayName = function() { console.log(this.name); };
  }
}

3. 继承

3.1 原型链继承(基本但有问题)

function Parent() { this.name = "parent"; }
Parent.prototype.sayHi = function() { console.log("hi"); };
function Child() {}
Child.prototype = new Parent();
let c = new Child();
  • 问题
    1. 引用类型属性被所有实例共享
    2. 无法在不影响所有实例的情况下给父类构造函数传参

3.2 盗用构造函数(经典继承)

function Parent(name) { this.name = name; this.colors = ["red"]; }
function Child(name) {
  Parent.call(this, name);  // 借用构造函数
}
  • 优点:解决引用共享和传参问题
  • 缺点:方法必须在构造函数中定义,无法复用

3.3 组合继承(最常用)

function Parent(name) {
  this.name = name;
  this.colors = ["red"];
}
Parent.prototype.sayName = function() { console.log(this.name); };
function Child(name, age) {
  Parent.call(this, name);   // 第二次调用 Parent
  this.age = age;
}
Child.prototype = new Parent();  // 第一次调用 Parent
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() { console.log(this.age); };
  • 缺点:父类构造函数被调用了两次。

3.4 寄生组合继承(最优)

function inheritPrototype(child, parent) {
  let prototype = Object.create(parent.prototype); // 创建父原型副本
  prototype.constructor = child;
  child.prototype = prototype;
}
function Parent(name) { this.name = name; }
Parent.prototype.sayName = function() { console.log(this.name); };
function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}
inheritPrototype(Child, Parent);
Child.prototype.sayAge = function() { console.log(this.age); };
  • 优点:只调用一次父构造函数,原型链干净。

4. 类(ES6+)

4.1 类定义

  • 类声明:class Person {}
  • 类表达式:let Person = class {}
  • 类不会被提升(与函数声明不同)

4.2 构造函数与方法

class Person {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log(this.name);
  }
  static create(name) {
    return new Person(name);
  }
}
let p = new Person("Nicholas");
  • 实例方法:定义在 prototype
  • 静态方法:使用 static 关键字,定义在类本身上
  • getter/setter:在类中直接定义 get prop() {}

4.3 类继承

class Animal {
  constructor(name) { this.name = name; }
  speak() { console.log(`${this.name} makes a sound.`); }
}
class Dog extends Animal {
  constructor(name, breed) {
    super(name);  // 调用父类构造函数
    this.breed = breed;
  }
  speak() {
    super.speak();  // 调用父类方法
    console.log(`${this.name} barks.`);
  }
}
  • super 关键字:必须在子类构造函数中先调用 super() 才能使用 this
  • extends 后可以跟任意可构造的函数或类

4.4 类的内部机制

  • ES6 类本质上还是基于原型的语法糖,但增加了一些特性:
    • 类内部代码默认在严格模式下
    • 类方法不可枚举
    • 类必须用 new 调用,不能当作普通函数
    • 类名不会被提升

5. 常见面试题速查

属性与对象

  1. Object.definePropertyObject.defineProperties 的区别?
    → 前者定义单个属性,后者定义多个属性。

  2. hasOwnPropertyin 操作符的区别?
    hasOwnProperty 只检查实例属性,in 检查实例和原型链上的属性。

  3. 如何获取对象的原型?
    Object.getPrototypeOf(obj)obj.__proto__(非标准)。

创建对象

  1. 构造函数模式有什么缺点?
    → 每个实例的方法无法共享,造成内存浪费。

  2. 原型模式有什么缺点?
    → 引用类型属性会被所有实例共享,修改一个实例会影响其他实例。

  3. 组合构造函数与原型模式的优点?
    → 实例属性独立,方法共享,最常用。

继承

  1. 原型链继承的缺点?
    → 引用共享、无法传参。

  2. 组合继承的缺点?
    → 父类构造函数被调用两次,存在效率问题。

  3. 寄生组合继承为什么是最优?
    → 只调用一次父构造函数,原型链干净,避免了额外的开销。

ES6 类

  1. ES6 类与 ES5 构造函数的区别?
    → 类必须用 new 调用、内部严格模式、方法不可枚举、无提升、可继承内置对象。

  2. super 关键字的作用?
    → 在子类构造函数中调用父类构造函数(super()),或在子类方法中调用父类方法(super.method())。

  3. 子类构造函数中为什么必须先调用 super()
    → 为了继承父类的 this 对象,否则无法使用 this

  4. 静态方法如何定义?
    → 使用 static 关键字,通过类名调用,不通过实例。

  5. 类中如何定义 getter/setter?
    → 使用 get prop()set prop(value)

posted @ 2024-04-11 09:29  Li_pk  阅读(9)  评论(0)    收藏  举报