orangeyyy
2/28/2018 - 6:58 AM

原型&继承

概述

原型与继承是JS中最重要的概念,没有之一,也是比较难懂的知识点,本文的主要目的就是深挖这一知识点,希望能够全面透彻的理解这一概念。

对象

JavaScript中,万物皆对象!但对象也是有区别的。分为普通对象和函数对象,Object 、Function 是 JS 自带的函数对象。

通过几个简单的例子很容易看出:

var o1 = {}; 
var o2 =new Object();
var o3 = new f1();

function f1(){}; 
var f2 = function(){};
var f3 = new Function('str','console.log(str)');

console.log(typeof Object) // function
console.log(typeof Function) // function

console.log(typeof o1); // object
console.log(typeof o2); // object
console.log(typeof o3); // object

console.log(typeof f1); // function
console.log(typeof f2); // function
console.log(typeof f3); // function

构造函数

我们可以先来看一下,平时我们构建对象的方式:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

var person1 = new Person('hello', 10);
var person2 = new Person('world', 20);

通过上面我们知道,person1和person2都是Person的实例,Person是person1和person2的构造函数,js中没有类的概念(ES6中的class也只是语法糖),我们通常都是通过构造函数来创建实例,通常实例都会有一个constructor属性指向它对应的构造函数:

console.log(typeof Person); // function
console.log(typeof person1); // object
console.log(person1.constructor === Person); // true
console.log(person2.constructor === Person); // true

通常我们在新建对象时往往喜欢直接赋值,其实下面两种创建对象方式是等价的:

var obj = {};

var obj = new Object();

通过以上的结果,我们可以得出以下结论:

  • 构造函数都是function类型;
  • 对象实例大部分都是object类型(当构造函数为Function时除外);
  • 通过new关键词可以创建构造函数的实例;
  • 实例都会有一个constructor属性指向它的构造函数;

prototype

引用《JavaScript权威指南》的一段描述:

Every JavaScript object has a second JavaScript object (or null , but this is rare) associated with it. This second object is known as a prototype, and the first object inherits properties from the prototype.

JS中每一个函数对象都会包含一个prototype属性,这个属性指向函数的原型对象。我们可以看下面的例子:

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.otherInfo = {
    city: 'Hangzhou'
  };
}

var person1 = new Person('hello', 10);
var person2 = new Person('world', 20);
console.log(person1.otherInfo === person2.otherInfo); //false

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.otherInfo = {
  city: 'Hangzhou'
};
var person1 = new Person('hello', 10);
var person2 = new Person('world', 20);
console.log(person1.otherInfo === person2.otherInfo); //true

通过上面的例子可以看出,所有同一构造函数构造出的实例都继承自同一个原型对象。

我们还可以发现构造函数的原型对象也包含一个constructor属性,并且它指向构造函数本身:

console.log(Person.prototype.constructor === Person); // true

console.log(Person.prototype instanceof Person) // fasle
console.log(Person.prototype instanceof Object) // true
console.log(person1 instanceof Person) // true
console.log(person1.__proto__ === Person.prototype) // true
console.log(Person.prototype.__proto__ === Person.prototype) // false

通过第一个例子,我们可以认为一个构造函数的原型对象是这个构造函数的实例,但是看到后面两个例子,我们又开始迟疑了,我们去看一下instanceof的文档:

The instanceof operator tests whether the prototype property of a constructor appears anywhere in the prototype chain of an object.

可以看出instanceof是根据一个实例的构造函数的原型链中是否有指定的函数对象来判断的,通过上面的现象我可以得知Person的protoType 并不在Person.prototype的prototype链中。

如果我们给实例的从原型对象继承下来的属性赋值会发现如下现象:

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.otherInfo = {
  city: 'Hangzhou'
};
Person.prototype.gender = 'male';
var person1 = new Person('hello', 10);
var person2 = new Person('world', 20);

person1.gender = 'female';
console.log(person1.gender); //female
console.log(person2.gender); //male

