结合巨人的思想,总结自己javaScript原型|原型链|继承 过目不忘

580 阅读19分钟

1.先了解它的历史和由来:

Javascript继承机制的设计思想

感谢@阮一峰老师,借阮总文章一用 如下:

我一直很难理解Javascript语言的继承机制。

它没有"子类"和"父类"的概念,也没有"类"(class)和"实例"(instance)的区分,全靠一种很奇特的"原型链"(prototype chain)模式,来实现继承。

我花了很多时间,学习这个部分,还做了很多笔记。但是都属于强行记忆,无法从根本上理解。 下面,我尝试用自己的语言,来解释它的设计思想。彻底说明白prototype对象到底是怎么回事。其实根本就没那么复杂,真相非常简单。

一、从古代说起

要理解Javascript的设计思想,必须从它的诞生说起。

1994年,网景公司(Netscape)发布了Navigator浏览器0.9版。这是历史上第一个比较成熟的网络浏览器,轰动一时。但是,这个版本的浏览器只能用来浏览,不具备与访问者互动的能力。比如,如果网页上有一栏"用户名"要求填写,浏览器就无法判断访问者是否真的填写了,只有让服务器端判断。如果没有填写,服务器端就返回错误,要求用户重新填写,这太浪费时间和服务器资源了。 因此,网景公司急需一种网页脚本语言,使得浏览器可以与网页互动。工程师Brendan Eich负责开发这种新语言。他觉得,没必要设计得很复杂,这种语言只要能够完成一些简单操作就够了,比如判断用户有没有填写表单。

994年正是面向对象编程(object-oriented programming)最兴盛的时期,C++是当时最流行的语言,而Java语言的1.0版即将于第二年推出,Sun公司正在大肆造势。

Brendan Eich无疑受到了影响,Javascript里面所有的数据类型都是对象(object),这一点与Java非常相似。但是,他随即就遇到了一个难题,到底要不要设计"继承"机制呢?

二、Brendan Eich的选择

如果真的是一种简易的脚本语言,其实不需要有"继承"机制。但是,Javascript里面都是对象,必须有一种机制,将所有对象联系起来。所以,Brendan Eich最后还是设计了"继承"。

但是,他不打算引入"类"(class)的概念,因为一旦有了"类",Javascript就是一种完整的面向对象编程语言了,这好像有点太正式了,而且增加了初学者的入门难度。

他考虑到,C++和Java语言都使用new命令,生成实例。

C++的写法是:

  ClassName *object = new ClassName(param);

Java的写法是:

  Foo foo = new Foo();

因此,他就把new命令引入了Javascript,用来从原型对象生成一个实例对象。但是,Javascript没有"类",怎么来表示原型对象呢?

这时,他想到C++和Java使用new命令时,都会调用"类"的构造函数(constructor)。他就做了一个简化的设计,在Javascript语言中,new命令后面跟的不是类,而是构造函数。

举例来说,现在有一个叫做DOG的构造函数,表示狗对象的原型。

  function DOG(name){

    this.name = name;

  }

对这个构造函数使用new,就会生成一个狗对象的实例。

  var dogA = new DOG('大毛');

  alert(dogA.name); // 大毛

注意构造函数中的this关键字,它就代表了新创建的实例对象。

三、new运算符的缺点

用构造函数生成实例对象,有一个缺点,那就是无法共享属性和方法。

比如,在DOG对象的构造函数中,设置一个实例对象的共有属性species。

  function DOG(name){

    this.name = name;

    this.species = '犬科';

  }

然后,生成两个实例对象:

  var dogA = new DOG('大毛');

  var dogB = new DOG('二毛');

这两个对象的species属性是独立的,修改其中一个,不会影响到另一个。

  dogA.species = '猫科';

  alert(dogB.species); // 显示"犬科",不受dogA的影响

每一个实例对象,都有自己的属性和方法的副本。这不仅无法做到数据共享,也是极大的资源浪费。

四、prototype属性的引入

考虑到这一点,Brendan Eich决定为构造函数设置一个prototype属性。

这个属性包含一个对象(以下简称"prototype对象"),所有实例对象需要共享的属性和方法,都放在这个对象里面;那些不需要共享的属性和方法,就放在构造函数里面。

实例对象一旦创建,将自动引用prototype对象的属性和方法。也就是说,实例对象的属性和方法,分成两种,一种是本地的,另一种是引用的。

