JavaScript中的工厂方法、构造函数与class

JavaScript中的工厂方法、构造函数与class

本文转载自:众成翻译

译者:谢于中

链接:http://www.zcfy.cc/article/1129

原文:https://medium.com/javascript-scene/javascript-factory-functions-vs-constructor-functions-vs-classes-2f22ceddf33e#.wby148xu6

在ES6出现之前,人们常常疑惑JavaScript中的工厂模式和构造函数模式到底有什么区别。ES6中提出了_class关键字,许多人认为这解决了构造函数模式中的诸多问题。事实上,问题并没有得到解决。下面就让我们来看看工厂模式、构造函数模式和class_的一些重要区别。

首先,让我们看看这三种方式的例子:

// class
class ClassCar {
  drive () {
    console.log(‘Vroom!‘);
  }
}

const car1 = new ClassCar();
console.log(car1.drive());

// constructor(构造函数模式)
function ConstructorCar () {}

ConstructorCar.prototype.drive = function () {
  console.log(‘Vroom!‘);
};

const car2 = new ConstructorCar();
console.log(car2.drive());

// factory (工厂模式)
const proto = {
  drive () {
    console.log(‘Vroom!‘);
  }
};

function factoryCar () {
  return Object.create(proto);
}

const car3 = factoryCar();
console.log(car3.drive());

这些方式都将方法存储于共享的原型中,然后通过构造函数的闭包有选择的支持私有数据。换句话说,他们几乎拥有相同的特性,所以通常来说也能交替使用。

> 在JavaScript中,任何函数都能返回一个新的对象。当这个函数不是构造函数或_class_时,它就叫做工厂函数。

ES6中的_class_其实是构造函数的语法糖,所以它具有构造函数的一切优点和缺点:

class Foo {}
console.log(typeof Foo); // function

构造函数和_class_的优点

  • 大多数书本都教了_class_或者构造函数。
  • _this_指向新的对象。
  • 一些人喜欢_myFoo = new Foo()_这种写法。
  • 构造函数和_class_存在许多微小的优化,不过除非你已经定量的分析了代码,并且这些优化确实对程序的性能很重要,否则这不应该被列入考虑范围。

构造函数和_class_的缺点

1.需要new

在ES6之前,漏写_new_是一个普遍的bug。为了对付它,许多人使用了以下方式:

function Foo() {
  if (!(this instanceof Foo)) { return new Foo(); }
}

在ES6(ES2015)中,如果你试图不使用_new来调用class构造函数,将抛出错误。只有用工厂模式包裹class,才有可能避免必须使用new。对于未来版本的JavaScript,有人提出建议,希望能够定制忽略new_时class构造函数的行为。不过这种方式依然增加了使用class时的开销(这也意味着会有更少的人使用它)。

2. 调用API时,实例化的细节被泄露(从new的角度展开)

构造函数的调用方法与构造函数的实现方式紧密耦合。当你需要其具有工厂方法的灵活性时,重构起来将会是巨大的变化。将class放入工厂方法进行重构是非常常见的,它甚至被写入了Martin Fowler, Kent Beck, John Brant, William Opdyke, 和 Don Roberts的 “Refactoring: Improving the Design of Existing Code”

3. 构造函数模式违背了开/闭原则

由于对_new_的要求,构造函数违背了开/闭原则:即一个API应该对扩展开放,对改装关闭。

我的意见是,既然从类到工厂方法的重构是非常常见的,那么应该将不应该造成任何破坏作为所有构造函数进行扩展时的标准。

如果你开放了一个构造函数或者类,而用户使用了这个构造函数,在这之后,如果需要增加这个方法的灵活性,(例如,换成使用对象池的方式进行实现,或者跨执行上下文的实例化,或者使用替代原型来拥有更多的继承灵活性),都需要用户同时进行重构。

不幸的是,在JavaScript中,从构造函数或者类切换到工厂方法需要进行巨大的改变

// Original Implementation:
// 原始实现:

// class Car {
//   drive () {
//     console.log(‘Vroom!‘);
//   }
// }

