1. 原型与原型链
1.1 什么是原型?原型是干嘛的?
原型是为其他对象提供共享属性(和方法)的对象。JS通过原型实现类的实例化和类的继承。除了undefined和null外,所有对象都有一个指向其原型的隐式指针属性"[[Prototype]]"(Chrome、IE11+、Safari等将此属性实现为 _ _ proto _ _;字面量数字、字符串等通过自动类型转换获得指向转换后对象原型的指针属性),而原型对象又可以有指向自己的原型的指针属性,这就形成了原型链。一个对象可以按照一定规则,顺着原型链寻找属性和方法,并将找到的属性和方法挪为己用。
1.2 原型与对象的构造
JS不以定义方式区分构造函数与普通函数,而是以函数调用方式区分。在一个函数调用前加一个new关键字,就构成构造函数调用。构造函数调用会产生一个新对象,这个新对象使用新开辟的内存。
JS函数对象除了拥有隐式指针属性[[Prototype]]外,还有一个显式属性prototype。protoype属性初始值具有两个属性:[[Prototype]]和constructor。[[Prototype]]就是prototype属性的隐式原型引用,而constructor是指向拥有prototype属性的函数的引用值(即函数对象本身)。
通过构造函数调用构建的对象的[[Prototype]]等于构造函数的prototype属性。在多次调用同一个构造函数创建多个对象的情形下,所有被创建的对象共享函数的prototype。其结构如图所示:
1.3 原型链与属性访问
一个对象的原型链以对象本身作为第一个节点,以null作为最后一个节点,以Object.prototype为倒数第二个节点。一个典型的原型链如下所示:
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
下面是一个属性访问和对象构造的例子
let f = function(){
this.a = 1;
this.b = 2;
}
let o1 = new f(); //o1/2由b创建,o1/2的[[Prototype]]指向f.prototype
let o2 = new f(); //两个对象共享f.prototype
o1.a = 11;//构造调用下,函数的this指向新构造对象。因此,a和b都是新对象自有的属性, 因此改变o1(2)的a属性不影响o2(1)的a属性
o2.a = 111;
//由于共享原型,为f.prototype添加属性相当于为每个oi都添加了属性
f.prototype.b = 3;
f.prototype.c = 4;
console.log(o1.a, o2.a, o1.b, o2.c); // 11, 111, 2, 4
//由于在对象本身就找到了b属性,就不再继续沿原型链找寻b了
f.prototype = {
"b":33,
"c":44
}
//虽然构造函数的prototype属性引用的对象变了,但是之前创造的对象的[[Prototype]]仍指向原来的原型对象,而原来的原型对象因为仍被引用,所以不会被销毁回收
let o3 = new f();
console.log(o1.b, o2.b, o1.c, o2.c, o3.c); //2, 2, 4, 4, 44
2. 类与继承的实现
2.1 类的实现
JS中类的实现方法有很多,最主流的方法是通过共享构造函数的原型方法、独享属性实现的。共享方法如下所示:
let f = function(a, b){
this.a = a;
this.b = b;
}
//所有实例共享方法,避免重复构造方法引起浪费
f.prototype.m = function(){
console.log(this.a + this.b);
}
// new关键字使f中的this指向新创建对象,属性a、b为自有
let o1 = new f(2,3);
let o2 = new f(5,6);
//方法被作为对象的方法调用,this指向对象,能够正确引用属性
o1.m(); // 5
o2.m(); // 11
函数,包括被用作构造函数的函数,也是另一个函数构造出来的。这个函数就是Function。Function本身也有prototype属性,也有隐性的[[Prototype]]属性(指向Object.prototype)。所以,函数(包括构造函数)的原型都为Function.prototype。而Function.prototype的原型是Object.prototype。这就形成下面的结构
2.2 继承的实现
JS的继承是通过原型链实现的。构造原型链也有很多种实现的方法。最被认可的一种方法如下所示:
function class1(a1){
//属性初始化;
this.a1 = a1 || undefined;
}
class1.prototype.m1 = function(){
//class1作为基类时,m1中this指向子类实例
console.log(this.a1);
}
function class2(a1, a2) {
//子类构造函数中调用基类构造函数,初始化基类属性。如果是多层继承,则链式调用。
class1.call(this, a1)
this.a2 = a2 || undefined;
}
class2.prototype = new class1 //class2.prototype指向无名class1实例,而此实例的__proto__指向class1.prototype
class2.prototype.constructor = class2 //相当于为无名class1实例添加属性
class2.prototype.m2 = function(){
console.log(this.a2);
}
let obj = new class2(1,2);
obj.m1(); //1
obj.m2(); //2
//这种方法将子类、基类属性都作为实例直接拥有的属性,将子类、基类方法放到原型链上;基类
console.log(obj.hasOwnProperty("a1")); // true
console.log(obj.hasOwnProperty("a2")); // true
console.log(obj.hasOwnProperty("m1")); // false
console.log(obj.hasOwnProperty("m2")); // false
obj的原型链如下:
可见原型链上有一些可能永远用不到的、被初始化为undefined是参数。为了避免这种场景,可以通过Object.create实现类的继承,方法如下:
var Person = function(name) {
this.name = name;
this.canTalk = true;
};
Person.prototype.greet = function() {
if (this.canTalk) {
console.log('Hi, I am ' + this.name);
}
};
var Employee = function(name, title) {
Person.call(this, name);
this.title = title;
};
Employee.prototype = Object.create(Person.prototype)
Employee.prototype.constructor = Employee
//必须先替换Employee.prototype,再添加属性。否则属性丢失,实例无法获得属性。
Employee.prototype.greet_1 = function () {
if (this.canTalk) {
console.log('Hi, I am ' + this.name + ', the ' + this.title);
}
};
function SalesMan(name,job,title) {
Employee.call(this, name, title);
this.job = job
};
SalesMan.prototype = Object.create(Employee.prototype);
SalesMan.prototype.constructor = SalesMan;
SalesMan.prototype.greet_2 = function () {
if (this.canTalk) {
console.log('Hi, I am ' + this.name + ', the ' + this.title + ', I ' + this.job);
}
};
var joe = new SalesMan('Joe','Sell things','Sales-Man');
console.log(joe);
joe.greet_2(); // Hi, I am Joe, the Sales-Man, I Sell things
joe.greet_1(); // Hi, I am Joe, the Sales-Man
joe.greet(); // Hi, I am Joe
对象直接拥有子类和基类的所有属性,对象的一切方法都在原型链上。