还是以DOG构造函数为例,现在用prototype属性进行改写:

  function DOG(name){

    this.name = name;

  }

  DOG.prototype = { species : '犬科' };


  var dogA = new DOG('大毛');

  var dogB = new DOG('二毛');


  alert(dogA.species); // 犬科

  alert(dogB.species); // 犬科

现在,species属性放在prototype对象里,是两个实例对象共享的。只要修改了prototype对象,就会同时影响到两个实例对象。

  DOG.prototype.species = '猫科';

  alert(dogA.species); // 猫科

  alert(dogB.species); // 猫科

2.再来细说 原型和原型链

1 介绍 原型链的作用及工作原理。

2 内容 一切皆是对象;对象是由函数创造的;原型与原型链。 3 一切皆是对象 这句话是说我们在js中接触的数据大多都是对象,具有对象的特征。下面我们来展开说明一下,如何理解“一切皆对象”。

3.1 函数是对象 我们来审视一下我们日常定义的函数:

var f = function () {}
// 调用函数
f(); 
// 给函数添加属性
f.abc = 100; 
// 给函数添加属性
f.f1 = function(){}; 
console.dir(f)
上面的代码中,我故意把给f添加了两个属性abc,f1 ,代码在语法并没有任何错误。输出内容如下(使用chrome浏览器)

3.2 构造器是对象 来看看平时使用的内置构造器

// 定义数组:构造器方式
var arr = new Array();
// 定义数组,字面量方式
var arr = []
// 输出构造器
console.dir(Array)

4 对象是由函数创建的 在js中,当我们谈及一个函数时,其实我们并不是特别确定在说什么,因为函数的角色有两种。

•基础功能。作为一个封装功能,提炼代码的工具。 •类的功能。一个函数可以在前面加上 new 以当作构造器来使用。

下面来看一些常见的创建对象的代码

var arr = new Array();

var fn = new Function();

var obj = new Object();

上面的三行代码中,我们均采用new 构造器()的方式来创建对象,而构造器(Array,Function,Object等)本来就是函数。注意,在实际开发中我们更常用的会使用字面量的方式去创建它,这里只是做演示。

更一般的情况,我们看自定义的函数当构造器来使用的情况:

function F () {}

// 当作普通函数来使用 var rs = F();

// 当作构造器来使用 var f1 = new F(); 好的,如果你认可了: 一切皆是对象,对象是函数创建的 这两个命题,那么接下来,我们就要去开始一段漫长的寻找之旅:寻找创建这个对象的构造器。

4.1 寻找构造器 对于某一个对象obj来说,如何才能找到创建出它的那个构造器呢?公式如下:

对象.proto.constructor 理解如下:对象.proto 是取出对象的__proto__属性。我们知道对象是集合的无序集合,那么这些个众多的属性之中就有一个名为__proto__的属性。那我们再把对象.__proto__的值再看成是一个对象,再去找它的constructor属性。

以数组为例。(其实,我们已经知道了数组是由Array构造器创建的,这里先假装不知道)

来用这个公式找下它的构造器:(以下代码在chrome浏览器中执行)

console.log([].proto.constructor)// ƒ Array() { [native code] }

结果可以看到到,[].proto.constructor === Array

同样的道理,我们可以继续去找字符串,对象,函数,a标签的构造器。结果如下:

var str ="" console.log(str.proto.constructor === String);// true

var obj = {} console.log(obj.proto.constructor === Object);// true

var f = function () {} console.log(f.proto.constructor === Object); // true

假设你的页面上有一个a标签,其id就是a

var a = document.getElementById('a') a.proto.constructor === HTMLAnchorElement 5 原型与原型链 好了,我们终于要切入主题啦。

5.1 原型 prototype 前面我们说了,对象是属性的集合,而函数也是一个对象。所以,每个函数都有很多的属性,但其中就有一个名为prototype属性,这个属性值也是一个对象,它是一个非常重要的属性,我们称为原型。

下面去看看函数的原型长什么样:

(1) 自定义的函数

var f = function() {}

console.dir(f)

(2)Array构造器

console.dir(Array)

由于prototype的值也是一个对象,所以我们也称prototype是原型对象。

5.2 隐式原型 proto

对象是属性的集合,每个对象可能都有数量不等的属性,但每个对象都有一个名为proto的属性,这个属性值也是一个对象。这个属性proto称为隐式原型。(注意写法:前后两个下划线)

注意:由于函数既是函数也是对象,所以对于函数来说,它既有prototype属性又有__proto__属性。

5.3 原型与隐式原型的关系 先直接给出结论:

对象.proto === 构造器.prototype 用语言来表述是:对象的隐式原型等于 创建这个对象的构造器的原型 (把这句话读出来,念3遍)

用公式表达就是:

如果: 对象 = new 构造器()

那么: 对象.proto === 构造器.prototype

5.4 验证这个关系

数组

console.log([].proto === Array.prototype); // true

函数

var f = function() console.log(f.proto === Function.prototype); //true

普通对象

console.log({}.proto === Object.prototype); // true 由自定义构造器创建的对象

function f(){}

var f1 = new f()

console.log( f1.proto === f.prototype )

6 原型链 当我们访问某个对象的属性时,要沿着某个路径去找这个属性,这条路径就是原型链。

具体以访问obj的属性p为例

console.info( obj.p ) 寻找属性p的步骤如下:

(1)先在自有属性中找,如果找到,则返回;如果找不到,则进入到它的隐式原型中去找。

(2)由于对象的隐式原型也是一个对象(对象又是属性的集合),所以继续在obj.__proto__的自有属 性中去找。找到则返回,找不到,则继续。

(3)在obj.__proto__这个对象的隐式原型中继续找。找到返回,找不到,则继续到obj.proto.__proto__中去找。

(4)直到__proto__为null 停止。

6.1 例子:查找成功

var arr = [1,2]; //这是一个对象。

arr.toString()

把arr看成一个对象,arr.toString就是要去找toString属性。(1)第一步,在自有属性中找,没有找到 (2)第二步,进入arr.proto这个对象中去找。找到了,返回使用。

也就是说,

arr.toString === arr.proto.toString

6.2 寻找失败的例子

var arr = []; //这是一个对象。

arr.abc 把arr看成一个对象,arr.toString就是要去找toString属性。

(1)第一步,在自有属性中找,没有找到

(2)第二步,进入arr.__proto__这个对象中去找。没有找到。

(3)第三步,进入arr.proto.__proto__这个对象中去找。没有找到。

(4)而由于arr.proto.proto.proto 的值是null了,所以整个查找结束,最终返回undefined.

7 原型链的应用

原理:把属性(或者是方法)添加在构造器的原型对象上后,由此构造器创建的对象均可以使用这个属性(方法)了。

7.1 示例1:给内置构造器添加属性

例如:给数组Array构造器添的原型对象添加一个a1()方法。则所有的数组对象都将具备这个方法。

Array.prototype.a1 = function(){ console.log('a') }

var arr1 = new Array();

var arr2 = []; // 等价于使用构造器创建

arr1.a1(); // 输出 a

arr2.a1(); // 输出 a

以arr1.a1为例,按原型链的查找过程,先在自有属性中找,并没有找到,再进一步去arr.__proto__中找。

注意由于:

arr.proto === Array.prototype

而在Array.prototype中我们已经添加过a1属性了,所以arr.__proto__中也就可以找到这个属性了。故而,arr.a1()能成功执行。

同理可分析 arr2.a1的查找过程。

其实,由于a1,a2都是由构造器Array创建的对象,所以 有如下公式成立

arr1.proto === arr2.proto === Array.prototype

7.2 示例2:给自定义构造器添加属性

下面这段代码是一段经典的把函数当作用构造器来用,并以其模拟面对对象中的类的概念。

functioin F() {}

F.prototype.func = function() {}

var f1 = new F();

f1.func()

那请你思考 ,为什么f1这个对象可以访问func这个方法呢? 如果你能答出来,就说明你已经get到原型链的作用啦。

8 小结 •如果 对象 = new 构造器(),那么对象.proto === 构造器.prototype

•原型链的作用在于确定对象的属性值;

•在确定对象的属性值时,我们会沿着原型链上去查找。先在自有属性中找,如果找不到,继续到对象.proto中去找,如果还没有找到,则继续在对象.proto.proto中找,直到找到,或者 对象...proto 为null为止。此过程的搜索路径就是原型链。

总结:原型 原型链;函数 两个价值 1.实现具体 2.作为对象;函数 既是函数也是对象 既有 prototype 也有__proto__

//原型与隐式原型的关系

//每一个构造函数都有prototype原型属性,通过构造函数创建出来的对象都继承自该原型属性。

//对象.ptoto == 构造器.prototype

//对象的隐式原型等于 创建这个对象的构造器的原型

//经典题目
function Func() {
  this.name = 11;
} //既有 prototype 也有__proto__
//每個对象都有__proto__

Func.prototype.func = function() {
  // console.log('222');
};

var nfunc = new Func();
nfunc.func();
nfunc.name;

console.log(nfunc.__proto__ == Func.prototype);