// const AutoMaker = { Car };

// Factory refactored implementation:
// 工厂函数重构实现:
const AutoMaker = {
  Car (bundle) {
    return Object.create(this.bundle[bundle]);
  },

  bundle: {
    premium: {
      drive () {
        console.log(‘Vrooom!‘);
      },
      getOptions: function () {
        return [‘leather‘, ‘wood‘, ‘pearl‘];
      }
    }
  }
};

// The refactored factory expects:
// 重构后的方法希望这样调用
const newCar = AutoMaker.Car(‘premium‘);
newCar.drive(); // ‘Vrooom!‘

// But since it‘s a library, lots of callers
// in the wild are still doing this:
// 但是由于这是一个库,许多用户依然这样使用
const oldCar = new AutoMaker.Car();

// Which of course throws:
// TypeError: Cannot read property ‘undefined‘ of
// undefined at new AutoMaker.Car
// 这样的话,就会抛出:
// TypeError: Cannot read property ‘undefined‘ of
// undefined at new AutoMaker.Car

在上面的例子中,我们首先提供了一个类,但是接下来希望提供不同的汽车种类。于是,工厂方法为不同的汽车种类使用了不同的原型。我曾经使用这种技术来存储不同的播放器接口,然后通过要处理的文件格式选择合适的原型。

4. 使用构造函数会导致instanceof具有欺骗性

与工厂方法相比,构造函数带来的巨大变化就是_instanceof的表现。人们有时使用instanceof进行类型检查。这种方式其实是经常会出问题的,我建议你避免使用instanceof_。

> instanceof会撒谎。

// instanceof is a prototype identity check.
// NOT a type check.

// instanceof是一个原型检查
// 而不是类型检查

// That means it lies across execution contexts,
// when prototypes are dynamically reassigned,
// and when you throw confusing cases like this
// at it:

function foo() {}
const bar = { a: ‘a‘};

foo.prototype = bar;

// Is bar an instance of foo? Nope!
console.log(bar instanceof foo); // false

// Ok... since bar is not an instance of foo,
// baz should definitely not be an instance of foo, right?
const baz = Object.create(bar);

// ...Wrong.
console.log(baz instanceof foo); // true. oops.

_instanceof进行类型检查的方式和强类型语言不一样,它会将对象的[[Prototype]]对象和Constructor.prototype_属性进行一致性检查。

例如,当执行上下文发生变化时,_instanceof会发生错误。当Constructor.prototype变化了之后,instanceof_一样不会正常工作。

当你从一个class或构造函数开始(这将会返回指向 Constructor.prototypethis),然后转而探索另外一个对象(没有指向 Constructor.prototype),这同样会导致instanceof的失败。这种情况在将构造函数转换为工厂函数时会出现。

简而言之,_instanceof_是另外一种将构造函数转换为工厂函数时会发生的巨大变化

使用class的优点

  • 方便的,自包含的语法。
  • 是JavaScript中使用类的一种单一、规范的方式。在ES6之前,在一些流行的库中已经出现了其实现方式。
  • 对于有基于类的语言的背景的人来说,会更加熟悉。

使用class的缺点

除了具有构造函数的缺点外,还有:

  • 用户可能会尝试使用extends关键字来创建导致问题的多层级的类。

多层级的类将会导致许多在面向对象程序设计中广为人知的问题,包括脆弱的基类问题,香蕉猴子雨林问题,必要性重复问题等等。不幸的是,class可以用来extends就像球可以用来扔,椅子可以用来坐一样自然。想了解更多内容,请阅读“The Two Pillars of JavaScript: Prototypal OO” and “Inside the Dev Team Death Spiral”.

值得指出的是,构造函数和工厂函数都有可能导致有问题的层次继承,但通过_extends关键字,class提供了一个让你犯错的功能可见性。换句话说,它鼓励你思考不灵活的的而且通常是错误的_is-a_关系,而不是更加灵活的_has-a 或者 _can-do_组件化关系。

> 功能可见性是让你能够执行一定动作的机会。例如,旋钮可以用来旋转,杠杆可以用来拉,按钮可以用来按,等等。

