原型
定义
原型(prototype)是一个普通的对象,它为所有特定类型的实例共享了属性和方法,因此可以将这些信息直接添加到原型对象中而不必在构造函数中定义对象实例的信息,
理解
在这个例子中,我们先声明了一个函数类型的变量Person
- 在创建这个函数的时候,会根据特定规则为该函数创建一个
prototype属性(指针)指向函数的原型对象。 - 将type和maxAge属性直接添加到了Person的
prototype属性中 - 接着我们使用new调用构造函数Person创建了一个
实例对象p,使p可以共享原型对象中的属性和方法 - 该实例的内部将包含一个指针(内部属性),ECMA-262 第 5 版中管这个指针叫
[[Prototype]]。虽然在脚本中没有标准的方式访问[[Prototype]],但 Firefox、Safari 和 Chrome 在每个对象上都支持一个属性__proto__,实际上,它是来自于Object.prototype
通过比较可以得出如下结论:
- Person函数的
prototype属性和实例对象p的属性__proto__属性都指向了原型对象(这就是原型继承:构造函数的每个实例都可以访问构造函数的原型!🤯) - 原型对象默认添加的
constructor属性(也是共享的)又指回了关联的构造函数Person - 原型对象中除了包含
constructor属性之外,还包括后来添加的其他属性。
注:
length是函数对象自带的属性值,值除默认值参数(比如function(xx = 0))之外的其余必传参数数量;如何创建一个不带原型的对象呢?我们可以使用Object.create()方法创建一个新对象💪🏼:
let obj = {};
let jbo = Object.create(null, {
name: {
writable: true, // 可写
enumerable: true, // 可枚举
value: "yu"
}
});
console.dir(jbo) // [Object: null prototype] { name: 'yu' }
作用
举一个简单的栗子🌰
function Dog(name) {
this.name = name;
this.bark = function () {
console.log('Woof!');
};
}
var dog1 = new Dog('jack');
var dog2 = new Dog('jane');
dog1.bark(); // Woof!
dog1.bark(); // Woof!
console.log(dog1.bark === dog2.bark); // false
bark事件完全相同但是结果返回false说明每次new的时候创建了一个新的函数,每次都消耗内存,前面提到原型会为所有特定类型的实例提供共享的属性,因此我们可以把这个方法放到原型对象上,每次我们需要用的时候去调用就可以了:
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function () {
console.log('Woof!');
};
var dog1 = new Dog('jack');
var dog2 = new Dog('jane');
dog1.bark(); // Woof!
dog1.bark(); // Woof!
console.log(dog1.bark === dog2.bark); // true
ES6中的原型
ES6实际上为构造函数和原型使用了一种更简单的语法:
类。
上面的栗子🌰用es的写法:
class Dog {
constructor(name) {
this.name = name;
}
bark() {
console.log('Woof!');
}
}
var dog1 = new Dog('jack');
var dog2 = new Dog('jane');
dog1.bark(); // Woof!
dog1.bark(); // Woof!
console.log(dog1.bark === dog2.bark); // true
二者实际上仍然以相同的方式工作。类仅是构造函数的语法糖!
小结
原型对象实质上也是一个对象,原型对象上面也会有__proto__、constructor等属性,所以原型对象可以通过原型对象.__proto__来访问原型对象的原型对象,也可以通过constructor来知道自己是属于哪个构造函数上面的实例对象;
原型链
定义
许多OO语言都支持两种继承方式:接口继承、实现继承。由于函数没有签名,在ECMAScript中无法实现接口继承,只支持实现继承,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
举个栗子🌰:当我们访问name属性的时候,引擎首先会先查找实例对象dog1中是否有定义,找到则返回该属性,否则引擎将通过__proto__属性继续搜索原型对象中是否有该属性
如果原型对象中还没有,就会把当前得到的原型对象当作实例对象,继续通过它的__proto__属性去查找(原型的原型),向上追溯直到原型链末端__proto__为null为止。由相互关联的原型组成的链状结构就是原型链
注:当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性,可以通过
delete实例属性重新访问原型属性
function Dog() {}
Dog.prototype.name = 'jack';
var dog1 = new Dog();
dog1.name = 'jane';
console.log(dog1.name); // jane--来自实例
delete dog1.name;
console.log(dog1.name); // jack--来自原型
实现
原型链的基本思路就是让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。另一个原型又是另一个类型的实例,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
举例说明。
function Animal() {
this.type = 'animal';
}
Animal.prototype.getType = function () {
return this.type;
};
function Dog() {
this.name = 'dog';
}
Dog.prototype = new Animal();
Dog.prototype.bark = function () {
console.log('Woof!');
};
var dog1 = new Dog();
dog1.getType(); // animal
dog1.__proto__ === Dog.prototype; // true
Dog.prototype.__proto__ === Animal.prototype; // true
Animal.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
在这个🌰中Dog通过创建Animal的实例赋给Dog.prototype的方式来继承Animal
此时Dog.prototype可以获取到原本Animal实例中的属性和方法,也可以在自己的原型上添加新的方法bark。
dog1指向Dog的原型,Dog的原型又指向Animal的原型。
原型方法getType()方法在Animal.prototype中,Dog.prototype现在是Animal的实例,因此实例属性type位于该实例(Dog.prototype)中。此外,要注意dog1.constructor现在指向的是Animal
调用dog1.getType()会经历三个搜索步骤:
- 搜索实例;
- 搜索 Dog.prototype;
- 搜索 Animal.prototype
最后一步才会找到该方法。在找不到属性或方法的情况下,搜索过 程总是要一环一环地前行到原型链末端才会停下来。
注:我们所有内置方法的来源都是位于原型链上!😃: 例如
toString()、valueOf()方法。我们知道所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。🙌🏼
图解总结:
一句话,Dog继承了Animal,而 Animal继承了Object。当调用instance.toString()时,实际上调用的是保存在Object.prototype中的那个方法。
注:原型继承(除了原型继承还有组合继承和寄生组合继承,有兴趣的同学可以了解一下)意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,使子类的实例共享了父类构造函数的引用属性,如下所示:
Funcion和Object
综合上面可以看出,prototype对象也有__proto__属性,向上追溯一直到null。
var Dog = function () {};
var dog = new Dog();
console.log(dog instanceof Dog); //=> true
console.log(dog instanceof Object); //=> true
console.log(dog instanceof Function); //=> false
console.log(dog.__proto__ === Dog.prototype); //=> true
console.log(dog.__proto__.constructor === Dog); //=> true
console.log(Dog.__proto__ === Function.prototype); //=> true
console.log(Dog.__proto__.constructor === Function); //=> true
console.log(Function.__proto__ === Function.prototype); //=> true
console.log(Dog.__proto__ === Dog.prototype); //=> false
console.log(Dog.__proto__ === Function.prototype); //=> true
console.log(Function.__proto__.constructor === Function); //=> true
console.log(Function.__proto__.__proto__); //=> {}
console.log(Function.__proto__.__proto__ === dog.__proto__.__proto__); //=> true
console.log(dog.__proto__.__proto__.__proto__ === null); //=> true
console.log(dog.__proto__ === Dog.prototype); //=> true
console.log(dog.__proto__.__proto__ === Object.prototype); //=> true
console.log(dog.__proto__.__proto__ === Function); //=> false
小结
- 一切函数都是由 Function 这个函数创建的包括它本身,所『Function.prototype === Dog.
__proto__』,『Function.prototype === Function.__proto__』 - 一切函数的原型对象都是由 Object 这个函数创建的,所以『Object.prototype === Dog.prototype.
__proto__』 - Object 也是函数。所以Function创建了Object,所以『Function.prototype === Object.
__proto__』 ; - Function.prototype 是普通对象,普通对象是由Object创建的,所以 『Function.prototype.
__proto__=== Object.prototype』
相关方法
用原型链实现中的🌰来做说明:isPrototypeOf:表示调用对象是否在另一个对象的原型链上:Animal.prototype.isPrototypeOf(dog1)
hasOwnProperty:检测一个属性存在于实例还是原型中,给定属性存在于实例中才返回true:dog1.hasOwnProperty('name')
instanceof:测试是否实例的原型链中出现过的构造函数:dog1 instanceof Animal
getPrototypeOf:获取[[prototype]]的值:Object.getPrototypeOf(dog1) == Dog.prototype
in 操作符只要通过对象能够访问到属性就返回 true:"name" in dog1
以上示例均返回true
由于 in 操作符只要通过对象能够访问到属性就返回 true,hasOwnProperty()只在属性存在于实例中时才返回 true,因此只要 in 操作符返回 true 而 hasOwnProperty()返回 false,就可以确定属性是原型中的属性。
'name' in dog1 && !dog1.hasOwnProperty('name'); // false
'bark' in dog1 && !dog1.hasOwnProperty('bark'); // true
参考文章 📜
❤️ javaScript高级程序设计(第三版)
❤️ 对原型、原型链、 Function、Object 的理解
扩展 🏆
如果你觉得本文对你有帮助,可以查看我的其他文章❤️: