最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 终于懂了原型链!

    正文概述 掘金(SDYZ)   2021-03-16   545

    本着包教包会,不会再看几遍就会,忘了再再看几遍也能想来的原则,将 JS 原型部分涉及概念一次说清楚。后续关于 ES6 Class 的语法糖也将基于本篇文章继续更新!

    下面这段话来自 MDN,感觉描述的很全面,希望看完本文能够彻底理解下面这段话的含义!

    JavaScript 常被描述为一种基于原型的语言。每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。

    通过原型这种机制,JavaScript 中的对象从其他对象继承功能特性;这种继承机制与经典的面向对象编程语言的继承机制不同。

    准确地说,这些属性和方法定义在 Object 的构造器函数(constructor functions)之上的 prototype 属性上,而非对象实例本身。

    在传统的 OOP 中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制——而是在对象实例和它的构造器之间建立一个链接(它是 __proto__ 属性,是从构造函数的 prototype 属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。

    本文大纲共分为三部分:

    • 第一部分 理解 JS 原型链
    • 第二部分 new 操作符到底做了什么?实现一个 new !
    • 第三部分 JS 怎么实现继承的?原型链继承!

    下面进入主题~

    第一部分 理解 JS 原型链

    1. 构造函数、实例对象

    • person 是 Person 函数的实例对象
    • Person 函数 是 person 的构造函数
    function Person() {}
    let person = new Person();
    person; // Person {__proto__} 是一个对象,有一个__proto__属性
    person.constructor == Person; // true 指向Person构造函数本身
    

    constructor 属性其实就是一个拿来保存自己构造函数引用的属性。实例对象的 constructor 属性指向构造函数本身

    (其实这么说不准确。后面会说明其实是 person 实例对象通过 __proto__ 属性指向了构造函数的原型对象 prototype,prototype 的 constructor 属性才指向了构造函数 Person 本身。所以打印结果上来看 person 上本身是没有 constructor 的,但是却能打印 person.constructor)

    2. JS 内置对象 Object、Function

    1. Function 函数和 Object 函数是 JS 内置对象,也叫内部类。这些内置函数可以当作构造函数来使用。

    2. 为什么 “函数即对象”?关于 Function 对象的特殊性。

    function Person() {}
    Person.constructor == Function; // true 构造函数的 constructor 属性指向内置函数 Function
    
    • 所有函数都是 Function 函数的实例对象,所以常说函数即对象,JS 中一切皆对象。
    // 也都指向内置 Function
    Function.constructor == Function; // true
    Object.constructor == Function; // true
    
    • 说明 Function 函数同时是自己的构造函数
    • 说明 Function 函数同样也是 Object 这类内置对象的构造函数。
    let obj = {};
    obj.constructor == Object; // true 普通对象 constructor 属性都指向 Object 构造函数
    
    • 说明普通对象也都是内置 Object 函数的实例对象。

    整理一下 constructor 指向链:

    • person.constructor ——> Person 构造函数Person.constructor ——> Function 函数Function.constructor ——> Function 自己

    • obj.constructor ——> ObjectObject.constructor ——> Function 函数Function.constructor ——> Function 自己

    3. 原型对象 prototype

    1. 为了让实例对象能够共享一些属性和方法,将他们全部放在了 构造函数的原型对象 prototype 上。
    function Person() {}
    Person.prototype.sayHello = function () {
      console.log("Hello!");
    };
    let person1 = new Person();
    let person2 = new Person();
    
    person1.sayHello === person2.sayHello; // true
    
    1. constructor 也是一个共享属性,存放在原型对象 prototype 中,作用依然是指向自己的构造函数。
    function Person() {...}
    Person.prototype.constructor == Person // true
    

    从示意图中看出实例对象构造函数原型对象 prototypeconstructor 的存在关系。

    终于懂了原型链!

    实例对象的 constructor 属性指向该实例对象的构造函数,constructor 属性作为共享属性存在于构造函数的 prototype 上。

    疑问 ? :前文所说的 person.constructor 指向 Person 构造函数,那么 person 和 constructor 又是如何联系起来的呢?也就是如何让实例对象能找到自己的原型对象呢?↓↓↓

    4. __proto__ 属性

    1. 先说结论,这也是理解原型链的核心公式: person.__proto__ == Person.prototype。即实例对象的 __proto__ 属性指向构造函数的原型对象 prototype

    通过在 persion 对象内部创建了一个属性 __proto__ 直接指向自己的原型对象 prototype,通过原型对象就可以找到共享属性 constructor ,此时 constructor 指向该实例对象的构造函数 Person() 了。

    重新理解一些开篇的第一个例子,其实应该是这样的~~

    function Person() {}
    let person = new Person();
    // 请背下来!!
    person.__proto__ == Person.prototype;
    // constructor存在于prototype上,指向构造函数
    Person.prototype.constructor == Person;
    // 所以person.__proto__可以读取原型prototype上的constructor,指向构造函数
    person.__proto__.constructor == Person;
    
    // 那为什么开篇的等式也成立呢?
    person.constructor == Person; // true 也成立?
    // 这就是因为原型链查找了,后面会详细说。因为person通过原型链查找到了constructor,并且指向了Person构造函数。
    // but 注意
    Person.constructor == Function; // 因为Person的构造函数是Function
    
    1. 关于 __proto__ 的第二个重要概念是,由于原型对象 prototype 也是个对象,那它也有个__proto__属性,指向自己的原型对象。那它的构造函数是谁呢?

    Person.prototype.__proto__ == Object.prototype , 找到了!

    所以函数内的原型对象 prototype 跟所有普通对象一样,也都是内置 Object 函数的实例对象。

    总结:实例对象的 __proto__ 属性指向构造函数的原型对象 prototype; 而所有原型对象 prototype__proto__ 属性,都指向了 Object 的原型对象!

    有点绕了,但这正是我们理解原型链的核心原理!!

    下面这张图描述了很好地描述了 __proto__ 与 prototype 原型对象的关系:

    终于懂了原型链!

    5. 原型链来了

    有了上面的基础,接下来理解下原型链到底是什么?

    • 每个 实例对象 都有一个 私有属性 __proto__ 指向它的 构造函数原型对象 prototype。该原型对象也有自己的私有属性 __proto__指向原型对象...层层向上直到一个对象(内置Object对象)的原型对象为 null,并作为这个原型链中的最后一个环节。

    • 将一个个 实例对象原型对象 关联在一起,关联的原型对象也是别人的实例对象,所以就形成了串连的形式,也就形成了我们所说的原型链

    • Object 函数是所有对象通过原型链追溯到最根的构造函数。即 “最后一个 prototype 对象”便是 Object 函数内的 prototype 对象了。

    • Object 函数内的 prototype 对象的 __proto__ 指向 null。 为什么 Object 函数不能像 Function 函数一样让 __proto__ 属性指向自己的 prototype?答案就是如果指向自己的 prototype,那当找不到某一属性时沿着原型链寻找的时候就会进入死循环,所以必须指向 null,这个 null 其实就是个跳出条件。

    还是上面的那张图,通过 Person 构造函数可以找到四条原型链:!!

    第 1 条:persion.__proto__ --> Persion.prototype --> Persion.prototype.__proto__ --> Object.prototype --> Object.prototype.__proto__ --> null

    第 2 条:Persion.__proto__ --> Function.prototype --> Function.prototype.__proto__ --> Object.prototype --> Object.prototype.__proto__ --> null

    第 3 条:Function.__proto__ --> Function.prototype --> Function.prototype.__proto__ --> Object.prototype --> Object.prototype.__proto__ --> null

    第 4 条:Object.__proto__ --> Function.prototype --> Function.prototype.__proto__ --> Object.prototype --> Object.prototype.__proto__ --> null (“我中有你,你中有我”。。。)

    下面我们简单看一下实例对象 p 上都有什么

    function P(){};
    let p = new P(); p;
    // ↓↓↓
    > P {}
      > __proto__: Object // 实例对象p上有 __proto__ 属性,指向构造函数的原型对象prototype
        > constructor: ƒ P() // prototype 上有公共方法 constructor
        > __proto__: // prototype 上还有 __proto__ 属性,指向内置Object的原型对象
          > constructor: ƒ Object() // 内置Object的原型对象上的constructor 以及hasOwnProperty等公共方法
          > hasOwnProperty: ƒ hasOwnProperty()
          ...
    

    那么构造函数的属性和方法放在 this 上和放在 prototpe 上有什么区别?

    其实是都可以的,区别在于 this 定义的属性和方法是生成的实例自己独有的属性和方法;而定义在 prototype 上的属性和方法,是每个实例所共有的。那么定义在 prototype 上的属性和方法发生改变则每个实例对象都会拿到。

    举个例子:

    function Person() {
      this.age = 0;
      this.list = [];
    }
    let p1 = new Person();
    p1.list.push(1);
    p1.list; // [1]
    
    let p2 = new Person();
    p2.list.push(2);
    p2.list; // [2]
    

    定义在 prototype 上的属性和方法,是每个实例所共有的:

    function Person() {
      this.age = 0;
    }
    Person.prototype.list = [];
    
    let p1 = new Person();
    p1.list.push(1);
    p1.list; // [1]
    
    let p2 = new Person();
    p2.list.push(2);
    p2.list; // [1,2]
    

    前面 1~5 节总结一下!!划重点!!!

    1. 函数/对象上都有什么呢?
    • 对象都有 __proto__ 属性
    • 函数也有 __proto__ 属性,还有原型对象 prototype
    • 原型对象 prototype上也有 __proto__ 属性、还有 constructor 属性、以及一些共享属性和方法(挂在 prototype 上)
    1. __proto__ 是浏览器实现的查看原型方案,也写成 [[prototype]]__proto__ 属性的作用就是,当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象(父对象)里找,一直找,直到__proto__属性的终点 null,再往上找就相当于在 null 上取值,会报错。通过__proto__属性将对象连接起来的这条链路即我们所谓的原型链。

    2. constructor 属性其实就是一个拿来保存自己构造函数引用的属性。实例对象的 constructor 属性指向构造函数本身。

    3. 原型对象 prototype 的作用就是,让该函数所实例化的对象们都可以找到公用的属性和方法

    第二部分 new 操作符到底做了什么?

    new 操作符原理解析

    结合前面原理的分析,我们来看下 new 关键字都实现了哪些功能呢?

    function Person(age) {
      this.age = age;
    }
    let p = new Person(20);
    p; // Person {age:20}
    
    1. 首先创建了一个空对象 p = {}
    2. 然后将 p 的 __proto__ 属性指向其构造函数 Person 的原型对象 prototype
    3. 将构造函数内部的 this 绑定到新对象 p 上面,执行构造函数 Person()(其实和调动普通函数一样,并传值 this.age = 20
    4. 若构造函数返回的是非引用类型,则返回该新建的对象 p;否则返回引用类型的值。

    手写一个 new 的实现

    不会手写的原理,不是真的理解了的原理,下面我们开始手动实现~

    function Person(name, age) {
      this.name = name;
      this.age = age;
      // 构造函数本身也可能有返回结果
      // return {
      //   name,
      //   age
      // }
    }
    
    function _new(Func, ...rest) {
      // 1. 定义一个实例对象
      let p = {};
      // 2. 手动将实例中的__proto__属性指向相应原型对象
      // 此时 p.constructor 就指向了 Person函数,即 p 已经承认Person函数是它自己的构造函数
      p.__proto__ = Person.prototype;
      // 3. p 需要能够调用构造函数私有属性/方法
      // 也就是需要在实例对象的执行环境内调用构造函数,添加构造函数设置的私有属性/方法
      let res = Person.apply(p, arguments);
      // 4. 如果构造函数内返回的是对象,则直接返回原返回结果(和直接调用函数一样);否则返回新对象。
      return res instanceof Object ? res : obj;
    }
    
    let p = _new(Person, "张三", "20");
    p; // Person {name: "test"}
    p.constructor; // ƒ Person() {}
    

    上述第 3 步,其实构造函数是一种特殊的方法,主要用来在创建对象时初始化对象,即为对象成员变量赋初始值。 函数声明后函数体内的语句并不会立即执行,而是在真正调用时才执行。如果不调用的话,此时 this 没有指向,age 也是没有值的。通过 apply 方法完成了函数调用,并为自己的对象成员变量赋初始值,同时将 this 的指向绑定到实例对象 p 上面了。

    终于懂了原型链!

    第三部分 原型链继承

    ES6 的 Class 可以通过 extends 关键字实现继承,这比ES5 的通过修改原型链实现继承,实际上要清晰和方便很多。 但文本主要分析在 Class 出现之前,JS 是如何实现继承的。在后续系列文章中会针对 ES6 中的 Class 继承再进一步扩展。

    ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this))。

    1. 使用 call 方法实现继承

    function Father() {
      this.id = "1999";
    }
    function Son(age) {
      this.age = age;
      // 通过调用父函数的 call 方法来实现继承
      Father.call(this);
    }
    let p = new Son(20);
    p; // Son {age: 20, id: "1999"} 拥有了父级的私有属性
    p.id; // 1999
    

    说明: 实例化一个对象 p,实现了两步:Son 内 this 指向 p,Father this 指向 Son。所以 p 有了 age、id 属性。

    2. 原型链继承

    function Father(id) {
      this.id = id;
    }
    function Son(age) {
      this.age = age;
    }
    // Son函数改变自己的prototype指向
    // 实际上是Son.prototype.__proto__ 指向了 Father的实例,而 Father的实例指向Father的原型对象。
    // 即 Son.prototype.__proto__  == Father.prototype(改变了原型链流向), 看图。。。
    Son.prototype = new Father("1999");
    let p = new Son(20);
    
    p; // Son {age: 20} 和第一种方式不同,p上面虽没有父级的私有属性id,但是却能访问到
    p.id; // 1999
    p.__proto__; // Father {id: "1999"}
    
    p.hasOwnProperty("age"); // true
    p.hasOwnProperty("id"); // false
    
    终于懂了原型链!

    不知道你是否也有这样的疑惑,为什么不是写成 p.__proto__.id() 而是 p.id 就能获取到呢? 这就是原型链的查找过程了:

    1. 实例对象直接查找 p.id--> 发现没有该属性 --> 通过 __proto__ 属性去创建它的构造函数的 Son.prototype 对象上查找 --> Son.prototype 指向了 Father 的实例对象,有前文 new 方法原理可知,Father 实例上有私有属性 id, 所以 p.id 通过原型链查找到了 id 属性,而不用写成 p.__proto__.id() 的形式。

    2. 同理,实例对象直接查找 p.age--> 当实例对象没有某一属性 --> 通过 __proto__ 属性去创建它的构造函数的 prototype 对象上查找 --> prototype 对象上没有的话, prototype对象本身也有一个 __proto__ 属性指向它自己的原型对象(目前是内置 Object 对象),Object 对象的prototype上面有着构造函数留下的共享属性和方法。比如 hasOwnProperty()、valueof()等

    3. __proto__属性的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的 __proto__属性所指向的那个对象(父对象)里找,一直找,直到__proto__属性的终点 null,再往上找就相当于在 null 上取值,会报错。通过__proto__属性将对象连接起来的这条链路即我们所谓的原型链。

    知识点基本就到这里了!!

    总结

    本文从三部分分析了原型链的基本原理、new 关键字是如何实现的、以及 ES5 中的继承实现方式。

    那问题来了,ES6 做了哪些改变? Class 原理、extends 继承原理、语法糖怎么实现的呢,有了本文的基础,再理解后面的这些概念希望能够得心应手,真正理解 JS 这些核心的机制。

    参考

    继承与原型链

    对象原型

    用自己的方式(图)理解 constructor、prototype、proto和原型链 (本文图片均参考自这篇文章~)

    帮你彻底搞懂 JS 中的 prototype、proto与 constructor(图解)


    起源地下载网 » 终于懂了原型链!

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元