JavaScript Object 对象 再解(四)

早期的 JavaScript 程序员一般都有过使用 JavaScript“模拟面向对象”的经历。 惨啊(😂😂😂),prototype 是好的,就是语法别扭。

本文内容基本上还是 winter 老师 重学前端里面的内容,重学前端、前端进阶训练营真是非常值得的内容,看到此文的前端工程师真的推荐买买买,看完以后真的能够对前端的理解更上一层楼。(来个广告 23333)

因为一些公司的政治原因,JavaScript 推出之时,管理层就要求它去模仿 Java。JavaScript 创始人 Brendan Eich又不喜欢 Java,用模仿着 Java 的语法搞出了 JavaScript。在“原型运行时”的基础上引入了 new、this 等 语言特性,使之“看起来语法更像 Java”。
但是 JavaScript 这样的半吊子模拟,缺少了继承等关键特性,导致大家试图对它进行修 补,进而产生了种种互不相容的解决方案。(JavaScript 是 Brendan Eich 十多天搞出来的,bug 也多,缺少了继承等关键特性,导致大家试图对它进行修补,进而产生了种种互不相容的解决方案。不过也就是因为JavaScript是动态语言、弱类型的语言,早就了当今前端百花齐放的格局)
从 ES6 开始,JavaScript 提供了 class 关键字来定义类,尽管,这样的方案仍 然是基于原型运行时系统的模拟,但是它修正了之前的一些常见的“坑”,统一了社区的 方案,这对语言的发展有着非常大的好处。

JavaScript 的原型

  • 如果所有对象都有私有字段 [[prototype]],就是对象的原型;
  • 读一个属性,如果对象本身没有,则会继续访问对象的原型,直到原型为空或者找到为止。

这个模型在 ES 的各个历史版本中并没有很大改变,但从 ES6 以来,JavaScript 提供了一 系列内置函数,以便更为直接地访问操纵原型。三个方法分别为:

  • Object.create() 根据指定的原型创建新对象,
  • Object.getPrototypeOf() 获取一个对象的原型
  • Object.setPrototypeOf() 设置一个对象的原型

利用这三个方法,我们可以完全抛开类的思维,利用原型来实现抽象和复用。我用下面的 代码展示了用原型来抽象猫和虎的例子。

var cat = {
  say() {
    console.log('meow~')
  },
  jump() {
    console.log('jump')
  }
}
// 老虎是猫科嘛
var tiger = Object.create(cat, {
  say: {
    writable: true,
    configurable: true,
    enumerable: true,
    value: function() {
      console.log("roar!");
    }
  }
})

var anotherCat = Object.create(cat);
anotherCat.say();
var anotherTiger = Object.create(tiger);
anotherTiger.say();

这段代码创建了一个“猫”对象,又根据猫做了一些修改创建了虎,之后我们完全可以用 Object.create 来创建另外的猫和虎对象,我们可以通过“原始猫对象”和“原始虎对 象”来控制所有猫和虎的行为。

但是,在更早的版本中,程序员只能通过 Java 风格的类接口来操纵原型运行时,可以说非 常别扭。

考虑到 new 和 prototype 属性等基础设施今天仍然有效,而且被很多代码使用,学习这 些知识也有助于我们理解运行时的原型工作原理,下面我们试着回到过去,追溯一下早年 的 JavaScript 中的原型和类。

var o = new Object;
var n = new Number;
var s = new String;
var b = new Boolean;
var d = new Date;
var arg = function(){ return arguments }();
var r = new RegExp;
var f = new Function;
var arr = new Array;
var e = new Error;
console.log([o, n, s, b, d, arg, r, f, arr, e].map(v => Object.prototype.toString.call(v))); 

因此,在 ES3 和之前的版本,JS 中类的概念是相当弱的,它仅仅是运行时的一个字符串属性。

在 ES5 开始,[[class]] 私有属性被 Symbol.toStringTag 代替,Object.prototype.toString 的意义从命名上不再跟 class 相关。我们甚至可以自定义 Object.prototype.toString 的行为,以下代码展示了使用 Symbol.toStringTag 来自定义 Object.prototype.toString 的行为:

var o = { [Symbol.toStringTag]: "MyObject" }
console.log(o + "");

这里创建了一个新对象,并且给它唯一的一个属性 Symbol.toStringTag,我们用字符串加法触发了 Object.prototype.toString 的调用,发现这个属性最终对 Object.prototype.toString 的结果产生了影响。但是,考虑到 JavaScript 语法中跟 Java 相似的部分,我们对类的讨论不能用“new 运算是针对构造器对象,而不是类”来试图回避。所以,我们仍然要把 new 理解成 JavaScript 面向对象的一部分,下面我就来讲一下 new 操作具体做了哪些事情。new 运算接受一个构造器和一组调用参数,实际上做了几件事:

