关于javascript:JavaScript-对象我们真的需要模拟类吗

19次阅读

共计 5666 个字符,预计需要花费 15 分钟才能阅读完成。

作者|程劭非

起源|极客工夫《重学前端》专栏

晚期的 JavaScript 程序员个别都有过应用 JavaScript“模仿面向对象编程”的经验,不过,JavaScript 自身就是面向对象的,它并不需要模仿,只是它实现面向对象的形式和支流的流派不太一样,所以才让很多人产生了误会。

那么,接着依照咱们了解的思路持续深刻,这些“模仿面向对象”,实际上做的事件就是“模仿基于类的面向对象”。

只管我认为,“类”并非面向对象的全副,但咱们不应该指责社区呈现这样的计划,事实上,因为一些公司政治起因,JavaScript 推出之时,管理层就要求它去模拟 Java,所以,JavaScript 创始人 Brendan Eich 在“原型运行时”的根底上引入了 new、this 等语言个性,使之“看起来更像 Java”,而 Java 正是基于类的面向对象的代表语言之一。

然而 JavaScript 这样的半吊子模仿,短少了继承等要害个性,导致大家试图对它进行修补,进而产生了种种互不相容的解决方案。

庆幸的是,从 ES6 开始,JavaScript 提供了 class 关键字来定义类,只管,这样的计划依然是基于原型运行时零碎的模仿,然而它修改了之前的一些常见的“坑”,对立了社区的计划,这对语言的倒退有着十分大的益处。

实际上,我认为“基于类”并非面向对象的惟一状态,如果咱们把眼帘从“类”移开,Brendan 当年抉择的原型零碎,就是一个十分优良的形象对象的模式。

咱们从头讲起。

什么是原型?

原型是适应人类天然思维的产物。中文中有个成语叫做“照猫画虎”,这里的猫看起来就是虎的原型,所以由此,咱们能够看出,用原型来形容对象的办法能够说是古已有之。

在不同的编程语言中,设计者也利用各种不同的语言个性来形象形容对象。

最为胜利的流派是应用“类”的形式来形容对象,这诞生了诸如 C++、Java 等风行的编程语言。这个流派叫做基于类的编程语言。

还有一种则就是基于原型的编程语言,它们利用原型来形容对象。咱们的 JavaScript 就是其中代表。

“基于类”的编程提倡应用一个关注分类和类之间关系开发模型。在这类语言中,总是先有类,再从类去实例化一个对象。类与类之间又可能会造成继承、组合等关系。类又往往与语言的类型零碎整合,造成肯定的编译时能力。

与此绝对,“基于原型”的编程看起来更为提倡程序员去关注一系列对象实例的行为,而后才去关怀如何将这些对象,划分到最近的应用形式类似的原型对象,而不是将它们分成类。基于原型的面向对象零碎通过“复制”的形式来创立新对象。一些语言的实现中,还容许复制一个空对象。这实际上就是创立一个全新的对象。

基于原型和基于类都可能满足根本的复用和形象需要,然而实用的场景不太雷同。

这就像专业人士可能喜爱在看到老虎的时候,喜爱用猫科豹属豹亚种来形容它,然而对一些不那么正式的场合,“大猫”可能更为靠近直观的感触一些。(插播一个冷常识:比起老虎来,美洲狮在历史上相当长时间都被划分为猫科猫属,所以性情也跟猫更类似,比拟亲人)

咱们的 JavaScript 并非第一个应用原型的语言,在它之前,self、kevo 等语言曾经开始应用原型来形容对象了,事实上,Brendan 更是曾走漏过,他最后的构想是一个领有基于原型的面向对象能力的 scheme 语言(然而函数式的局部是另外的故事,这篇文章里,我临时不做具体讲述)。

在 JavaScript 之前,原型零碎就更多与高动态性语言配合,并且少数基于原型的语言提倡运行时的原型批改,我想,这应该是 Brendan 抉择原型零碎很重要的理由。

原型零碎的复制操作,有两种实现思路,一个是并不真的去复制一个原型对象,而是使得新对象持有一个原型的援用,另一个是切实地复制对象,从此两个对象再无关联。历史上的基于原型语言因而产生了两个流派,显然,JavaScript 显然抉择了前一种形式。

JavaScript 的原型

如果咱们抛开 JavaScript 用于模仿 Java 类的简单语法设施(如 new、Function Object、函数的 prototype 属性等),原型零碎能够说相当简略,我能够用两条概括:

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

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

  • Object.create 依据指定的原型创立新对象,原型能够是 null;
  • 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 中的原型和类。

晚期版本中的类与原型

在晚期版本的 JavaScript 中,“类”的定义是一个公有属性 [[class]],语言规范为内置类型诸如 Number、String、Date 等指定了 [[class]] 属性,以示意它们的类。语言使用者惟一能够拜访 [[class]] 属性的形式是 Object.prototype.toString。

以下代码展现了所有具备内置 class 属性的对象:

 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 的后果产生了影响。

然而,思考到 JS 语法中跟 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 的不残缺的 pollyfill,见以下代码:


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

这段代码创立了一个空函数作为类,并把传入的原型挂在了它的 prototype,最初创立了一个它的实例,依据 new 的行为,这将产生一个以传入的第一个参数为原型的对象。

这个函数无奈做到与原生的 Object.create 统一,一个是不反对第二个参数,另一个是不反对 null 作为原型,所以放到明天意义曾经不大了。

ES6 中的类

好在 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 类,并且通过 entends 关键字让 Dog 继承了它,展现了最终调用子类的 speak 办法获取了父类的 name。

比起晚期的原型模仿形式,应用 extends 关键字主动设置了 constructor,并且会主动调用父类的构造函数,这是一种更少坑的设计。

所以当咱们应用类的思维来设计代码时,应该尽量应用 class 来申明类,而不是用旧语法,拿函数来模仿对象。一些激进的观点认为,class 关键字和箭头运算符能够齐全代替旧的 function 关键字,它更明确地区分了定义函数和定义类两种用意,我认为这是有肯定情理的。

原文地址:

https://time.geekbang.org/col…

相干文章

  1. ES2020 中 Javascript 10 个你应该晓得的新性能
  2. javascript 代码重构之:写好函数
  3. 再看 JavaScript 继承

最初

关注公众号:前端开发博客,回复 1024,支付前端进阶材料

正文完
 0