背景
1994年,网景公司(Netscape)发布了历史上第一个比较成熟的网络浏览器,但它只能浏览,不具备与用户互动的能力,浏览器无法对用户填写的内容进行校验,只能发送到服务器端进行判断,这不仅浪费服务器资源且耗时较长对用户体验不佳。设计者Brendan Eich创建了JavaScript这门语言,该语言设计的初衷是解决类似表单验证这种简单的操作。而关于"继承"的实现,他选择了基于原型的继承模型
。
new运算符
C++和Java使用new命令时,都会调用"类"的构造函数(constructor)。在Javascript语言中设计者做了简化的设计,new命令后面跟的不是类,而是构造函数。
js
function Animal(name) {
this.name = name;
// 定义一个公共属性
this.category = '动物类';
}
// 生成两个实例对象
const dog = new Animal('dog');
const cat = new Animal('cat');
dog.category = '犬类';
console.log(dog.category); // 犬类
console.log(cat.category); // 动物类
用构造函数生成实例对象,无法共享属性和方法。两个实例对象中的category属性是相互独立的,修改其中一个,另一个并不受影响。因此,为节省资源、实现数据共享,引入了prototype属性。
prototype
每个JavaScript对象(null除外)在创建时就会与之关联另一个对象,这个关联的对象就被称为该对象的原型。原型本身也是一个对象,它包含可以被原对象共享的属性和方法。
所有实例对象需要共享的属性和方法,都放在 prototype 对象中;而不需要共享的属性和方法,则放在构造函数中。
上面的例子, 使用 prototype 改写:
js
function Animal(name) {
this.name = name;
}
// 将公共属性定义在prototype对象中
Animal.prototype.category = '动物类';
const dog = new Animal('dog');
const cat = new Animal('cat');
// 更改公共属性
Animal.prototype.category='animal';
console.log(dog.category); // animal
console.log(cat.category); // animal
原型链
原型链是JavaScript中实现继承
和属性查找
的一种机制。
构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,实例都包含一个原型对象的内部指针。
那么,假设我们让原型对象等于另一个构造函数的实例,结果会怎样?显然,此时原型对象中将包含一个指向另一个原型的指针,另一个原型对象中也包含着一个指向另一个原型对象的指针,直至终点null,而构成实例与原型之间关系的这条链,就是所谓的 原型链
。
js
const a = { a: 1 };
// a ---> Object.prototype ---> null
const b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
const c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null
const d = Object.create(null);
// d ---> null(d 是一个直接以 null 为原型的对象)
原型搜索机制
:当你尝试访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript解释器就会去该对象的原型对象中查找。如果原型对象也没有这个属性或方法,解释器会继续沿着这个原型链向上查找,直到找到这个属性/方法,或者到达原型链的顶端 null
1、默认的原型
原型最常见的用途是实现继承。当你创建一个函数对象时,JavaScript解释器会为这个函数添加一个prototype
属性,指向一个原型对象。然后,当你通过这个构造函数来创建新对象时,这个新对象的内部原型(通常通过[[Prototype]]
属性访问)将会指向构造函数的 prototype
属性引用的那个对象。
js
var obj = {};
obj.toString(); // [object Object]
我们使用字面量的方式创建一个新的对象,为什么可以直接调用toString()、valueOf()等默认方法呢?
实际上,obj通过原型链的方式继承了object.prototype, 当调用 obj.toString()的时候其实是调用了保存在object.prototype中的方法。
默认原型都会包含一个内部指针, 指向 object.prototype。这就是所有自定义类型都会继承 toString()、valueOf()等默认方法的根本原因
2、原型和实例的关系
我们可以通过以下两种方法确定原型与实例之间的关系
instanceof
js
dog instanceof Object; // true
dog instanceof Animal; // true
我们可以说: dog 是 object 的实例, 也可以说 dog 是Animal 的实例。
isPrototypeof()
只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型
js
Object.prototype.isPrototypeOf(dog); //true
Animal.prototype.isPrototypeOf(dog); //true
3、给原型添加方法的顺序很重要!
给原型添加方法的代码一定要放在替换原型的语句之后
当你替换一个构造函数的原型对象时(例如,通过直接赋值一个新对象给构造函数的 prototype 属性),这个操作会切断原有原型对象与构造函数之间的链接,同时建立一个新的原型对象与构造函数之间的链接。如果在这个操作之前,你已经给原有的原型对象添加了一些方法,这些方法不会自动转移到新的原型对象上。所以这些方法对于通过新原型创建的实例来说是不可用的。
举个例子:
js
function Parent() {
this.parentValue = 1;
}
Parent.prototype.getParentValue = function() {
return this.parentValue;
}
function Child() {
this.childValue = 2;
}
// 给原型添加方法
Child.prototype.getChildValue = function() {
return this.childValue;
}
Child.prototype.getParentValue = function() {
return 3;
}
// 替换原型
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const instance = new Child();
console.log(instance.getParentValue()); // 1
console.log(instance.getChildValue()); // instance.getChildValue is not a function
上面例子,先添加方法后替换原型,导致我们在 Child 原型对象中添加的方法 getChildValue、getParentValue都不可用。遵循原型搜索机制,找到在Parent的原型对象中定义的getParentValue,返回结果1;而在原型链中并不存在getChildValue的定义,故报错。
js
function Parent() {
this.parentValue = 1;
}
Parent.prototype.getParentValue = function() {
return this.parentValue;
}
function Child() {
this.childValue = 2;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
// 添加新方法
Child.prototype.getChildValue = function() {
return this.childValue;
}
// 重写方法
Child.prototype.getParentValue = function() {
return 3;
}
const instance = new Child();
console.log(instance.getChildValue()); // 2
console.log(instance.getParentValue()); // 3
在这个例子中我们先替换了原型再添加方法,重写了getParentValue方法,Child.prototype中定义的方法可用并返回了结果。Parent.prototype中定义的getParentValue会被遮蔽。
通过原型链实现继承时, 不能使用对象字面量创建原型方法,因为这样做会重写原型链
js
function Parent() {
this.parentValue = 1;
}
Parent.prototype.getParentValue = function() {
return this.parentValue;
}
function Child() {
this.childValue = 2;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
// 使用字面量的方式添加新方法,会重写原型链,导致上面的继承无效
Child.prototype = {
getChildValue: function() {
return this.childValue;
}
}
const instance = new Child();
console.log(instance.getParentValue()); // instance.getParentValue is not a function
4、原型链继承存在的问题
js
function Parent() {
this.colors = ['red', 'green', 'blue'];
}
function Child() {}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const instance1 = new Child();
instance1.colors.push('orange');
console.log(instance1.colors); // ['red', 'green', 'blue', 'orange']
const instance2 = new Child();
console.log(instance2.colors); // ['red', 'green', 'blue', 'orange']
- 问题1:引用类型会被共享
- 问题2:没办法在不影响所有对象实例的情况下,给父类构造函数传递参数
性能
原型链上较深层的属性的查找时间可能会对性能产生负面影响;尝试访问不存在的属性始终会遍历整个原型链。某些情况下,我们可能只需要遍历实例自身的属性即可,此时,我们可以使用 hasOwnProperty 或 Object.hasOwn 方法进行判断。