person1.otherInfo.city = 'Beijing';
console.log(person1.otherInfo.city); //Beijing
console.log(person2.otherInfo.city); //Beijing

person1.otherInfo = {
  city: 'Shanghai'
};
console.log(person1.otherInfo.city); //Shanghai
console.log(person2.otherInfo.city); //Beijing

我们在查看person1和person2的对象结构:

person1 = {
  age: 10,
  gender:"female"
  name:"hello"
  otherInfo:{
    city: "Shanghai"
  }
  __proto__: {
    gender: 'male',
    otherInfo: {
      city: 'Beijing'
    }
    ...
  }
};

person2 = {
  age: 20,
  name: 'world',
  __proto__: {
    gender: 'male',
    otherInfo: {
      city: 'Beijing'
    }
    ...
  }
}

通过上面的输出结果和person1及person2的对象结构我们可以看出,如果我们直接对实例中继承自prototype的属性赋值,则会在当前实例中创建新的属性,而不会覆盖prototype中的属性,当然我们也可以通过实例直接修改prototype中对象属性中的值,这样将不会再实例中创建新的属性,当访问实例的属性时会一次往上查找实例的proto链,直到找到对应属性并返回,否则返回undefined。

我们也可以看看下面这个栗子:

function Person(name) {
  this.name = name;
}

Person.prototype.getName = function() {
  return this.name;
}

var person1 = new Person('hello');
console.log(person1.getName()); // hello

通过上面的结果可以看出无论是prototype属性中的this还是构造函数中的this最终都指向实例本身。

综合上面的内容,得到以下结论:

  • 每一个函数对象都包含一个prototype属性;
  • 每一个函数对象的prototype属性都包含一个constructor对象指向函数对象本身;
  • 每一个实例的proto属性都指向它的构造函数的prototype;
  • 大部分构造函数的prototype都是普通对象(Function除外);
  • 访问实例的属性会从当前实例开始一次往上查找proto链,直到找到对应属性并返回,否则返回undefiend;
  • 当对实例从构造函数prototype继承的属性直接赋值时,不会覆盖prototype中的属性,而是会在当前实例下创建一个新的同名属性,不会影响其他实例;
  • 如果实例中对构造函数prototype中对象属性的某个属性进行赋值,会更改prototype的内容同时影响其他实例;
  • constructor中的this和prototype中的this都指向实例本身;

proto

JS中所有对象都会有一个proto属性,JS在创建对象时(无论是普通对象或函数对象)都会有一个proto属性指向指向该实例的构造函数的原型对象。

因此我们可以看到以下例子:

function Person(name) {
  this.name = name;
}

var person1 = new Person('hello');

console.log(person1.__proto__ === Person.prototype) // true
console.log(Person.__proto__ === Function.prototype) // true

因此我们可以得到下面结论:

  • 所有构造函数的proto都指向Function.prototype;
  • 我们通常说的原型链是通过proto而不是通过prototype连接的;

其他

ES5 提供的新方法:Object.getOwnPropertyNames获取所有(包括不可枚举的属性)的属性名不包括 proto 中的属性,hasOwnProperty也是类似 :

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.otherInfo = {
  city: 'Hangzhou'
};
Person.prototype.gender = 'male';
var person1 = new Person('hello', 10);
Object.getOwnPropertyNames(person1) // ['name', 'age']
person1.hasOwnProperty('name') // true
person1.hasOwnProperty('gender') // false
``

## 总结
其实JS的原型链及继承看似比较难懂,但是只要静下心来认真思考,还是可以理清的,最后放一张在网上找到的图片。

![](https://github.com/mqyqingfeng/Blog/raw/master/Images/prototype5.png)
## 参考
* https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain
* https://www.jianshu.com/p/dee9f8b14771
* http://www.ruanyifeng.com/blog/2011/06/designing_ideas_of_inheritance_mechanism_in_javascript.html
* https://github.com/mqyqingfeng/Blog/issues/2