面向对象编程很重要的一个方面,就是对象的继承。A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。
大部分面向对象的编程语言,都是通过"类"(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class,而是通过"原型对象"(prototype)实现。
原型链的顶端:Object.prototype
在JavaScript中,Object 是一个内置对象,它有一个原型(prototype)。Object 的原型可以被所有对象继承和共享。
Object 的原型包含许多通用的方法和属性,包括:
javascript
Object.prototype.toString()
Object.prototype.isPrototypeOf()
Object.prototype.__proto__
Object.prototype.hasOwnProperty()
Object.prototype.valueOf()
等等。
这些方法提供了对象操作和检查的基础,所有对象可以通过原型链继承这些基础操作。这一机制是在JavaScript中实现面向对象编程的基础。
下面是一个简单的例子,演示了 Object 的原型在原型链中的作用:
javascript
// 创建一个对象
const myObject = {};
// 调用 Object 原型的方法
console.log(myObject.toString()); // 输出: [object Object]
console.log(myObject.hasOwnProperty('someProperty')); // 输出: false
在这个例子中,myObject 对象继承了 Object 原型上的 toString 和 hasOwnProperty 方法。这些方法是通过原型链在 Object 的原型上定义的,所以所有对象都可以使用它们。
什么是原型链?它是如何工作的?
对象的原型属性(_ proto_)指向它的原型,它的原型对象也有自己的原型,这样的连续指向形成一条链,即为原型链。原型链机制使得对象可以共享和继承属性和方法。
工作原理
- 对象创建: 当创建一个对象时,该对象自动包含一个 _ proto_ 属性,指向其构造函数的原型对象。
javascript
function Animal() {}
const dog = new Animal();
- 原型对象关联: 每个构造函数都有一个 prototype 属性,它指向一个对象,即该构造函数的原型对象。
javascript
function Animal() {}
console.log(Animal.prototype); // 输出: Animal {}
- 连接构造函数和实例: 当通过构造函数创建实例时,实例的 _ proto_ 指向构造函数的 prototype。
javascript
function Animal() {}
const dog = new Animal();
console.log(dog.__proto__ === Animal.prototype); // 输出: true
在这个例子中,dog 对象的 _ proto_ 指向 Animal.prototype。
- 原型链的延续: 如果原型对象本身也有 _ proto_ ,则会形成链式关系。
javascript
function Animal() {}
function Mammal() {}
Mammal.prototype = new Animal();
const cat = new Mammal();
console.log(cat.__proto__.__proto__ === Animal.prototype); // 输出: true
在这个例子中,cat 的原型链延续到了 Animal.prototype。
实例对象、原型链、构造函数之间的关系:
原型链是如何工作的?
- 当访问一个对象的属性或方法时,JavaScript引擎会先查找该对象本身是否包含该属性或方法。
- 如果对象本身没有找到,引擎会沿着对象的原型链向上查找,直到找到该属性或方法或到达原型链的顶端(Object.prototype)为止。
- 如果一直找不到,返回 undefined。
这个链式查找路径,正是 instancOf 操作符的实现原理。
大致代码实现如下:
javascript
function myInstanceof(obj, ctor) {
let proto = Object.getPrototypeOf(obj);
let prototype = ctor.prototype;
while (true) {
if (!proto) return false;
if (proto === prototype) return true;
proto = Object.getPrototypeOf(proto);
}
}
创建对象时是如何应用原型链的
在 JavaScript 中创建对象的写法:
javascript
function Animal() {}
const dog = new Animal();
可以看出,问题"创建对象时是如何应用原型链的"其实可以转化成"new"操作符做了什么。
使用 new 命令时,它后面的函数依次执行下面的步骤。
- 创建一个空对象,作为将要返回的对象实例。
- 将这个空对象的原型,指向构造函数的 prototype 属性。
- 将这个空对象赋值给函数内部的 this 关键字。
- 开始执行构造函数内部的代码。
具体代码实现大致如下:
javascript
function objectFactory() {
// 将 arguments 对象转为数组
const args = [].slice.call(arguments);
// 取出构造函数
const constructor = args.shift();
// 创建一个空对象,继承构造函数的 prototype 属性
const context = Object.create(constructor.prototype);
// 执行构造函数
const result = constructor.apply(context, args);
// 如果返回结果是对象,就直接返回,否则返回 context 对象
const flag = result != null && typeof result === "object";
return flag ? result : context;
}
实现继承时对原型链的应用
组合继承
实现
- 使用原型链实现对原型属性和方法的继承。
- 使用构造函数实现对实例属性的继承。
下面是一个简单的组合继承的示例:
javascript
// 父类构造函数
function Animal(name) {
this.name = name;
}
// 父类原型方法
Animal.prototype.sayName = function() {
console.log(`I am ${this.name}`);
};
// 子类构造函数
function Dog(name, breed) {
// 借用构造函数继承实例属性
Animal.call(this, name);
this.breed = breed;
}
// 子类原型继承父类
Dog.prototype = new Animal();
// 子类自己的方法
Dog.prototype.bark = function() {
console.log('Woof!');
};
// 创建子类实例
const myDog = new Dog('Buddy', 'Golden Retriever');
// 调用父类和子类的方法
myDog.sayName(); // 输出: I am Buddy
myDog.bark(); // 输出: Woof!
优点和缺点
优点
- 结合了原型链继承和借用构造函数继承的优点: 可以继承原型链上的方法,同时也能够在子类构造函数中传递参数,避免了原型链继承的共享引用问题。
- 能够利用原型链实现方法的共享: 子类通过原型链继承父类的方法,避免了在每个实例上创建相同方法的重复消耗。
缺点
- 调用两次父类构造函数: 在创建子类实例时,会调用两次父类构造函数。一次是通过 Animal.call(this, name) 借用构造函数实现实例属性的继承,另一次是通过 Dog.prototype = new Animal() 继承原型属性和方法。这可能会导致一些性能上的浪费。
- 原型链上多余的属性: 在通过 Dog.prototype = new Animal() 继承原型属性时,实际上创建了一个父类实例,而这个实例上的属性可能是子类实例不需要的,存在一定的冗余。
尽管组合继承有一些缺点,但它仍然是JavaScript中实现继承的一种常见模式。在ES6之后,也出现了更多简化的继承方式,比如 class 关键字。
寄生组合继承
- 避免了组合继承中多次调用父类构造函数导致的属性重复定义问题。
- 具有父类构造函数的属性和方法,并且原型链上继承了父类原型的方法。
javascript
function prototype(child, parent) {
var prototype = Object.create(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
// 使用方式
prototype(Child, Parent);
原型链深度过深可能影响性能
因为在属性查找时需要逐级遍历整个原型链。为了解决这个问题,可以将对象扁平化,例如:
javascript
var entryObj = {
a: {
b: {
c: {
dd: "abcdd",
},
},
d: {
xx: "adxx",
},
e: "ae",
},
};
// 要求转换成如下对象
var outputObj = {
"a.b.c.dd": "abcdd",
"a.d.xx": "adxx",
"a.e": "ae",
};
function flat(obj, path = '', res = {}, isArray) {
for (let [key, value] of Object.entries(obj)) {
if (Array.isArray(value)) {
let _key = isArray ? `${path}[${key}]` : `${path}${key}`;
flat(value, _key, res, true);
} else if (typeof value === 'object') {
let _key = isArray ? `${path}[${key}].` : `${path}${key}.`;
flat(value, _key, res, false);
} else {
let _key = isArray ? `${path}[${key}]` : `${path}${key}`;
res[_key] = value;
}
}
return res;
}
console.log(flat({ a: { aa: [{ aa1: 1 }] } }));
有哪些继承模式可以替代原型链?
在现代JavaScript中,除了直接写原型链继承,还有一些其他的继承模式可以使用,这些模式可以更灵活地满足特定的需求。以下是一些常见的继承模式:
- 类继承(ES6+的Class语法): 使用ES6引入的Class语法,通过 class 关键字创建类,使用 extends 关键字实现继承。这种方式更直观、清晰,并且可以使用 super 调用父类方法。
javascript
class Animal {
constructor(name) {
this.name = name;
}
sayName() {
console.log(`I am ${this.name}`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
console.log('Woof!');
}
}
const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.sayName(); // 输出: I am Buddy
myDog.bark(); // 输出: Woof!
- Object.create(): 使用 Object.create() 方法创建对象,并将父对象作为参数传入,可以创建一个新对象,该对象的原型链指向父对象。
javascript
const animal = {
sayName() {
console.log(`I am ${this.name}`);
}
};
const dog = Object.create(animal);
dog.name = 'Buddy';
dog.breed = 'Golden Retriever';
dog.sayName(); // 输出: I am Buddy
- 工厂函数: 使用工厂函数创建对象,可以在函数内部定义私有变量和方法,并返回一个带有这些私有成员的新对象。
javascript
function createAnimal(name) {
const sayName = () => {
console.log(`I am ${name}`);
};
return {
sayName
};
}
function createDog(name, breed) {
const dog = createAnimal(name);
dog.breed = breed;
dog.bark = () => {
console.log('Woof!');
};
return dog;
}
const myDog = createDog('Buddy', 'Golden Retriever');
myDog.sayName(); // 输出: I am Buddy
myDog.bark(); // 输出: Woof!
这些继承模式在JavaScript中提供了更灵活、清晰和易于理解的方式,可以根据项目需求选择合适的模式。
参考资料
JavaScript 教程:wangdoc.com/javascript/...
ES6 入门教程:es6.ruanyifeng.com/
JavaScript深入之继承的多种方式和优缺点:github.com/mqyqingfeng...