使用工厂方法的优点

工厂方法比构造函数或类都要灵活,并且它不会诱惑人们使用_extends_来构造太深的继承层级。你可以使用多种方法来继承工厂函数。特别的,如果想了解组合式工厂方法,请查看Stamp Specification

1. 返回任意对象与使用任意原型

例如,你可以通过同一API轻松的创建多种类型的对象,例如,一个能够针对不同类型视频实例化播放器的媒体播放器,活着能够出发DOM事件或web socket事件的事件库。

工厂函数还能跨越之行上下文来实例化对象,充分利用对象池,并且允许更灵活的原型模型继承。

2. 没有重构的忧虑

你永远不需要从一个工厂转换到一个构造函数,所以重构将永远不会是一个问题。

3. 没有_new_

关于要不要使用_new只有一个选择,那就是不要用。(这将会使this_表现不好,原因见下一点)。

4. 标准的_this_行为

this和它通常的表现一样,所以你可以用它来获取其父级对象。例如,在player.create()中,this指向player,正如其他方法调用那样。call() 和_apply()也会同样的指向this_。

5. 不会有欺骗性的_instanceof_问题

6. 有些人喜欢myFoo = createFoo()这种写法

工厂方法的缺点

  • 不会创建一个指向_Factory.prototype的链接——但这其实是件好事,因为这样你就不会得到一个具有欺骗性的instanceof。相反,instanceof_会一直失败。详情见工厂方法的优点。
  • _this_不会指向工厂方法中的新对象。详情见工厂方法的优点。
  • 在经过微优化的基准中,工厂方法可能会稍微比构造函数模式慢一些。如果这对你有影响,请务必在你程序的上下文中进行测试。

结论

在我看来,_class或许有简单的语法形式,但这不能弥补它引诱粗心的用户在类继承中犯错的事实。对于未来它也是有风险的,因为你有可能会想将其升级成为一个工厂函数,而由于new_关键字,你的所有调用都将和构造函数紧密耦合,于是从class向工厂方法迁移将会是一个巨大的改变。

你也许会想可以只重构调用部分,不过在大的团队中,或者你使用的class是公共API的一部分,你就有可能要破坏不在你掌控中的代码。换句话说,不能假设只重构调用部分永远是一个可选项。

关于工厂方法,有趣的事情在于它们不仅更加强大和灵活,而且是鼓励整个团队,以及所有API用户使用简单、灵活和安全的模式的最简单的方法。

关于工厂函数的好处,特别是关于对象组合的能力,还有许多内容可以详述。想要了解更多内容,以及这种方式与类继承的区别,请阅读“3 Different Kinds of Prototypal Inheritance”.



For more training in in prototypal inheritance techniques, factory functions, and object composition, be sure to check out “The Two Pillars of JS: Composition with Prototypes”?—?free for members. Not a member yet?

Learn JavaScript with Eric Elliott



Eric Elliott_ is the author of “Programming JavaScript Applications” (O’Reilly), and “Learn JavaScript with Eric Elliott”. He has contributed to software experiences for Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, and top recording artists including Usher, Frank Ocean,Metallica, and many more._

He spends most of his time in the San Francisco Bay Area with the most beautiful woman in the world.

时间: 2024-11-15 02:15:53

JavaScript中的工厂方法、构造函数与class的相关文章

javascript 中关于call方法的详解。

关于javascript中的call方法,网上查了一些资料总是不得详解.总结网上的观点,call有两个妙用: 1: 继承.(不太喜欢这种继承方式.) 2: 修改函数运行时的this指针. js中关于call的解释如下: js关于call的这份文档容易让人迷糊.而<javascript权威指南>对call的描述就比较容易理解了. 注意红色框中的部分,f.call(o)其原理就是先通过 o.m = f 将 f作为o的某个临时属性m存储,然后执行m,执行完毕后将m属性删除. 如 function f

javascript中的toString()方法

