原型和原型链详解

原型和原型链怎么来的?

1994年,网景公司(Netscape)发布了Navigator浏览器0.9版,但是刚开始的Js没有继承机制,更别提像同时期兴盛的C++和Java这样拥有面向对象的概念。在实际的开发过程中,构造函数内部的属性方法,每次new一个实例对象的时候,都会创建内部的这些属性和方法,并且不同的实例对象之间,不能共享这些方法,造成了资源的浪费。工程师们发现没有继承机制很难解决一些问题,必须有一种机制能将所有的对象关联起来。于是有了原型这个概念。

Brendan Eich鉴于以上情况,但不想把Js设计得过为复杂,于是引入了new关键字 和 constructor构造函数来简化对象的设计,引入了prototype函数对象来包含所有实例对象的构造函数的属性和方法,引入了proto和原型链的概念解决继承的问题

一、原型是什么

每个函数都有一个prototype(原型)属性,这个属性都有一个指针,指向一个对象,这个对象包含由特定类型所有实例共享的属性和方法,我这里就给他安置了一个辈分——对象的父亲。瞬间逼格就高了一级,哈哈哈哈...使用原型的好处是什么呢? 可以让所有对象实例共享原型包含的方法和属性。利用原型是当前构造函数创建的对象的父类,这个特点我们可以提取对象的公共属性和方法放在原型中,从程序的视角中具有封装性

1.1 原型的构成:原型的属性和方法+constructor

 

        function Car(color) {
            this.color = color
        }
        Car.prototype.name = "BMW";
        Car.prototype.setColor = function (color) {
            this.color = color
        }
        var car = new Car()

原型和原型链详解_第1张图片

从上面可以看到:car是构造函数创建的对象;

Car是构造函数;

Car.prototype是car对象的父亲,这也就不难理解对象身上没有name和setColor的属性和方法,car对象却能访问的缘故了。

二、如何使用原型

构造函数创建的实例可以访问构造函数的属性和方法,也可以访问原型的属性和方法。对于原型只能访问自己的属性和方法。考虑这一特性,如果对象公有的属性和方法,我们可以添加到原型上面,对象需要的时候直接访问原型的属性和方法就可以。例子如下:setAge的方法代表所有动物都设置自己的年龄的功能,所以我们放在原型上面,但是现实生活并不是这样的,这个例子举得不太合适了,嘻嘻...

function Animal(name) {
            this.name= "animal";
            this.age= 18;
            this.setName= function (name) {
                this.name = name
            }
        }
        Animal.prototype.setAge =function(age){
            this.age = age
        }
        var dog = new Animal("dog")
        var cat = new Animal("cat")

原型和原型链详解_第2张图片

原型和原型链详解_第3张图片

总结下:对象共性的属性和方法我们需要放在原型上

三、了解构造函数、原型、 对象之间的关系(重要)

原型和原型链详解_第4张图片

注:构造函数创建的对象调用的是__proto__,原型调用的是prototype

原型和原型链详解_第5张图片

原型和原型链详解_第6张图片

 

四、new关键字原理解析

对于对象的创建,new关键字处于举足轻重的地位,我们简单的阐述下。

Car.prototype={
            name : "BMW",
            width :"1400",
            health:100,
            run : function(){
                this.health--
            }
        }
        function Car (color){
            //var this ={
            //     car.__prote__ : Car.prototype;
            // }
            this.color =color
            // return this;
            
        }
        var car = new Car('red');
        var car1 = new Car('green');

 

17、 18行使用new关键字之后在构造函数中声明var this ={},然后car.__prote__放到this对象里面,最后把对象返回出来。注意如果人为恶意的修改返回值,返回值修改为其他对象的话会返回相应的对象,如果修改为基本数据类型的话不起作用。

注意:并不是所有的对象对会继承自object.protetype,特例Object.create(null);

至此原型告一段落。接下来我们看一下原型链

五、原型链

当对象访问属性和方法的时候,会往自身查找,如果没有才会去原型中找。(一级一级传递 形成了原型链)

原型链实现:让原型等于父类构造函数创建的对象

原型链的本质:子类构造函数的prototype__proto__指向父类构造器的prototype,建立对象之间的关联

我们通过下图和代码可以验证上面结论

