JavaScript筑基(六):原型与类

226 阅读5分钟

原型与类

简述原型

  • 原型:每一个 对象(null 除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性,其实就是 prototype 对象。 在对象中可以通过__proto__访问原型,在构造函数中可以通过prototype访问。二者指向相同。

  • 原型链:由相互关联的原型组成的链状结构就是原型链。

    • 当前实例使用了自己没有的变量或者属性,就会顺着原型链查找
    • instanceOf 的原理是通过查找原型链,如果目标对象在其原型链上,则返回 true,反之为 false
    • 原型链是 ES 中的类 class 出现前,实现继承的方式

如果能理解以下这张图,原型就没问题了

 function Foo() {}
 ​
 let f1 = new Foo();
 let f2 = new Foo();

image.png

最后要注意,

  • 所有构造器都是由 new Function 而来,包括 Function 本身,即构造器.__proto__ === Function.prototype

    至于为什么是本身,就是鸡生蛋蛋生鸡的故事了,可以理解为引擎在实现时就定义了这个规则。

  • ObjectFunction 的关系:基于上一点,既然 Object 是被 new Function 出来的,那么 Function 为什么又会在 Object 的原型链上呢。与上一点相同,可以理解为初始的定义。

    Function 是最顶层的构造器Object 是最顶层的对象从原型链讲Function 继承了 Object从构造器讲Funtion 构造了 Object

组合寄生式继承

一般来说,除了class之外有六种继承方式,在这里就不报菜名了。直接分析一下历史给出的较好的方案,组合寄生式继承

之后再将这个继承方式与ES6之后使用的类中的 extend / super 通过 babel 编译后进行对比

先来看看"菜名",组合+寄生=组合寄生式。那么它组合寄生分别表现在哪,为什么要这么命名?

组合

组合:使用原型链对原型的属性和方法继承,使用构造函数在新建子实例的时候传递参数。即这两种操作的组合

 function Parent(name) {
   this.name = name
 }
 ​
 // 使用构造函数在新建子实例的时候传递参数。这样可以继承Parent
 function Child(name, age) {
   Parent.call(this, name)   // 操作一
   this.age = age
 }
 // 直接将Child的原型换成Parent的一个实例。将Child加入Parent的原型链中
 // 可以使用原型上的方法和属性
 Child.prototype = new Parent()  // 操作二

寄生

组合中的操作二 Child.prototype = new Parent() 是可以通过寄生进行优化的。

为什么说是可以进行优化的。当 Parent 中的属性和操作很多的时候,对它进行调用,会产生一定的性能损耗(应该也没多少)

 function objectCopy(prototype) {
   function Fun() { };
   Fun.prototype = prototype;
   // 这里用空函数Fun创建实例,实现了实例obj.__proto__ === Parent.prototype 减少了性能开销
   return new Fun();
 }
 ​
 // 以下操作效果和 Child.prototype = new Parent() 几乎等价
 function inheritPrototype(child, parent) {
   const prototype = objectCopy(parent.prototype); // 创建原型(增强对象)
   prototype.constructor = child; // 完善原型属性
   Child.prototype = prototype;
 }
 ​
 // 在非上古浏览器中可以用一行这个替代上面全部的代码
 // Child.prototype = Object.create(parent.prototype, {
 //   constructor: { value: subClass },
 // })

组合寄生式继承

将上述两种拼在一起大概是这样的

 function objectCopy(obj) {
   function Fun() {}
   Fun.prototype = obj;
   return new Fun();
 }
 ​
 function inheritPrototype(child, parent) {
   const prototype = objectCopy(parent.prototype);
   prototype.constructor = child;
   Child.prototype = prototype;
 }
 ​
 // ------------组合------------
 function Parent(name) {
   this.name = name;
   this.friends = ["rose", "lily", "tom"];
 }
 ​
 Parent.prototype.sayName = function () {
   console.log(this.name);
 };
 ​
 function Child(name, age) {
   Parent.call(this, name);
   this.age = age;
 }
 ​
 // ------------寄生------------
 inheritPrototype(Child, Parent);
 ​
 // -------------测试样例-------------
 Child.prototype.sayAge = function () {
   console.log(this.age);
 };
 ​
 const child1 = new Child("yhd", 23);
 child1.sayAge();  // 23
 child1.sayName(); // yhd
 child1.friends.push("jack");
 console.log(child1.friends); // ["rose", "lily", "tom", "jack"]
 ​
 const child2 = new Child("yl", 22);
 child2.sayAge();  // 22
 child2.sayName(); // yl
 console.log(child2.friends); // ["rose", "lily", "tom"]

语法糖,本质是个函数。简化很多类似于设置原型链的操作,使书写对象更加清晰,内部工作机制就是原型操作。

基本对比

实例属性与方法
 // 类
 class User2 {
   constructor(name) {
     this.name = name;
   }
   // 会自动把方法放到原型中
   getName() {
     return this.name;
   }
 }
 ​
 // 直接原型操作
 function User1(name) {
   this.name = name;
 }
 User1.prototype.getName = function () {
   return this.name;
 };
 ​
 // ---------------测试用例---------------
 const stu1 = new User1("zhangsan");
 console.log(stu1.name);     // zhangsan
 console.log(stu1.getName());// zhangsan
 ​
 const stu2 = new User2("lisi");
 console.log(stu2.name);     // lisi
 console.log(stu2.getName());// lisi
静态属性和方法
 // 类
 class Web1 {
   static url = "baidu.com";
   static show() {
     console.log(`im ${Web1.url}`);
   }
 }
 ​
 // 原型
 function Web2() {}
 Web2.url = "niming.com";
 Web2.show = function () {
   console.log(`im ${Web2.url}`);
 };
 ​
 ​
 // -----------测试用例-----------
 console.log(Web1.url); // baidu.com
 Web1.show(); // im baidu.com
 ​
 console.log(Web2.url); // niming.com
 Web2.show(); // im niming.com

继承

类中的代码
 class Animal {
   constructor(name) {
     this.name = name;
   }
   run() {
     console.log(`${this.type}: ${this.name} is running`);
   }
 }
 ​
 class Dog extends Animal {
   constructor(name, age) {
     super(name);
     this.age = age;
     this.type = "dog";
   }
   getAge() {
     console.log(`my age is ${this.age}`);
   }
 }
 ​
 let dog1 = new Dog("旺财", 2);
 dog1.run();     // 旺财 is running
 dog1.getAge();  // my age is 2
babel compiler

将上面的代码用babel编译之后会是什么样的呢?

这里只看核心代码,为了简洁同时把一些 edgecase 删除了。有兴趣的可以自己到babel官网试试看 Babel · The compiler for next generation JavaScript (babeljs.io)

 // 寄生的实现
 function _inherits(subClass, superClass) {
   subClass.prototype = Object.create(superClass && superClass.prototype, {
     constructor: { value: subClass, writable: true, configurable: true },
   });
   if (superClass) Object.setPrototypeOf(subClass, superClass);
 }

这里做了两件事

  • 使得 subClass.prototype.__proto__ -> superClass.prototype ,同时配置了 constructor 属性。 因为 subClass.prototype 是由 Object.create 创建出来的,所以它是不包含任何属性的纯对象,只有一个__proto__ 指向父类的原型 这样,new subClass() 出来的实例函数,就能够访问父原型上的属性方法
  • 使得 subClass.__proto__ -> superClass 。这里跟后续的 super 实现有关。 同时实现了可以通过子类访问父类的方法和属性。

如果只看第一件事,那么是实现效果上和组合寄生式继承式一样的,不过 babel 实现的更加严谨

 // 组合的实现
 var Dog = /*#__PURE__*/ (function (_Animal) {
   _inherits(Dog, _Animal);  // 寄生
 ​
   var _super = _createSuper(Dog);   // 获取父类
 ​
   function Dog(name, age) {
     var _this;
 ​
     _classCallCheck(this, Dog);
 ​
     _this = _super.call(this, name);
     // _this = _Animal.call(this, name); //上一行可以简单的等价为这个
     _this.type = "dog";
     _this.age = age;
     return _this;
   }
 ​
   _createClass(Dog, [
     {
       key: "getAge",
       value: function getAge() {
         console.log("my age is ".concat(this.age));
       },
     },
   ]);
 ​
   return Dog;
 })(Animal);

这里的核心代码 _this = _super.call(this, name) 实现了组合

结论

到此我们简单的验证了一件事情,class 不过是一个语法糖,其实现完全可以通过对原型的操作进行还原。

参考资料

《JavaScript高级程序设计》(第四版)

《JavaScript忍者秘籍》(第二版)

JavaScript深入系列-冴羽

多图详解,一次性啃懂原型链