Fork me on GitHub

原型链和继承

https://kmknkk.oss-cn-beijing.aliyuncs.com/Blog-image/%E5%8E%9F%E5%9E%8B%E9%93%BE%E5%92%8C%E7%BB%A7%E6%89%BF/%E5%8E%9F%E5%9E%8B%E9%93%BE%E7%B1%BB.jpg

理解原型链

如上图,是一个原型链的一部分。我们来通过这个图解释一下原型链:(以下obj代指所有对象,Origin代指对象的原型)

  • 每个obj都有一个constructor,通过prototype指向它的Origin
  • constructor可以通过new操作符生成一个obj
  • 每个obj又都有一个___proto__指向它的Origin,obj是Origin的实例对象
  • 而它的Origin本质也是对象,所以Origin也有__proto__属性指向Origin的Origin
  • 层层向上,直到指向Object为止,而每个Origin又都有constructor实例对象(obj),这样就构成了一条原型链

原型链引申出来的几个问题:

Q1: 在原型链上Object再往上是什么?
A: 由于Object已经到顶了,所以Object.prototype.__proto__ === null

Q2: 哪种情况下proto和prototype的指向是同一个?
A: 由于function也是对象,所以有constructor,而constructor本质也是function,所以:

1
2
var func = function(){}
func.__proto__ === func.constructor.prototype // true

Q3: 判断Array有哪几种方法?
A: 除了Object.prototype.toString.call(o) === '[object Array]'这种万能方式以外,还有两种原型链方式:

1
2
a instanceOf Array      // (true/false) 判断a对象的原型链中是否存在构造函数Array
a.constructor === Array // (true/false) 直接判断a对象的构造函数是不是Array

但是这两种方法有个缺陷:如果这个a对象是从别的页面获取的,比如ifram,那么会造成原型链断裂现象,可能导致数组检测出的结果是Object而不是Array。

扩展:new操作符的原理

(1)创建一个新对象,其原型为构造函数的prototype
(2)将构造函数的作用域赋给新对象(于是this指向这个新对象)
(3)执行构造函数中的代码(为该对象添加属性)
(4)返回新对象给实例
我们可以试着模拟一下new运算符过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var _new(func) {
var o = Object.create(func.prototype);
/*
* 第一步也可以写成:
* var o = {};
* o.__proto__ === func.prototype
*/
var k = func.call(o);
if(typeof k === Object){
return k;
} else {
return o;
}
}

原型链继承

我们创建的每一个函数都有一个原型(prototype)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

使用原型对象的好处是我们不必在构造函数中定义对象实例的信息,而是将这些信息直接添加到原型对象当中。如下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* 定义被继承的对象 */
function Person() {

}

/* 往被继承对象中注入信息(实际上就是键值对) */
Person.prototype.name = "Sam";
Person.prototype.sayName = function() {
alert(this.name);
};

/* Person1继承Person */
function Person1() {
}
Person1.prototype = new Person();

/* Person1继承Person */
function Person2() {
}
Person2.prototype = new Person();

var person1 = new Person1();
var person2 = new Person2();

console.log(person1.name); // Sam
console.log(person2.name); // Sam
console.log(person1.sayName === person2.sayName); // true, 共享原型链上的方法并且地址相同

这种方法很方便但是缺点也很明显:但是由于 person1 和 person2 只是继承于 Person,所以他们自身并没有属性,他们的属性都是来自于向上查找原型链得到的。

也就是说:通过这种方法创建的实例,他们的的属性和方法是共享的。只要原型链上端的属性一改动,所有实例的属性都会改变。

组合继承

所以一般我们结合着构造函数方式一起使用,让这些实例拥有自己的”私人属性”。

这种方式集两种模式之长,每个实例都会有自己的一份构造函数中的实例属性副本,同时又共享着原型中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Person(name, age, job) {        //每个实例独有
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby","Court"];
}

Person.prototype.sayName = function() {
console.log(this.name);
}

/* Person1继承Person */
function Person1() {
let arg = arguments;

// 构造函数继承:继承属性和构造方法
Person.call(this, arg[0], arg[1], arg[2]);
}

// 原型链继承:继承原型链上的属性和方法(一般为继承方法)
Person1.prototype = new Person();

let showObj = new Person1('Sam', 18, 'coder');
console.log(showObj.name, showObj.age, showObj.job); // Sam, 18, coder
console.log(showObj.friends); // ["Shelby","Court"]

showObj.sayName(); // Sam

class继承

当然了,随着ES6的进(填)步(坑),JavaScript也可以用class来进行继承了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(name, age, job) {        //每个实例独有
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby","Court"];
}

Person.prototype.sayName = function() {
console.log(this.name);
}

// 两个字:简洁
class Person1 extends Person {
}

let showObj = new Person1('Sam', 18, 'coder');
console.log(showObj.name, showObj.age, showObj.job); // Sam, 18, coder
console.log(showObj.friends); // ["Shelby","Court"]

showObj.sayName(); // Sam
返回顶部