原型和原型链详解_第7张图片

        function GrandFather(name) {
            this.name = name;
            this.age = 18;
            this.setName = function (name) {
                this.name = name
            }
        }
        GrandFather.prototype.setAge = function (age) {
            this.age = age
        }

        function Father() {

        }

        function Son() {

        }
        var grandFather = new GrandFather("grandFather")
        Father.prototype = grandFather;// equal to Object.setPrototypeOf(grandFather,  Father.prototype)
        var father = new Father();
        Son.prototype = father // equal to Object.setPrototypeOf(father, Son.prototype)
        var son = new Son();
                console.log(son.__proto__ === Son.prototype)
        console.log(Son.prototype.__proto__ === Father.prototype)
        console.log(Father.prototype.__proto__ === GrandFather.prototype)

image.png

缺点:这种方式不难发现继承了很多自己不需要的属性会导致效率、性能等问题。其次主要是一层套一层太繁琐

于是乎我们想到了call、apply可以改变this的指向,借助别人的方法实现自己的功能,但是它也有问题,不能使用其他构造函数的方法,这个时候我么就想到了Son的构造函数和GrandFather的构造函数指向同一个原型,但是这样又出现一个问题就是Son修改原型的数据会导致GrandFather的原型数据也会改变,因为他俩指向同一个内存地址,这是我们不想看到的,于是乎我们就找到了最完美的解决方案:圣杯模式

 

        function inherit(Target, Origin) {
            function F() { }
            F.prototype = Origin.prototype
            Target.prototype = new F()
            Target.prototype.constructor = Target
        }

圣杯模式的演变解决了以下问题:

1)虽然实现了继承但相互影响各自的数据

2)构造函数指向问题

原型和原型链详解_第8张图片

上面的方法赋值之后Origin.prototype和F.prototype指向同一个内存空间,Target.prototype和new F()指向同一个内存空间,修改Origin.prototype的时候F.prototype会改变,因为原型链的存在导致new F()的内存地址的内容会改变,进而Target.prototype改变。但是修改Target.prototype内存地址的属性只会导致new F()的内存地址的属性会改变,原型链是不可逆的,从而解决了可以随意修改子类的属性而不用担心父类的会改变

如果不考虑性能的话上面的圣杯模式实现是没问题的

原型和原型链详解_第9张图片

内容引自MDN:https://developer.mozilla.org/zhCN/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf

如果使用ES5继承的话建议使用Object.create()使用现有的对象来提供新创建的对象的__proto__

不了解Object.create()可移步:https://www.yuque.com/taowuhua/gfneg0/vfz5sf

        function GrandFather(name,age) {
            this.name = name;
            this.age = age;
           
        }
        GrandFather.prototype.setAge = function (age) {
            this.age = age
        }
                GrandFather.prototype.setName = function (name) {
            this.name = name
        }
        function Son() {

        }
        Son.prototype = Object.create(GrandFather.prototype);
                Son.prototype.constructor = Son;
        var son = new Son();
        console.log(son.setAge)
        console.log(son.name)

 

ES6中通过extends实现继承:直观,代码组织更加清晰

        class GrandFather {
            constructor(name, age) {
                this.name = name;
                this.age = age;
            }
            modifyName(name) {
                this.name = name;
            }
            eatFood() {

            }

        }



        class Son extends GrandFather{
            constructor(name,age,height){
                                //尽量把 super 写在第一行
                super(name, age);//等价于 GrandFather.call(this, name, age)
                this.height = height;
            }
            readBook(){

            }

        }
        var son = new Son("taowuhua",18,180)

 

原型和原型链详解_第10张图片

 

虽然son没有modifyName这个方法,但是通过extends还是继承了GrandFather的方法。这个还是很容易理解的。

注意:内建类的坑

class MyArray extends Array {
}

这种方式可以让开发者继承内建类的功能创造出符合自己想要的类。所有 Array 已有的属性和方法都会对继承类生效。这确实是个不错的诱惑,也是继承最大的吸引力。

但现实总是悲催的。extends 内建类会引发一些奇怪的问题,很多属性和方法没办法在继承类中正常工作。举个例子:

var a = new Array(1, 2, 3)
a.length  // 3
var b = new MyArray(1, 2, 3)
b.length  // 0

如果说语法糖可以用 Babel.js 这种 transpiler 去编译成 ES5 解决 ,扩充的 API 可以用 polyfill 解决,但是这种内建类的继承机制显然是需要浏览器支持的。而目前唯一支持这个特性的浏览器是………… Microsoft Edge 。

 

 

 

你可能感兴趣的:(前端)