"红宝书" 通常指的是《JavaScript 高级程序设计》,这是一本由 Nicholas C. Zakas(尼古拉斯·扎卡斯)编写的 JavaScript 书籍,是一本广受欢迎的经典之作。这本书是一部翔实的工具书,满满的都是 JavaScript 知识和实用技术。
不管你有没有刷过红宝书,如果现在还没掌握好,那就一起来刷红宝书吧,go!go!go!
系列文章:
第一部分:基本知识(重点、反复阅读)
第二部分:进阶内容(重点、反复阅读)
第 8 章 对象、类与面向对象编程
理解对象
对象
:一组属性的无序集合。
Object.defineProperty()
是 JavaScript 中用于定义或修改对象属性的方法。
通过这个方法,你可以精确地控制属性的行为,包括可写性、可枚举性、可配置性
等。
该方法接受三个参数:要定义属性的对象、属性名和属性描述符对象
。
js
Object.defineProperty(obj, prop, descriptor)
- obj: 要定义属性的对象。
- prop: 要定义或修改的属性名。
- descriptor: 包含属性特性的对象,可以设置以下属性:
- value: 属性的值,默认为
undefined
。 - writable: 属性是否可写,布尔值,默认为
false
。 - enumerable: 属性是否可枚举,布尔值,默认为
false
。 - configurable: 属性是否可配置,布尔值,默认为
false
。 - get: 一个获取属性值的函数。
- set: 一个设置属性值的函数。
- value: 属性的值,默认为
数据属性
js
let obj = {};
Object.defineProperty(obj, 'name', {
value: 'John',
writable: true,
enumerable: true,
configurable: true
});
console.log(obj.name); // John
访问器属性
js
let obj = {
_name: 'John',
get name() {
console.log('Getting name');
return this._name;
},
set name(value) {
console.log('Setting name to', value);
this._name = value;
}
};
obj.name = 'Alice'; // Setting name to Alice
console.log(obj.name); // Getting name, Alice
可配置性
js
let obj = {
name: 'John'
};
Object.defineProperty(obj, 'name', {
configurable: false
});
delete obj.name; // Error: Cannot delete property 'name' of object
Object.defineProperty()
允许你更细致地控制对象属性的行为,特别是在创建或修改对象的属性时非常有用。
Vue2 的数据响应式的原理就是借用了 Object.defineProperty()
定义多个属性
Object.defineProperties()
是 JavaScript 中用于定义或修改多个对象属性的方法。
与 Object.defineProperty()
不同,Object.defineProperties()
接受两个参数:要定义属性的对象和一个包含多个属性描述符的对象。
js
Object.defineProperties(obj, descriptors)
- obj: 要定义属性的对象。
- descriptors: 包含多个属性特性的对象,其中每个键值对的键是属性名,值是属性描述符对象,描述符对象的结构与
Object.defineProperty()
中的描述符相同。
js
let obj = {};
Object.defineProperties(obj, {
name: {
value: 'John',
writable: true,
enumerable: true,
configurable: true
},
age: {
value: 30,
writable: false,
enumerable: true,
configurable: true
},
sayHello: {
value: function() {
console.log('Hello!');
},
enumerable: false,
configurable: true
}
});
console.log(obj.name); // John
console.log(obj.age); // 30
obj.sayHello(); // Hello!
读取属性的特性
Object.getOwnPropertyDescriptor()
是 JavaScript 中用于获取对象属性描述符的方法。它接受两个参数:要获取属性描述符的对象和属性名,然后返回一个包含该属性特性的对象。
js
Object.getOwnPropertyDescriptor(obj, prop)
- obj: 要获取属性描述符的对象。
- prop: 要获取的属性名。
js
let obj = {
name: 'John',
age: 30
};
let descriptor = Object.getOwnPropertyDescriptor(obj, 'name');
console.log(descriptor);
// Output:
// {
// value: 'John',
// writable: true,
// enumerable: true,
// configurable: true
// }
这个方法常用于检查属性的特性,特别是在进行属性的复杂操作或者在使用 Object.defineProperty()
时需要了解已有特性的情况。
合并对象
Object.assign()
是 JavaScript 中用于将一个或多个源对象的属性复制到目标对象的方法。它是一个浅拷贝操作,只复制对象的属性值,不复制对象的引用。如果目标对象已经有相同属性名的属性,它们将被后面的源对象覆盖。
js
Object.assign(target, ...sources)
- target: 目标对象,将源对象的属性复制到这个对象。
- sources: 一个或多个源对象,它们的属性将被复制到目标对象。
js
let target = { a: 1, b: 2 };
let source1 = { b: 3, c: 4 };
let source2 = { d: 5 };
Object.assign(target, source1, source2);
console.log(target);
// Output: { a: 1, b: 3, c: 4, d: 5 }
Object.assign()
不会递归复制对象内部的对象,而是复制对象的引用。如果源对象的属性值是对象,它们仍然会被视为同一对象。
js
let obj = { a: 1, b: { c: 2 } };
// 使用 Object.assign() 克隆对象
let clone = Object.assign({}, obj);
console.log(clone);
// Output: { a: 1, b: { c: 2 } }
console.log(obj === clone); // false
console.log(obj.b === clone.b); // true,因为是浅拷贝
对象标识与相等判定
对象标识
指的是对象在内存中的唯一标识符。两个不同的对象具有不同的标识。可以使用 ===
操作符来比较对象的标识。
js
let obj1 = { name: 'John' };
let obj2 = { name: 'John' };
console.log(obj1 === obj2); // false,不同的对象标识
相等判定
是指比较对象的值是否相等。可以使用 ==
或 ===
进行相等判定。相等判定的结果取决于比较操作符的类型。
js
let obj1 = { name: 'John' };
let obj2 = { name: 'John' };
console.log(obj1 == obj2); // false,因为对象标识不同
console.log(obj1.name == obj2.name); // true,比较对象属性值
Object.is()
是 JavaScript 中用于比较两个值是否严格相等的方法。它的行为与 ===
运算符相似,但有一些区别。主要的区别在于 Object.is()
对于特殊值(+0、-0、NaN)的处理以及对对象的处理。
js
console.log(Object.is(5, 5)); // true
console.log(Object.is(5, '5')); // false
console.log(Object.is(-0, 0)); // false
console.log(Object.is(NaN, NaN)); // true
console.log(Object.is(+0, -0)); // false
console.log(Object.is(+0, 0)); // true
console.log(Object.is(-0, 0)); // false
增强的对象语法
js
// 属性值的简写
let name = "John";
let age = 30;
let person = { name, age };
// 方法的简写
let person = {
name: "John",
sayHello() {
console.log("Hello!");
}
};
// 计算属性名
let prop = "name";
let person = {
[prop]: "John"
};
对象解构
js
// 对象的解构
let person = { name: "John", age: 30 };
let { name, age } = person;
console.log(name); // "John"
console.log(age); // 30
// 别名
let person = { name: "John", age: 30 };
// 使用对象解构,同时给变量取别名
let { name: fullName, age: years } = person;
console.log(fullName); // "John"
console.log(years); // 30
// 默认值
let person = { name: "John" };
// 使用对象解构,设置默认值
let { name, age = 25 } = person;
console.log(name); // "John"
console.log(age); // 25,age 没有在 person 对象中定义,使用了默认值
// 嵌套解构
let person = { name: "John", address: { city: "New York", zip: "10001" } };
// 嵌套对象解构
let { name, address: { city, zip } } = person;
console.log(name); // "John"
console.log(city); // "New York"
console.log(zip); // "10001"
// 剩余属性
let person = { name: "John", age: 30, city: "New York" };
let { name, ...rest } = person;
console.log(name); // "John"
console.log(rest); // { age: 30, city: "New York" }
// 扩展运算符
let person = { name: "John", age: 30 };
let additionalInfo = { city: "New York", occupation: "Developer" };
let combined = { ...person, ...additionalInfo };
创建对象
工厂模式
使用工厂函数来创建对象,工厂函数实际上是一个返回新对象的函数。
js
function createPerson(name, age, gender) {
return {
name: name,
age: age,
gender: gender
};
}
let person = createPerson("John", 30, "Male");
工厂模式虽然可以解决创建多个类似对象的问题,但是没有解决对象标识问题(即新创建对象是什么类型)
构造函数模式
构造函数模式是一种使用构造函数来创建对象的方式。通过使用 new
关键字调用构造函数,可以创建一个新的对象,并且构造函数中的 this
关键字引用了这个新创建的对象。
构造函数名称的首字母都是要大写的。
js
// 构造函数
function Person(name, age, gender) {
// 使用 this 关键字引用新创建的对象
this.name = name;
this.age = age;
this.gender = gender;
// 如果需要添加方法,也可以在构造函数内部定义
this.sayHello = function() {
console.log("Hello, my name is " + this.name);
};
}
// 使用构造函数创建对象
let person1 = new Person("John", 30, "Male");
let person2 = new Person("Alice", 25, "Female");
// 访问属性
console.log(person1.name); // John
console.log(person2.age); // 25
// 调用方法
person1.sayHello(); // Hello, my name is John
person2.sayHello(); // Hello, my name is Alice
person1 instanceof Object // true
person1 instanceof Person // true
定义自定义构造函数可以确保实例被标识为特定类型,相比工厂模式,这是一个很大的好处。但是也有一个弊端,就是导致自定义类型引用的代码不能很好的聚集在一起。
原型模式
对象的原型模式是 JavaScript 中的一种创建和继承对象的方式
。
每个对象都有一个原型(prototype)属性
,原型是一个对象,它包含可以被共享的属性和方法。
当试图访问一个对象的属性或方法时,如果对象本身没有定义这个属性或方法,JavaScript 会从对象的原型链
中寻找。
对象的原型链指的是从对象自身开始,一直到达原型链的顶端。可以通过对象的 __proto__
属性访问其原型。
js
let person = { name: "John" };
console.log(person.__proto__ === Object.prototype); // true
需要注意的是,如果在构造函数中定义方法,每次创建对象时都会为该方法分配新的内存空间。如果有多个实例共享相同的方法,推荐将方法定义在构造函数的原型上,以避免每次创建对象都复制一份方法。
JS
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
// 在构造函数的原型上定义方法
Person.prototype.sayHello = function() {
console.log("Hello, my name is " + this.name);
};
let person1 = new Person("John", 30, "Male");
let person2 = new Person("Alice", 25, "Female");
person1.sayHello(); // Hello, my name is John
person2.sayHello(); // Hello, my name is Alice
person1 instanceof Object // true
person1 instanceof Person // true
isPrototypeOf()
是 JavaScript 中 Object
对象的一个方法,用于检测一个对象是否是另一个对象的原型。该方法返回一个布尔值,如果调用该方法的对象是传入的对象的原型,返回 true
,否则返回 false
。
js
// 定义一个原型对象
let personPrototype = {
sayHello: function() {
console.log("Hello!");
}
};
// 创建一个对象,并将 personPrototype 设置为其原型
let person = Object.create(personPrototype);
// 使用 isPrototypeOf() 判断原型关系
console.log(personPrototype.isPrototypeOf(person)); // true
// 使用 Object.getPrototypeOf() 获取对象的原型
let prototypeOfPerson = Object.getPrototypeOf(person);
console.log(prototypeOfPerson === personPrototype); // true
Object.setPrototypeOf()
可能会严重影响代码性能。不推荐。
为了避免使用 Object.setPrototypeOf() 可能造成的性能下降,可以通过 Object.create() 来创建一个新对象,同时为其指定原型:
js
let biped = {
numLength: 2
}
let person = Object.create(biped)
person.name = 'Matt'
person.name // Matt
person.numLength // 2
Object.getPrototypeOf(person) === biped // true
- Object.keys(obj): 返回一个包含对象所有可枚举属性的数组。
- Object.values(obj): 返回一个包含对象所有可枚举属性值的数组。
- Object.entries(obj): 返回一个包含对象所有可枚举属性键值对的数组。
- Object.hasOwnProperty(prop): 检查对象是否包含指定属性,不会检查原型链。
for-in
循环和 Object.assign()
的枚举顺序是不确定的,取决于 js 引擎,可能因浏览器而异。
Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()、Object.assgin()
的枚举顺序是确定性的。先以升序枚举数值键,然后以插入顺序枚举字符串和符号键。
继承
很多面向对象语言都支持两种继承:接口继承和实现继承
。
实现继承
是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现
的。
原型链继承
原型链继承方式的基本思想:通过原型继承多个引用类型的属性和方法。
构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指向构造函数,而实例有一个内部指针指向原型
。
原型链
是 JavaScript 中用于实现对象继承的一种机制。每个对象都有一个原型对象,而原型对象也可以有自己的原型,形成一个链式结构。
通过原型链,JavaScript 实现了对象之间的继承关系。子对象可以访问父对象的属性和方法,形成了一种基于原型的继承。
js
// 父构造函数
function Animal(name) {
this.name = name;
}
// 在父构造函数的原型上添加方法
Animal.prototype.makeSound = function() {
console.log("Generic animal sound");
};
// 子构造函数
function Dog(name, breed) {
// 实现对父构造函数的属性继承
Animal.call(this, name);
this.breed = breed;
}
// 通过原型链继承
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;
// 创建子对象实例
let myDog = new Dog("Buddy", "Golden Retriever");
// 调用继承的方法
myDog.makeSound(); // Generic animal sound
原型链继承的问题
- 共享问题
原型链继承导致子对象实例共享父对象原型上的属性和方法。如果在子对象实例上修改原型上的属性,会影响到所有子对象实例。
js
let dog1 = new Dog("Buddy", "Golden Retriever");
let dog2 = new Dog("Max", "Labrador");
dog1.makeSound(); // Generic animal sound
dog2.makeSound(); // Generic animal sound
// 修改原型上的属性,会影响所有子对象实例
Dog.prototype.makeSound = function() {
console.log("Woof! Woof!");
};
dog1.makeSound(); // Woof! Woof!
dog2.makeSound(); // Woof! Woof!
- 构造函数参数传递问题
在原型链继承中,子构造函数调用父构造函数时传递的参数存在一定的限制,可能不够灵活。无法在创建子对象实例时向父构造函数传递参数,因为父构造函数的调用发生在子对象实例创建之前。
js
function Animal(name) {
this.name = name;
}
function Dog(breed) {
Animal.call(this, "DefaultName"); // 无法传递具体的名字参数
this.breed = breed;
}
let myDog = new Dog("Golden Retriever");
console.log(myDog.name); // DefaultName
- 原型链深层次的问题
在原型链继承中,可能会存在多层嵌套的原型链,导致属性查找的性能问题。在查找属性时,如果找不到,JavaScript 引擎会沿着原型链向上查找,直到找到或者到达链的顶端。
js
function Animal() {}
function Dog() {}
Dog.prototype = new Animal();
let myDog = new Dog();
// 原型链深度过大可能影响性能
盗用构造函数
盗用构造函数(Constructor Stealing)是一种通过在子构造函数中调用父构造函数来实现继承的技术
。它是继承的一种形式,旨在解决原型链继承的一些问题,如共享属性和构造函数参数传递的限制。
在盗用构造函数的模式中,子构造函数使用父构造函数的实例作为自己的实例,从而达到复用父构造函数的属性的目的。这样,父构造函数的属性不再被共享,而且可以向父构造函数传递参数。
js
// 父构造函数
function Animal(name) {
this.name = name;
this.sound = "Generic animal sound";
}
// 子构造函数
function Dog(name, breed) {
// 盗用构造函数
Animal.call(this, name);
this.breed = breed;
}
// 创建子对象实例
let myDog = new Dog("Buddy", "Golden Retriever");
console.log(myDog.name); // Buddy
console.log(myDog.sound); // Generic animal sound
console.log(myDog.breed); // Golden Retriever
盗用构造函数的问题:
- 方法不共享: 由于方法都在构造函数内部定义,它们不会被共享。每个实例都有自己的方法副本,可能导致内存浪费。
- 无法继承原型链上的方法: 通过盗用构造函数,无法继承父构造函数原型链上的方法,这减少了代码的复用性。
组合继承
组合继承是一种在 JavaScript 中实现继承的模式,结合了构造函数继承和原型链继承的优点
,避免了它们各自的缺点。在组合继承中,使用构造函数来继承属性,同时通过原型链来继承方法
,以实现更有效的继承。
js
// 父构造函数
function Animal(name) {
this.name = name;
}
// 在父构造函数的原型上添加方法
Animal.prototype.makeSound = function() {
console.log("Generic animal sound");
};
// 子构造函数
function Dog(name, breed) {
// 继承属性(通过构造函数调用)
Animal.call(this, name);
this.breed = breed;
}
// 继承方法(通过原型链)
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;
// 子对象实例
let myDog = new Dog("Buddy", "Golden Retriever");
// 调用继承的方法
myDog.makeSound(); // Generic animal sound
组合继承的问题:
- 原型链上多余的实例属性: 子构造函数通过
new Animal()
创建了一个实例,这个实例包含了一些不必要的属性,可能会影响性能。 - 构造函数调用两次: 子构造函数调用了两次父构造函数,一次是通过
Animal.call(this, name)
,一次是通过new Animal()
,可能导致一些不必要的计算和内存浪费。
寄生式继承
寄生式继承的思路:类似于寄生构造函数和工厂模式,创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象
。
js
// 原始对象
function createAnimal(name) {
let animal = {
name: name,
makeSound: function() {
console.log("Generic animal sound");
}
};
// 在原始对象基础上增强
animal.makeCustomSound = function(customSound) {
console.log(customSound);
};
return animal;
}
// 创建继承对象
function createDog(name, breed) {
let dog = createAnimal(name);
// 在继承对象基础上增强
dog.breed = breed;
// 重写方法
let originalMakeSound = dog.makeSound;
dog.makeSound = function() {
originalMakeSound.call(this); // 调用原始方法
console.log("Woof! Woof!");
};
return dog;
}
// 使用寄生式继承创建对象
let myDog = createDog("Buddy", "Golden Retriever");
// 调用继承对象的方法
myDog.makeSound(); // Generic animal sound \n Woof! Woof!
myDog.makeCustomSound("Custom sound!"); // Custom sound!
寄生式继承问题:
- 不同实例之间的方法不共享: 寄生式继承中,每个对象的方法都是独立复制的,不会共享,可能导致一定的内存浪费。
- 构造函数不能传递参数: 与寄生构造函数继承类似,寄生式继承无法传递参数给构造函数,因为并没有使用
new
操作符来调用构造函数。
寄生式组合继承
寄生式组合继承是一种结合构造函数继承和寄生式继承的继承模式
,旨在避免组合继承的缺点。组合继承的问题在于它会调用两次父构造函数,而寄生式组合继承通过寄生式继承来继承父类的原型,避免了不必要的构造函数调用。
js
// 父构造函数
function Animal(name) {
this.name = name;
}
// 在父构造函数的原型上添加方法
Animal.prototype.makeSound = function() {
console.log("Generic animal sound");
};
// 子构造函数
function Dog(name, breed) {
// 继承属性(通过构造函数调用)
Animal.call(this, name);
this.breed = breed;
}
// 寄生式继承父构造函数的原型
function inheritPrototype(child, parent) {
// 创建一个空的对象,该对象的原型指向父构造函数的原型
let prototype = Object.create(parent.prototype);
// 将子构造函数的原型指向该空对象
child.prototype = prototype;
// 修复子构造函数的构造器
child.prototype.constructor = child;
}
// 使用寄生式组合继承
inheritPrototype(Dog, Animal);
// 在子构造函数的原型上添加方法
Dog.prototype.makeSound = function() {
// 调用父构造函数原型上的方法
Animal.prototype.makeSound.call(this);
console.log("Woof! Woof!");
};
// 创建子对象实例
let myDog = new Dog("Buddy", "Golden Retriever");
// 调用继承的方法
myDog.makeSound(); // Generic animal sound \n Woof! Woof!
// 检查构造器
console.log(myDog.constructor); // Dog
类
定义类也有两种主要方式:类声明和类表达式
。
与函数表达式类似,类表达式在它们被求值前也不能引用。不过,与函数定义不同的是,虽然函数声明可以提升,但类定义不能
。
另一个跟函数声明不同的地方是,函数受函数作用域限制,而类受块作用域限制
。
js
// 类声明
class Person {}
// 类表达式
const Animal = class {}
使用 new 调用类的构造函数会执行如下操作:
- 在内存中创建一个新对象
- 在这个新对象内部的 [[Prototype]] 指针被赋值为构造函数的 prototype 属性。
- 构造函数内部的 this 被赋值为这个新对象。
- 执行构造函数内部的代码(给新对象添加属性)。
- 如果构造函数返回非空对象,则返回该对象,否则返回刚创建的新对象。
在 ES6 中,使用 class
和 extends
语法糖,继承关系更加清晰,而不需要手动操作原型链。 super
关键字用于在子类的构造函数中调用父类的构造函数,实现对父类属性的继承。
js
// 父类
class Animal {
constructor(name) {
this.name = name;
}
makeSound() {
console.log("Generic animal sound");
}
}
// 子类继承父类
class Dog extends Animal {
constructor(name, breed) {
// 调用父类构造函数,实现属性继承
super(name);
this.breed = breed;
}
// 子类添加自己的方法
bark() {
console.log("Woof! Woof!");
}
}
// 创建子类实例
let myDog = new Dog("Buddy", "Golden Retriever");
// 调用继承的方法
myDog.makeSound(); // Generic animal sound
// 调用子类自己的方法
myDog.bark(); // Woof! Woof!
super 使用的几个注意问题:
- super 只能在派生类构造函数和静态方法中使用。
- 不能单独引用 super,要么用它调用构造函数,要么用它引用静态方法。
- 调用 super() 会调用父类构造函数,并将返回实例赋值给 this。
- super 行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入。
- 如果没有定义类构造函数,在实例化派生类会调用 super(),而且会传入所有传给派生类的参数。
- 在类构造函数中,不能在调用 super() 之前引用 this。
- 如果派生类中显示定义了构造函数,则要么必须在其中调用 super(),要么必须在其中返回一个对象。
小结
本章节内容真的很多,而且很重要,需要反复阅读。
未完待续...
参考资料
《JavaScript 高级程序设计》(第 4 版)