原型链的发展设计思路与常规应用方式初探

  • 历史

    • 背景

      • 1994年,网景公司(Netscape)发布了Navigator浏览器0.9版,这个版本的浏览器只能用来浏览,不具备与访问者互动的能力,网景公司急需一种网页脚本语言,使得浏览器可以与网页互动。
    • 简单设计

      • 工程师Brendan Eich负责开发这种新语言,他觉得,没必要设计得很复杂,这种语言只要能够完成一些简单操作就够了,比如判断用户有没有填写表单。
    • 万物皆对象

      • 1994年正是面向对象编程(object-oriented programming)最兴盛的时期,C++是当时最流行的语言,而Java语言的1.0版即将于第二年推出,Sun公司正在大肆造势。Brendan Eich无疑受到了影响,Javascript里面所有的数据类型都是对象(object)
    • 继承与不要类

      • 如果真的是一种简易的脚本语言,其实不需要有"继承"机制。但是,Javascript里面都是对象,必须有一种机制,将所有对象联系起来。所以,Brendan Eich最后还是设计了"继承"。
      • 考虑不引入类,因为一旦有了"类",Javascript就是一种完整的面向对象编程语言了,这好像有点太正式了,而且增加了初学者的入门难度 (在 ES2015/ES6 中引入了 class 关键字,但那只是语法糖,JavaScript 仍然是基于原型的)
    • 原型与构造函数

      • Brendan Eich把new命令引入了Javascript,用来从原型对象生成一个实例对象,C++和Java使用new命令时,都会调用"类"的构造函数(constructor)。他就做了一个简化的设计,在Javascript语言中,new命令后面跟的不是类,而是构造函数。
    • new运算符的缺点

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

      • 为了解决new的缺点,Brendan Eich决定为构造函数设置一个prototype属性。
      • 这个属性包含一个对象(以下简称"prototype对象"),所有实例对象需要共享的属性和方法,都放在这个对象里面;那些不需要共享的属性和方法,就放在构造函数里面。
      • 实例对象一旦创建,将自动引用prototype对象的属性和方法。也就是说,实例对象的属性和方法,分成两种,一种是本地的,另一种是引用的。
  • 原理

    • 继承与链式查找

      • 当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例
    • 构造函数、原型对象、实例

      • 每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针
  • 原理的边界理解

    • 原型做什么

      • 原型是为了共享多个对象之间的一些共有特性(属性或方法)
    • 最顶层的原型对象

      • 这个对象就是Object.prototype,这个对象中保存了最常用的方法,如toString、valueOf、hasOwnProperty等,因此我们才能在任何对象中使用这些方法
    • 构造函数

      • 函数的构造调用(注意,我们不把它叫做构造函数,因为JavaScript中同样没有构造函数的概念,所有的函数都是平等的,只不过用来创建对象时,函数的调用方式不同而已)
    • 属性遮蔽 (property shadowing)

      • 对象自身的属性会优先与原型链上的同名属性被访问到,比如:o.b, b是o的自身属性吗?是的,原型上也有一个'b'属性,但是它不会被访问到。
    • this指向

      • 当继承的函数被调用时,this 指向的是当前继承的对象,而不是继承的函数所在的原型对象。注意闭包与箭头函数
    • 创建非拷贝

      • JavaScript的对象创建不存在拷贝,对象的原型实际上也是一个对象,它和对象本身是完全独立的两个对象
    • 委托非继承

      • 继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。
  • 关键属性介绍

    • prototype

      • 每个构造函数都有一个prototype原型对象
      • prototype是函数才具有的属性
      • 被构造函数创建的实例对象的 [[Prototype]] 指向 func 的 prototype 属性。Object.prototype 属性表示 Object 的原型对象。
      • 默认情况下,new 一个函数创建出的对象,其原型都指向这个函数的prototype属性。
    • proto 与getPrototypeOf+setPrototypeOf

      • 每个对象都有一个__proto__属性,并且指向它的prototype原型对象
      • someObject.[[Prototype]] 符号是用于指向 someObject 的原型。从 ECMAScript 6 开始,[[Prototype]] 可以通过 Object.getPrototypeOf() 和 Object.setPrototypeOf() 访问器来访问。这个等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__。
      • __proto__属性指向问题

        • 字面量创建的对象

          • 创建: var a = {}
          • 指向: obj.__proto__ === Object.prototype; a.__proto__ === a.constructor.prototype
        • 构造器方式

          • 创建: var A = function (); var a = new A()
          • 指向: a.__proto__ === A.prototype; a.__proto__ === a.constructor.prototype
        • Object.create方式

          • 创建: var a1 = {} var a2 = Object.create(a1)
          • 指向: a2.__proto__ === a1; a.__proto__ !== a.constructor.prototype
    • prototype与__proto__

      • prototype是函数才具有的属性
      • __proto__是每个对象都具有的属性,但是不是规范属性,只有部分浏览器实现了,标准属性是someObject.[[Prototype]]
    • constructor

      • 原型对象上默认有一个属性constructor,该属性也是一个指针,指向其相关联的构造函数。
      • prototype原型对象里的constructor指向构造函数本身
    • hasOwnProperty 与 Object.keys()

      • JavaScript 中唯一2个处理属性并且不会遍历原型链的方法。
  • 边界情况

      1. 对于JavaScript中的内置对象,如String、Number、Array、Object、Function等,因为他们是native代码实现的,他们的原型打印出来都是ƒ () { [native code] }
      1. 内置对象本质上也是函数,所以可以通过他们创建对象,创建出的对象的原型指向对应内置对象的prototype属性,最顶层的原型对象依然指向Object.prototype。
      1. Object.create(null) 创建出的对象,不存在原型。 Object.create(null).__proto__ == undefined
  • 创建对象的方法

    • 1.字面量方式, var obj = {};

      • 当通过字面量方式创建对象时,它的原型就是Object.prototype。
      • 虽然我们无法直接访问内置属性[[Prototype]],但我们可以通过Object.getPrototypeOf()或对象的__proto__获取对象的原型。
      • Object.getPrototypeOf(obj) === Object.prototype;
      • obj.__proto__ === Object.prototype;
    • 2.函数的构造调用, var obj = new f();

      • javaScript在定义一个函数时,同时为这个函数定义了一个 默认的prototype属性,所有共享的属性或方法,都放到这个属性所指向的对象中.
      • 通过一个函数的构造调用创建的对象,它的原型就是这个函数的prototype指向的对象。
      • obj.__proto__ === f.prototype
    • 3.Object.create(), var obj2 = Object.create(obj);

      • 这个方法会以你传入的对象作为创建出来的对象的原型。
      • 这种方式还可以模拟对象的“继承”行为。
      • obj2.__proto__ === obj;
  • 应用

    • 实现继承

      • 构造函数的继承

        • 1、 构造函数绑定,使用call或apply方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行:Animal.apply(this, arguments);
        • 2、 prototype模式,"猫"的prototype对象,指向一个Animal的实例; 【注意!!!如果替换了prototype对象,那么,下一步必然是为新的prototype对象加上constructor属性,并将这个属性指回原来的构造函数。不然容易造成原型链紊乱】Cat.prototype = new Animal();  Cat.prototype.constructor = Cat;
        • 3、 改进prototype模式,让Cat()跳过 Animal(),直接继承Animal.prototype;

          • Cat.prototype = Animal.prototype; Cat.prototype.constructor = Cat;
          • 优点是效率比较高(不用执行和建立Animal的实例了),比较省内存。
          • 缺点是 Cat.prototype和Animal.prototype现在指向了同一个对象,那么任何对Cat.prototype的修改,都会反映到Animal.prototype。
        • 4、 利用空对象作为中介

          • var F = function(){}; F.prototype = Animal.prototype;
          • Cat.prototype = new F(); Cat.prototype.constructor = Cat;
          • F是空对象,所以几乎不占内存。这时,修改Cat的prototype对象,就不会影响到Animal的prototype对象。
        • 5、  拷贝继承,把父对象的所有属性和方法,拷贝进子对象

          • function extend2(Child, Parent) {

            • var p = Parent.prototype; var c = Child.prototype;
            • for (var i in p) { c[i] = p[i]; }
            • c.uber = p; }
      • 非构造函数的继承

        • object()方法,使用prototype链

          • function object(o) { function F() {} F.prototype = o; return new F(); }
          • 把子对象的prototype属性,指向父对象,从而使得子对象与父对象连在一起
        • 浅拷贝, 把父对象的属性,全部拷贝给子对象,也能实现继承
        • 深拷贝, 递归调用"浅拷贝"
    • 属性方法的全局作用域

      • vue.prototype.xxx
    • 如何体现原型的扩展性-插件机制 JQ / zepto

      • 思考

        • 为何要把原型方法放在$.fn?:扩展插件。(第一,构造函数的 prototype 肯定得指向能扩展的对象;第二,$.fn 肯定得指向能扩展的对象)
      • 好处:

        • 1,只有$会暴露在window全局变量
        • 2,将插件扩展统一到 $.fn.xxx 这一个接口,方便使用
    • instanceOf

      • 遍历原型链查找是否是某个原型对象的实例
    • this

      • this指向问题

        • 当继承的函数被调用时,this 指向的是当前继承的对象,而不是继承的函数所在的原型对象。注意闭包与箭头函数
      • class中的this指向
      • bind

        • 改变this指向,并返回一个函数
        • 通过原型链取到某些特殊的方法实现拼接参数

          • 思路:通过构造函数取到特殊方法实现拼接参数+apply
          • Array.prototype.slice.call(arguments);
      • call / apply
    • new // new运算符及背后工作原理

      • new后跟构造函数
      • 1,一个新对象被创建,它继承自func.prototype(构造函数的原型对象)
      • 2,构造函数func被执行,执行的时候,相应的传参会被传入,同时上下文(this)会被指定为这个新实例
      • 3,如果构造函数返回了一个“对象”,那么这个对象会取代整个new出来的结果,如果构造函数没有返回对象,那么new出来的结果为步骤1创建的对象
  • 发散思考

  • 参考链接

你可能感兴趣的:(javascript,前端,jquery,vue.js,node.js)