Javascript(三)之原型继承理解

进阶路线

image.png

3 原型继承

3.1 优秀文章

  1. 最详尽的 JS 原型与原型链终极详解 一
  2. 最详尽的 JS 原型与原型链终极详解 二
  3. 最详尽的 JS 原型与原型链终极详解 三
  4. 【THE LAST TIME】一文吃透所有JS原型相关知识点
  5. 代码复用模式

3.2知识点总结

以下讨论都以Person构造函数为例

function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() { alert(this.name) } 
}
var person1 = new Person('Zaxlct', 28, 'Software Engineer');
var person2 = new Person('Mick', 23, 'Doctor');

3.2.1 对象和函数

1、万物皆对象!但对象也是有区别的。分为普通对象和函数对象,Object 、Function 是 JS 自带的函数对象。怎么区分,其实很简单,凡是通过 new Function() 创建的对象都是函数对象,其他的都是普通对象
2、构造函数的实例都有(constructor)属性,该属性指向构造函数

person1.constructor == Person

3.2.2 原型对象--protoptye

在规范里,prototype 被定义为:给其它对象提供共享属性的对象。
prototype 自己也是对象,只是被用以承担某个职能罢了.

1、每当定义一个对象(函数也是对象)时候,对象中都会包含一些预定义的属性。其中每个函数对象都有一个prototype 属性,这个属性指向函数的原型对象。【只有函数对象才有 prototype 属性】
2、原型对象(Person.prototype)是构造函数的一个特殊实例,所以有一个constructor属性指向构造函数

Person.prototype.constructor == Person
person1.constructor == Person --对比细品

3、原型对象其实就是普通对象,但 Function.prototype 除外,它是函数对象

function Person(){};
 console.log(Person.prototype) //Person{}
 console.log(typeof Person.prototype) //Object
 console.log(typeof Function.prototype) // Function,这个特殊
 console.log(typeof Object.prototype) // Object
 console.log(typeof Function.prototype.prototype) //undefined

3.2.3 [__ proto __] 指向构造函数的原型对象

1、JS 在创建对象(不论是普通对象还是函数对象)的时候,都有一个叫做proto 的内置属性,用于指向创建它的构造函数的原型对象

person1.__proto__ == Person.prototype
image.png

根据上面的图片可以得到以下结论

Person.prototype.constructor == Person;
person1.__proto__ == Person.prototype;
person1.constructor == Person;

3.2.4 构造器

1、构造函数(Object)本身就是一个函数(就是上面说的函数对象),它和上面的构造函数 Person 差不多,可以创建对象的构造器不仅仅有 Object,也可以是 Array,Date,Function等

ar obj = new Object()
obj.constructor === Object
obj.__proto__ === Object.prototype

var b = new Array();
b.constructor === Array;
b.__proto__ === Array.prototype;

var c = new Date(); 
c.constructor === Date;
c.__proto__ === Date.prototype;

var d = new Function();
d.constructor === Function;
d.__proto__ === Function.prototype;

这些构造器都是函数对象


image.png

3.2.5 原型链

1、person1.__proto__ 是什么?
    答:person1.__proto__ == Person.prototype

2、Person.__proto__ 是什么?
    答:Person.__proto__ == Function.prototype
       因为 Person.__proto__ === Person的构造函数.prototype
       因为 Person的构造函数 === Function
       所以 Person.__proto__ === Function.prototype

3、Person.prototype.__proto__ 是什么?
     答:Person.prototype 是一个普通对象,我们无需关注它有哪些属性,只要记住它是一个普通对象。
        因为一个普通对象的构造函数 === Object
        所以 Person.prototype.__proto__ === Object.prototype

4、Object.__proto__ 是什么?
    答:Object.__proto__ == Function.prototype(原因同第二题)

5、Object.prototype__proto__ 是什么
    答:Object.prototype 对象也有proto属性,但它比较特殊,为 null 。因为 null 处于原型链的顶端,这个只能记住。
        Object.prototype.__proto__ === null

3.2.6 函数对象

1、所有函数对象的proto都指向Function.prototype,它是一个空函数(Empty function)

Number.__proto__ === Function.prototype  // true
Number.constructor == Function //true

Boolean.__proto__ === Function.prototype // true
Boolean.constructor == Function //true

String.__proto__ === Function.prototype  // true
String.constructor == Function //true

// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身
Object.__proto__ === Function.prototype  // true
Object.constructor == Function // true

// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身
Function.__proto__ === Function.prototype // true
Function.constructor == Function //true