以构造器的 prototype 属性(注意与私有字段[[prototype]]的区分)为原型,创建新对象;将 this 和调用参数传给构造器,执行;如果构造器返回的是对象,则返回,否则返回第一步创建的对象。new 这样的行为,试图让函数对象在语法上跟类变得相似,但是,它客观上提供了两种方式,一是在构造器中添加属性,二是在构造器的 prototype 属性上添加属性。

function c1(){
    this.p1 = 1;
    this.p2 = function(){
        console.log(this.p1);
    }
} 
var o1 = new c1;
o1.p2();



function c2(){
}
c2.prototype.p1 = 1;
c2.prototype.p2 = function(){
    console.log(this.p1);
}

var o2 = new c2;
o2.p2();

第一种方法是直接在构造器中修改 this,给 this 添加属性。第二种方法是修改构造器的 prototype 属性指向的对象,它是从这个构造器构造出来的所有对象的原型。

没有 Object.create、Object.setPrototypeOf 的早期版本中,new 运算是唯一一个可以指定[[prototype]]的方法(当时的 mozilla 提供了私有属性 proto,但是多数环境并不支持),所以,当时已经有人试图用它来代替后来的 Object.create,我们甚至可以用它来实现一个 Object.create 的不完整的 polyfill,见以下代码:

Object.create = function(prototype){
    var cls = function(){}
    cls.prototype = prototype;
    return new cls;
}

这段代码创建了一个空函数作为类,并把传入的原型挂在了它的 prototype,最后创建了一个它的实例,根据 new 的行为,这将产生一个以传入的第一个参数为原型的对象。这个函数无法做到与原生的 Object.create 一致,一个是不支持第二个参数,另一个是不支持 null 作为原型,所以放到今天意义已经不大了。

ES6 中的类

ES6 实现的 class 本质上依然是语法糖,说白了用的还是原型

好在 ES6 中加入了新特性 class,new 跟 function 搭配的怪异行为终于可以退休了(虽然运行时没有改变),在任何场景,我都推荐使用 ES6 的语法来定义类,而令 function 回归原本的函数语义。下面我们就来看一下 ES6 中的类。

ES6 中引入了 class 关键字,并且在标准中删除了所有[[class]]相关的私有属性描述,类的概念正式从属性升级成语言的基础设施,从此,基于类的编程方式成为了 JavaScript 的官方编程范式。

我们先看下类的基本写法:

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
  // Getter
  get area() {
    return this.calcArea();
  }
  // Method
  calcArea() {
    return this.height * this.width;
  }
}

在现有的类语法中,getter/setter 和 method 是兼容性最好的。

我们通过 get/set 关键字来创建 getter,通过括号和大括号来创建方法,数据型成员最好写在构造器里面。

类的写法实际上也是由原型运行时来承载的,逻辑上 JavaScript 认为每个类是有共同原型的一组对象,类中定义的方法和属性则会被写在原型对象之上。

此外,最重要的是,类提供了继承能力。我们来看一下下面的代码。

class Animal { 
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name); // call the super class constructor and pass in the name parameter
  }

  speak() {
    console.log(this.name + ' barks.');
  }
}

let d = new Dog('Mitzie');
d.speak(); // Mitzie barks.

以上代码创造了 Animal 类,并且通过 extends 关键字让 Dog 继承了它,展示了最终调用子类的 speak 方法获取了父类的 name。

比起早期的原型模拟方式,使用 extends 关键字自动设置了 constructor,并且会自动调用父类的构造函数,这是一种更少坑的设计。

所以当我们使用类的思想来设计代码时,应该尽量使用 class 来声明类,而不是用旧语法,拿函数来模拟对象。

一些激进的观点认为,class 关键字和箭头运算符可以完全替代旧的 function 关键字,它更明确地区分了定义函数和定义类两种意图,我认为这是有一定道理的。

**() => **写起来简洁啊

总结

在新的 ES 版本中,我们不再需要模拟类了:我们有了光明正大的新语法。而原型体系同时作为一种编程范式和运行时机制存在。

以后都class 了,用起来真的方便,看看 react,但是 class 就是烦那些constructor,this 的指向性问题。参考 react

我们可以自由选择原型或者类作为代码的抽象风格,但是无论我们选择哪种,理解运行时的原型系统都是很有必要的一件事。

当然是 类 了,语法糖真甜

在你的工作中,是使用 class 还是仍然在用 function 来定义“类”?为什么这么做?如何把使用 function 定义类的代码改造到 class 的新语法?

在 react 应用里面写的多,class 也有用,就是比较繁琐,function 加上现在 react 的 hook 用起来真的挺爽。

怎么做的话???这个就不说了

posted @ 2020-06-16 16:37  jaiodfjiaodf  阅读(145)  评论(0编辑  收藏  举报