//nfunc.__proto__ == Func.prototype
//Func.prototype__proto__ == Object.prototype;
//Object.prototype.__proto__ == null

function Munc(name, sex) {
  this.name = name;
  this.sex = sex;
}

Munc.prototype.hellword = function() {
  console.log(this.name, this.sex);
};

var m_func = new Munc("111", "man");
m_func.hellword();

加深知识点记忆 来一道经典题目 实现 一个new功能

///A.实现一个new  1.新建一个空对象  2 对象的__proto__ == 调用者的prototype 3.改变指向 apply
//备注:函数上的方法 是挂载在__proto__上的
//第一步 空位对象
//第二步 通过原型链  让 空对象的__proto__ == 传入对象的prototype 目的是空对象可以调用到传入对象的方法
//第三步 通过apply或call 改变this指向  目的是 让空对象可以访问传入对象的属性值
function _new() {
  var _object = {};

  let constructor = [].shift.apply(arguments); //arguments是类数组对象 需要转换 或者用Array.prototype
  _object.__proto__ = constructor.prototype;

  let resback = constructor.apply(_object, arguments);

  return resback;
}
//B 第二种 更简洁的  利用 Object.create 创建一个新对象
function _new() {
  var _object = {};
  let nModel = Array.prototype.shift.call(arguments);
  _object = Object.create(nModel);
  let res = nModel.apply(_object, arguments);
  return res;
}

3.了解完原型和原型链再来看继承

这里感谢 @浪里行舟 大佬的文章 JavaScript常见的六种继承方式

javaScript继承 ES5的 总来来说有两种:

一种原型继承; 一种借用构造函数改变this指向来继承

1.原型链继承

这种方式关键在于:子类型的原型为父类型的一个实例对象。

       //父类型
       function Person(name, age) {
           this.name = name,
           this.age = age,
           this.play = [1, 2, 3]
           this.setName = function () { }
       }
       Person.prototype.setAge = function () { }
       //子类型
       function Student(price) {
           this.price = price
           this.setScore = function () { }
       }
       Student.prototype = new Person() // 子类型的原型为父类型的一个实例对象
       var s1 = new Student(15000)
       var s2 = new Student(14000)
       console.log(s1,s2)

image.png 但这种方式实现的本质是通过将子类的原型指向了父类的实例,所以子类的实例就可以通过__proto__访问到 Student.prototype 也就是Person的实例,这样就可以访问到父类的私有方法,然后再通过__proto__指向父类的prototype就可以获得到父类原型上的方法。于是做到了将父类的私有、公有方法和属性都当做子类的公有属性;

子类继承父类的属性和方法是将父类的私有属性和公有方法都作为自己的公有属性和方法,我们都知道在操作基本数据类型的时候操作的是值,在操作引用数据类型的时候操作的是地址,如果说父类的私有属性中有引用类型的属性,那它被子类继承的时候会作为公有属性,这样子类1操作这个属性的时候,就会影响到子类2。

s1中play属性发生变化,与此同时,s2中play属性也会跟着变化。

另外注意一点的是,我们需要在子类中添加新的方法或者是重写父类的方法时候,切记一定要放到替换原型的语句之后

       function Person(name, age) {
           this.name = name,
           this.age = age
       }
       Person.prototype.setAge = function () {
           console.log("111")
       }
       function Student(price) {
           this.price = price
           this.setScore = function () { }
       }
       // Student.prototype.sayHello = function () { }//在这里写子类的原型方法和属性是无效的,
      //因为会改变原型的指向,所以应该放到重新指定之后
       Student.prototype = new Person()
       Student.prototype.sayHello = function () { }
       var s1 = new Student(15000)
       console.log(s1)

特点:

父类新增原型方法/原型属性,子类都能访问到 简单,易于实现

缺点:

无法实现多继承 来自原型对象的所有属性被所有实例共享 创建子类实例时,无法向父类构造函数传参 要想为子类新增属性和方法,必须要在Student.prototype = new Person() 之后执行,不能放到构造器中

总结:就一句话,自己(函数)没有的去prototype去找,prototype上的就是继承别人的;自己想公布出去的 就放到prototype上给别人去继承

构造函数继承

这种方式关键在于:在子类型构造函数中通用call()调用父类型构造函数

<script type="text/javascript">
  function Person(name, age) {
    this.name = name,
    this.age = age,
    this.setName = function () {}
  }
  Person.prototype.setAge = function () {}
  function Student(name, age, price) {
    Person.call(this, name, age)  // 相当于: this.Person(name, age)
    /*this.name = name
    this.age = age*/
    this.price = price
  }
  var s1 = new Student('Tom', 20, 15000)