Array.__proto__ === Function.prototype   // true
Array.constructor == Function //true

RegExp.__proto__ === Function.prototype  // true
RegExp.constructor == Function //true

Error.__proto__ === Function.prototype   // true
Error.constructor == Function //true

Date.__proto__ === Function.prototype    // true
Date.constructor == Function //true

2、所有的构造器都来自于 Function.prototype,甚至包括根构造器Object及Function自身。所有构造器都继承了·Function.prototype·的属性及方法。Function.prototype也是唯一一个typeof XXX.prototype为 function的prototype。其它的构造器的prototype都是一个对象

console.log(typeof Function.prototype) // function
console.log(typeof Object.prototype)   // object
console.log(typeof Number.prototype)   // object
console.log(typeof Boolean.prototype)  // object
console.log(typeof String.prototype)   // object
console.log(typeof Array.prototype)    // object
console.log(typeof RegExp.prototype)   // object
console.log(typeof Error.prototype)    // object
console.log(typeof Date.prototype)     // object
console.log(typeof Object.prototype)   // object

3、知道了所有构造器(含内置及自定义)的proto都是Function.prototype,那Function.prototype的proto是谁呢 Object.prototype的proto是谁?

Function.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null // true

4、原型链的形成是真正是靠proto 而非prototype

1)Object.setPropertyOf,给我两个对象,我把其中一个设置为另一个的原型。

2)Object.create,给我一个对象,它将作为我创建的新对象的原型。

3.2.7 问答

面试官:谈谈你对 JS 原型和原型链的理解?

候选人:JS 原型是指为其它对象提供共享属性访问的对象。在创建对象时,每个对象都包含一个隐式引用指向它的原型对象或者 null。

原型也是对象,因此它也有自己的原型。这样构成一个原型链。

面试官:原型链有什么作用?

候选人:在访问一个对象的属性时,实际上是在查询原型链。这个对象是原型链的第一个元素,先检查它是否包含属性名,如果包含则返回属性值,否则检查原型链上的第二个元素,以此类推。

面试官:那如何实现原型继承呢?

候选人:有两种方式。一种是通过 Object.create 或者 Object.setPrototypeOf 显式继承另一个对象,将它设置为原型。

另一种是通过 constructor 构造函数,在使用 new 关键字实例化时,会自动继承 constructor 的 prototype 对象,作为实例的原型。

在 ES2015 中提供了 class 的风格,背后跟 constructor 工作方式一样,写起来更内聚一些。

3.2.8 继承

继承的目的是代码复用,继承只是代码复用的一种手段或者实现方式

一个在构造函数上常用的规则是,用于复用的成员(译注:属性和方法)应该被添加到原型上。

3.2.8.1 类式继承1——默认模式

子类构造函数的原型对象指向父类构造函数的实例实现继承

// 定义继承函数
function inherit(C, P) {
    C.prototype = new P();
}
//parent构造函数
function Parent(name) {
    this.name = name || 'Adam';
}

//给原型增加方法
Parent.prototype.say = function () {
    return this.name;
};

//空的child构造函数
function Child(name) {}

//继承
inherit(Child, Parent);

var child1 = new Child("hh");
alert(child1 .hasOwnProperty('name')); // false