javascript中的toString()方法,主要用于Array.Boolean.Date.Error.Function.Number等对象.下面是这些方法的一些解析和简单应用,做个纪律,以作备忘. (1)Array.toString():将数组转换成一个字符串,并且返回这个字符串.描述:当数组用于字符串环境中时,javascript会调用这一方法将数组自动转换成一个字符串.toString()在把数组转换成字符串时,首先要将数组的每个元素都转换成字符串(通过调用这些元素的toString方

Javascript中,数学方法可以分成以下几类: constans(常数)、power functions(乘方函数)、trigonometic functions(三角函数)、rounding functions(舍入函数)、random numbers(随机数字)

在Javascript中,数学方法可以分成以下几类: constans(常数).power functions(乘方函数).trigonometic functions(三角函数).rounding functions(舍入函数).random numbers(随机数字) 常数和乘方函数 Math.E 自然对数的底(为常数) 2.718 Math.LN10 10的自然对数 2.302 Math.LN2 2的自然对数 0.693 Math.PI 圆周率 3.1415 Math.SQRT1_2 1/

javascript中数组的方法你真的都了解吗?

本篇文章主要讲述ES5中的数组,包括数组两种创建方式,属性,以及 9 大类 ,总共23个操作方法,非常全面,看完之后ES5数组这一部分基本都了解了,下一篇文章,我会讲述ES6中对数组的加成,新增了哪些方法,以及定型数组,类数组和类数组的所有方法,记得关注哦! 数组作为javascript中最常用的数据类型之一,掌握好数组的方法在日常的开发中是非常有必要的,javascript中的数组相比其他语言来说更灵活,因为数组的每一项都可以是不同的数据类型,可以是对象,数组,字符串,数值等等,接下来一点一点

javascript中的sort()方法

非原创,转载于博客园 现在在学习javascript中,发现sort()函数是有点奇怪的东西(可能是本人水平的问题-_-!),于是就在这里记录一下自己找到的东西吧.sort()这个方法的参数很奇怪,必须是函数,但也是可选参数,如果没有参数的话,就会默认以字符串的字典顺序来排列(就算是数值,也会被转化为字符串来处理).这个参数是要能够比较两个值的大小,如: function sortNumber(a, b){   return a - b; //这里返回的是他们的差值,如果是小于0的值,就会将a排

Java和JavaScript中使用Json方法大全

林炳文Evankaka原创作品. 转载请注明出处http://blog.csdn.net/evankaka   摘要:JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式. 它基于ECMAScript的一个子集. JSON採用全然独立于语言的文本格式,可是也使用了相似于C语言家族的习惯(包含C.C++.C#.Java.JavaScript.Perl.Python等).这些特性使JSON成为理想的数据交换语言. 易于人阅读和编写.同一时候也易于机器解析和生成

javascript中常用数组方法详细讲解

javascript中数组常用方法总结 1.join()方法: Array.join()方法将数组中所以元素都转化为字符串链接在一起,返回最后生成的字符串.也可以指定可选的字符串在生成的字符串中来分隔数组的各个元素.如果不指定分隔符,默认使用逗号.案例如下: var a=[1,2,3]; a.join();//=> "1,2,3" 因为没有指定分隔符,默认为逗号. a.join("+");//=> "1+2+3" 指定分隔符为+ a.

javascript 中 call apply 方法的区别

function foo(arg1, arg2, arg3){ alert(art1 + arg2 + arg3); } foo.call(this, arg1,arg2,arg3) == foo.apply(this, arguments) == this.foo(arg1, arg2, arg3) call , apply 都属于 Function.prototype的一个方法, 它是JavaScript引擎内在实现的, 因为属于 Function.prototype  所以每个Functi

javascript中的call方法

该方法应用于function对象(即function本身或者通过new关键字创建的function实例) 该方法的第一个用途是调用一个对象的一个方法(非常的干巴巴是不是?官方解释往往都不说人话,看下面的例子即可): function add(a,b){ document.write(a + b); } function sub(a,b){ document.write(a - b); } add.call(sub,3,1); 页面输出结果为:4所以到这儿可有这样的总结:call方法可以由函数A来