image.png

这种方式只是实现部分的继承,如果父类的原型还有方法和属性,子类是拿不到这些方法和属性的。 console.log(s1.setAge())//Uncaught TypeError: s1.setAge is not a function 复制代码特点:

解决了原型链继承中子类实例共享父类引用属性的问题 创建子类实例时,可以向父类传递参数 可以实现多继承(call多个父类对象)

缺点:

实例并不是父类的实例,只是子类的实例

只能继承父类的实例属性和方法,不能继承原型属性和方法

无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

结合原型继承和构造函数的各自有点 完善继承

这种方式关键在于:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。

        function Person(name, age) {
            this.name = name,
            this.age = age,
            this.setAge = function () { }
        }
        Person.prototype.setAge = function () {
            console.log("111")
        }
        function Student(name, age, price) {
            Person.call(this,name,age)
            this.price = price
            this.setScore = function () { }
        }
        Student.prototype = new Person()
        Student.prototype.constructor = Student//组合继承也是需要修复构造函数指向的
        Student.prototype.sayHello = function () { }
        var s1 = new Student('Tom', 20, 15000)
        var s2 = new Student('Jack', 22, 14000)
        console.log(s1)
        console.log(s1.constructor) //Student
        console.log(p1.constructor) //Person

image.png 这种方式融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。不过也存在缺点就是无论在什么情况下,都会调用两次构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数的内部,子类型最终会包含父类型对象的全部实例属性,但我们不得不在调用子类构造函数时重写这些属性。

优点:

1.可以继承实例属性/方法,也可以继承原型属性/方法

2.不存在引用属性共享问题

3.可传参

4.函数可复用

缺点:

调用了两次父类构造函数,生成了两份实例

组合继承优化1

这种方式通过父类原型和子类原型指向同一对象,子类可以继承到父类的公有方法当做自己的公有方法,而且不会初始化两次实例方法/属性,避免的组合继承的缺点。

       function Person(name, age) {
            this.name = name,
                this.age = age,
                this.setAge = function () { }
        }
        Person.prototype.setAge = function () {
            console.log("111")
        }
        function Student(name, age, price) {
            Person.call(this, name, age)
            this.price = price
            this.setScore = function () { }
        }
        Student.prototype = Person.prototype
        Student.prototype.sayHello = function () { }
        var s1 = new Student('Tom', 20, 15000)
        console.log(s1)


但这种方式没办法辨别是对象是子类还是父类实例化

优点:

不会初始化两次实例方法/属性,避免的组合继承的缺点

缺点:

没办法辨别是实例是子类还是父类创造的,子类和父类的构造函数指向是同一个。

组合继承优化2

借助原型可以基于已有的对象来创建对象,var B = Object.create(A)以A对象为原型,生成了B对象。B继承了A的所有属性和方法。

       function Person(name, age) {
            this.name = name,
            this.age = age
        }
        Person.prototype.setAge = function () {
            console.log("111")
        }
        function Student(name, age, price) {
            Person.call(this, name, age)
            this.price = price
            this.setScore = function () {}
        }
        Student.prototype = Object.create(Person.prototype)//核心代码
        Student.prototype.constructor = Student//核心代码
        var s1 = new Student('Tom', 20, 15000)
        console.log(s1 instanceof Student, s1 instanceof Person) // true true
        console.log(s1.constructor) //Student
        console.log(s1)


同样的,Student继承了所有的Person原型对象的属性和方法。目前来说,最完美的继承方法!

4. 最后:ES6中class 的继承

ES6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

需要注意的是,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的。

       class Person {
            //调用类的构造方法
            constructor(name, age) {
                this.name = name
                this.age = age
            }
            //定义一般的方法
            showName() {
                console.log("调用父类的方法")
                console.log(this.name, this.age);
            }
        }
        let p1 = new  Person('kobe', 39)
        console.log(p1)
        //定义一个子类
        class Student extends Person {
            constructor(name, age, salary) {
                super(name, age)//通过super调用父类的构造方法
                this.salary = salary
            }
            showName() {//在子类自身定义方法
                console.log("调用子类的方法")
                console.log(this.name, this.age, this.salary);
            }
        }
        let s1 = new Student('wade', 38, 1000000000)
        console.log(s1)
        s1.showName()


优点: 语法简单易懂,操作更方便

缺点:

并不是所有的浏览器都支持class关键字 微信图片_20210625145221.jpg