缺点

  • 由于子类通过其原型prototype对父类实例化,继承了父类,所以说父类中如果共有属性是引用类型,就会在子类中被所有的实例所共享,因此一个子类的实例更改子类原型从父类构造函数中继承的共有属性就会直接影响到其他的子类(概括为:子类的实例修改父类的属性,会影响其他子类的实例的属性
  • 由于子类实现的继承是靠其原型prototype对父类进行实例化实现的,因此在创建父类的时候,是无法向父类传递参数的。因而在实例化父类的时候也无法对父类构造函数内的属性进行初始化(概括:无法向子类传递参数
3.2.8.2 构造函数继承

下面这种模式解决了从子对象传递参数到父对象的问题。它借用了父对象的构造函数,将子对象绑定到this,同时传入参数.使用这种模式时,只能继承在父对象的构造函数中添加到this的属性,不能继承原型上的成员。使用借用构造函数的模式,子对象通过复制的方式继承父对象的成员,而不是像类式继承1中那样获得引用。下面的例子展示了这两者的不同:

function Child(a, c, b, d) {
    Parent.apply(this, arguments);
}

//父构造函数
function Article() {
    this.tags = ['js', 'css'];
}
//StaticPage通过借用构造函数的方式从Article继承
function StaticPage() {
    Article.call(this);
}
var page = new StaticPage();
alert(page.hasOwnProperty('tags')); // true

利用借用构造函数模式实现多继承

function Cat() {
    this.legs = 4;
    this.say = function () {
        return "meaowww";
    }
}

function Bird() {
    this.wings = 2;
    this.fly = true;
}

function CatWings() {
    Cat.apply(this);
    Bird.apply(this);
}

var jane = new CatWings();
console.dir(jane);

优缺点

  • 由于这种类型的继承没有涉及到原型prototype,所以父类的原型方法自然不会被子类继承,违背了代码复用的原则。
  • 这种模式的一个好处是获得了父对象自己成员的拷贝,不存在子对象意外改写父对象属性的风险。
3.2.8.3 组合式继承

综合以上两种模式,首先借用父对象的构造函数,然后将子对象的原型设置为父对象的一个新实例。这样做的好处是子对象获得了父对象自己的成员,也获得了父对象中可复用的(在原型中实现的)方法。子对象也可以传递任何参数给父构造函数。这种行为可能是最接近Java的,子对象继承了父对象的所有东西,同时可以安全地修改自己的属性而不用担心修改到父对象

function Child(a, c, b, d) {
    Parent.apply(this, arguments);
}
Child.prototype = new Parent();

缺点

  • 一个弊端是父构造函数被调用了两次,所以不是很高效。最后,(父对象)自己的属性(比如这个例子中的name)也被继承了两次。

测试

//父构造函数
function Parent(name) {
    this.name = name || 'Adam';
}

//在原型上添加方法
Parent.prototype.say = function () {
    return this.name;
};

//子构造函数
function Child(name) {
    Parent.apply(this, arguments);
}
Child.prototype = new Parent();

var kid = new Child("Patrick");
kid.name; // "Patrick"
kid.say(); // "Patrick"
delete kid.name;
kid.say(); // "Adam"
.3.2.8.4 原型式继承(共享原型)

F()函数是一个空函数,它充当了子对象和父对象的代理。F()的prototype属性指向父对象的原型。子对象的原型是一这个空函数的一个实例

function inherit(C, P) {
    var F = function () {};
    F.prototype = P.prototype;
    C.prototype = new F();
}

这种模式通常情况下都是一种很棒的选择,因为原型本来就是存放复用成员的地方。在这种模式中,父构造函数添加到this中的任何成员都不会被继承。
我们来创建一个子对象并且检查一下它的行为:

var kid = new Child();

如果你访问kid.name将得到undefined。在这个例子中,name是父对象自己的属性,而在继承的过程中我们并没有调用new Parent(),所以这个属性并没有被创建。当访问kid.say()时,它在3号对象中不可用,所以在原型链中查找,4号对象也没有,但是1号对象有,它在内在中的位置会被所有从Parent()创建的构造函数和子对象所共享。

存储父类(Superclass)
在上一种模式的基础上,还可以添加一个指向原始父对象的引用。这很像其它语言中访问超类(superclass)的情况,有时候很方便。

function inherit(C, P) {
    var F = function () {};
    F.prototype = P.prototype;
    C.prototype = new F();
    C.uber = P.prototype;
}

重置构造函数引用
这个近乎完美的模式上还需要做的最后一件事情就是重置构造函数(constructor)的指向,如果不重置构造函数的指向,那所有的子对象都会认为Parent()是它们的构造函数,而这个结果完全没有用

// parent, child, inheritance
function Parent() {}
function Child() {}
inherit(Child, Parent);

// testing the waters
var kid = new Child();
kid.constructor.name; // "Parent"
kid.constructor === Parent; // true

原型继承的最终实现

function inherit(C, P) {
    var F = function () {};
    F.prototype = P.prototype;
    C.prototype = new F();
    C.uber = P.prototype;
    C.prototype.constructor = C;
}
3.2.8.5 现代继承之原型继承

让我们从一个叫作“原型继承”的模式来讨论没有类的现代继承模式。在这种模式中,没有任何类进来,在这里,一个对象继承自另外一个对象。你可以这样理解它:你有一个想复用的对象,然后你想创建第二个对象,并且获得第一个对象的功能。下面是这种模式的用法

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}  
//需要继承的对象
var parent = {
    name: "Papa"
};

//新对象
var child = object(parent);

//测试
alert(child.name); // "Papa"

在ECMAScript 5中,原型继承已经正式成为语言的一部分。这种模式使用Object.create方法来实现。换句话说,你不再需要自己去写类似object()的函数,它是语言原生的了:

var child = Object.create(parent);

你可能感兴趣的:(Javascript(三)之原型继承理解)