《JavaScript专家编程》——1.2 对象概述

异步社区 2017-05-02

编程语言 javascript 函数 HTTPS 面向对象 编程 ScreenShot

本节书摘来自异步社区《JavaScript专家编程》一书中的第1章,第1.2节,作者:【美】Mark Daggett(达格特)著,更多章节内容可以访问云栖社区“异步社区”公众号查看

1.2 对象概述

JavaScript是由Brendan Eich创建的一种面向对象编程(OOP)语言,当时他还在Netscape公司工作,花了几周的开发时间就发布了。虽然JavaScript的名字中有个“Java”,但它实际上跟Java语言没什么关系。在InfoWorld的一篇对Eich的采访稿中,他解释了JavaScript命名的由来:

InfoWorld:据我所知,JavaScript开始的时候叫Mocha,后来改名叫LiveScript,在 Netscape和Sun合并以后才叫JavaScript的。但实际上它跟Java没什么关系,是这样吗?

Eich:没错。从Mocha变到LiveScript,是从5月到12月(1995)的事情。后来到了12月初,Netscape跟Sun签了一个许可协议,它就改名为JavaScript了。而当时的想法是,让JavaScript成为补充编译型语言Java的一种脚本语言2。

即使随便比较一下这两种语言都能看出,其实它们是完全不一样的。跟Java很不同的一点是,JavaScript不需要编译,非强类型,也没有一种正规的基于类的继承机制。相反,JavaScript运行在宿主环境的上下文中(比如一个Web浏览器),支持动态类型的变量,通过原型链而不是类来实现继承。因此,我们可以认为当时应该是想让人们因为JavaScript与Java名字相似而记住它,并对市场宣传有一定的帮助,其实这两种语言没有什么实质上的联系。

然而,尽管它们大相径庭,但Java和JavaScript都是面向对象编程语言家族中的一员。面向对象是对象通过相互之间的通信而控制程序执行的方法。它是一种比较流行的编程范式,除此之外还有函数式(Functional)、命令式(Imperative)和声明式(Declarative)。


 JavaScript虽然被大家普遍认为是面向对象的编程语言,但并不意味着它不支持其他的编程范式。例如,流行类库Underscore.js3就是用函数式的风格编写而成的。
1.2.1 对象化
作为一个面向对象的编程语言意味着什么呢?对一个有经验的程序员来说这可能不是个问题,但是回答这个问题会给你机会来评价一下JavaScript实现面向对象的方式。本书大量的篇幅会让你设计和思考对象以及它们之间的关系。但要记住,对象只是用于编程建模的多种隐喻(metaphors)之一。

隐喻通常都诱人而又晦涩难懂。对它的理解可以让你更加清晰地构思一个问题的解决方案,而不是陷入不必要的复杂的泥潭。当你要回答面向对象编程意味着什么时,思考下你自己的理解和预设,你可能会发现自己对这个问题的理解是有偏见的。

在JavaScript中,对象仅仅是属性(properties)的容器。我听说有的程序员把对象形容成“属性包”,听起来很有画面感。每个对象都可以有零个或多个属性,这些属性可以持有一个基本类型(primitive)的值,也可以指向一个复杂类型的对象。JavaScript可以通过三种方式创建对象:使用字面量(literal notation)、new()运算符或create()函数。这三种方式可以简单表示如下:

screenshot

这些方法之间的区别在于对象初始化的方式,稍后我们会详细解释。现在让我来看一下,如何通过自定义属性来修饰对象。

1.属性管理器
很多开发人员都认为对象的属性只是一个容器,用于将name和value赋进去。实际上,JavaScript让开发人员可以通过一系列强大的属性描述符,进一步定制属性的行为。下面我们逐个分析一下。

(1)可配置特性(configurable)
当这个特性(attribute)设为true时,属性可以从父对象中删除,未来还可以修改属性的描述符;当设置为false时,属性的描述符会被锁定,无法修改。下面是一个简单的例子:

screenshot

正如在例子中看到的,wheel属性不可变而doors属性仍然可变。程序员可能会将属性的configurable特性设为false,用于保护对象不被修改,这是一种防御性编程的形式,就像语言中内置对象一样。

(2)可枚举特性(enumberable)
如果对象的属性可以使用代码来遍历,那这些属性就是可枚举(enumberable)的。当将其设为false时,这些属性就不能被遍历了。举个例子:
screenshot

正如在例子中看到的,即使一个属性不是enumerable的,这也不意味着这个属性是完全被隐藏起来的。enumerable特性可以阻止程序员使用某些属性,但它不应该作为一种属性不被检视的方式。

(3)可写特性(writable)
当可写特性(writable)为true时,与属性相关联的值是可以改变的;否则,值不可改变。

screenshot

2.检视对象
在上一小节中,你知道了如何定义对象的属性。在这一节中,你会学到如何在JavaScript中深入地挖掘对象,这和生活中知道如何读写一样有用。下面是一系列检视对象时需要了解的函数和属性。

(1)Object.getOwnPropertyDescriptor
在上一小节中,你看到了很多用于设置属性特性的方式。Object.getOwnProperty Descriptor可以详细告诉你对象属性特性的配置。
screenshot

(2)Object.getOwnPropertyNames
这个方法返回对象全部属性的名字,包括那些不能枚举的:

screenshot

(3)Object.getPrototypeOf
这个方法用来返回特定对象的原型。有时还可以使用__proto__方法来代替这个方法,很多解释器的实现会使用这个方法来获取对象的原型。然而,使用__proto__总让人感觉有点hack的味道,JavaScript社区也主要把它用作权宜之计。然而值得注意的是,即使Object.get PrototypeOf可以让你访问一个对象的原型,但是设置一个对象实例的原型的唯一方法是使用__proto__属性。

screenshot

(4)Object.hasOwnProperty
JavaScript的原型链可以让你通过遍历一个对象的实例,返回所有可枚举的属性,包括不存在于这个对象上但存在于原型链中的属性。hasOwnProperty方法可以让你分辨出某个属性是否存在于对象实例中:

screenshot

(5)Object.keys
这个方法仅返回对象中可枚举的属性:

screenshot

(6)Object.isFrozen
如果对象不能扩展,属性也不能修改,那么这个方法返回true,反之返回false。

screenshot

(7)Object.isPrototypeOf
这个方法在对象的整个原型链中检查每一环,看传入的对象是否存在于其中:

screenshot


 在写这篇文章的时候,=>箭头语法仅被如Firefox 22(SpiderMonkey 22)等浏览器支持。这种语法在不支持的浏览器中运行会产生语法错误。
(8)Object.isExtensible
默认情况下,在JavaScript中新生成的对象是可扩展的,即可添加新的属性。然而,对象在未来可以被标记为不可扩展。某些环境下,在一个不可扩展的对象上设置属性会抛出错误。在试图修改一个对象前,你可以使用Object.isExtensible来检查这个对象是否可被修改:

screenshot

(9)Object.isSealed
这个函数返回true还是false,取决于一个对象是否可以被扩展,以及它的全部属性是否都不可配置(nonconfigurable):

screenshot

(10)Object.valueOf
如果你曾经试过检视一个对象,仅仅通过看它返回“[object Object]”,就已经可以知道这个函数的功能了。Object.valueOf使用一个基本类型的值来描述一个对象。所有对象都包含这个函数,但基本上都是stub的,意味着未来要用自定义的函数覆写。通过创建自己的valueOf函数,可以给你提供额外的方法来描述自定义对象。

screenshot

(11)Object.is(ECMAScript 6)
对一些程序员来说,在JavaScript中测试两个值相等一直都是个痛点,因为JavaScript实际上支持两种形式的相等判定。对于检查抽象相等,JavaScript使用双等号的语法==。当检查严格相等时,JavaScript使用三等号的语法===。两者之间的主要区别是,默认情况下,抽象相等运算符比较时会进行强制转换。Object.is方法可以判定两个传入参数,在不需要强制转换的情况下,是否具有相同的值。下面是如何使用Object.is方法的一些例子:

screenshot

不要将这个方法和严格相等运算符搞混,严格相等只有当两个比较的对象具有相同的类型,而不仅仅是相同的值时,才会返回true。可以很容易地用下面的例子来表示:
screenshot

3.修改对象
除了探索存在对象的结构之外,也需要了解对象修改(或者禁止修改)。本节会介绍多种操作对象的方法。

(1)Object.freeze
冻结对象可以防止它再次被改变。被冻结的对象不能加入新的属性,已有的属性也不能被移除,已有的属性值也不能被改变:

screenshot

(2)Object.defineProperties
此函数允许定义新的属性或修改已有的属性:

screenshot

(3)Object.defineProperty
此函数允许把某个属性加到对象中,或者修改一个已存在的属性。

screenshot

(4)Object.preventExtensions
这个函数可以阻止新的属性被加到一个已有的对象中。不要把这个方法和冻结对象搞混。虽然一个对象不能被扩展,但可以减缩,也就是说属性可以被移除。

screenshot

(5)Object.prototype
通过设定对象原型,使对象从现有的原型链解耦,并将此对象加到新对象的原型链尾部。这非常有用,可以把其他对象的属性和方法,加到已有对象中。

screenshot

(6)Object.seal
密封(sealing)一个对象使其不可变,意味着不能添加新的属性,已有的属性被标识为不可配置(noconfigurable)的。这与冻结对象不同,冻结对象会使对象不能进一步修改,你可以在下面的例子中看到它们的区别:

screenshot

4.调用对象
一个对象可以从其他对象中借一些函数来使用,这很有用。也就是说运行借来的函数就像运行自己的函数一样简单。就好像你从朋友那借了一件外套。你可以临时使用外套来取暖,但你用完了就把它还回去。在JavaScript中想要“借外套”,可以通过call()和apply()函数来实现。它们的行为非常相似,只是call()函数接受一个参数列表,apply()函数接受一个参数数组。这两个方法对使用临时函数来填充对象非常有用,例如使用核心对象的内置函数或者链式调用构造函数。

Function.call和Function.apply
screenshot

5.创建对象
JavaScript把几乎一切都当作对象,因此语言中几乎所有的元素都可以被创建、赋予属性以及被链接到原型链中。仅有的例外就是null和undefined。在JavaScript中对象是创建出来的,它们不是凭空产生的。在本节中,我会解释三种对象创建的方法,以及为什么我们不仅仅用一种方法来创建。


 我曾经错误地认为数字不是对象,因为我不能用点语法来调用方法,例如,1.toString()。事实证明,大部分解释器假设这个句点是用来描述整数和小数之间的分隔符。如果你使用圆括号来调用方法,如(1).toString()或者双点如1..toString(),这样它就工作了!
(1)对象字面量
字面量(Literal)语法可以用内联的方式描述一个对象,外面有一个大括号,里面的代码是一系列由逗号隔开的属性。不像new Object()和Object.create()语法,字面量语法不会被显示调用,因为在特定的上下文中,字面量实际上是使用Object.create方法的快捷方式。举个例子:

screenshot

字面量语法很清晰,表现力强而且很紧凑。你可以使用内联的方式同时描述和创建对象。这种特质让字面量语法成为创建简单的一次性对象的最佳选择。这种一次性对象可以用来处理事件、整理对象间状态的改变或者划分功能,同时可以保持代码在视觉上的聚合。字面量语法和new Object()的另一个细小区别是,字面量语法的构造函数不能被重定义。不过,原生对象的构造函数属于全局命名空间,如果修改可能会导致意想不到的行为,很难追踪。字面量语法被隐式调用这一点,给代码提供了一点防御性。

screenshot

这种字面量语法并不是在所有情况下都适用,例如,你不能创建一个原型不是内置对象的对象。而且因为字面量语法是隐式调用的,所以它没有显式的构造函数,这意味着字面量对象不能作为对象工厂。


 对象字面量不是JSON。很多人搞不清楚什么是对象字面量,什么是JSON,即使它们看起来很相似,但实则大不相同。JSON是一种数据描述语言,因此它不能包含函数。另外,很多JSON解释器需要属性使用双引号来定义,而字面量语法并没有这样的要求。
(2)new Object()
当我谈到new Object()时,我其实讨论的是new运算符。这个运算符根据需求来创建对象的实例。它接受一个构造函数和一系列初始化过程中使用的可选参数。对象创建完成后,新创建的对象继承自构造函数的原型。

screenshot

new运算符是一种JavaScript试图让自己类似于Java的退化结构。很多人都会对new运算符感到很疑惑,因为它强加了一种伪类(pseudo-classical)动词到JavaScript中,但JavaScript并不是一种正规的基于类继承方法的语言。为了更好地理解new关键字在幕后做了什么,让我们看一下前面的例子,来剖析下new都为我们做了什么。希望这可以清除掉由它的语义带来的潜在的歧义。

1.JavaScript创建一个新的对象
这相当于创建了一个对象字面量{}。

2.JavaScript将新创建的对象的构造函数链接到函数Animal上
screenshot

3.JavaScript将对象的原型链接到Animal.prototype
在对象构造过程中,新创建的对象会获得前一个构造函数属性的引用。它们是一个浅拷贝(shallow copy),如果​​以后修改的话,实际情况是原构造函数属性的引用会被一个本地的引用覆盖。

screenshot

4.JavaScript将传入的参数赋给新创建的对象
new运算符在新对象创建时,将任意数量的属性初始化到对象中。它们会作为参数传入到构造函数中。

screenshot

如果你觉得new运算符是一个勤劳的小精灵,根据一个清单来创建对象,那还不错。然而,你如果假设new的行为就如在其他语言比如Java中一样,那绝不会发生什么好事情。

(3)Object.create
在ECMAScript 5中引入Object.create方法之前,只能使用new运算符来实现原型链继承。总而言之,Object.create()和字面量对象应该替换new Object()这种方法。Object. create()给开发者提供了new运算符的优点,而且方法签名与语言其他的部分还能保持一致。Object.create的好处已经超越了仅仅提升语义本身,实际上它在对继承的支持方面更加强大。Object.create可以接收两个参数:一个是提供原型的对象,另一个是可选属性对象,这个对象包含对新创建对象的配置。

screenshot

这一部分研究了在JavaScript中对象创建、访问和修改的多种方法。沿着这条路,我展示了原型概念是如何工作的。下一节会解释JavaScript是如何实现面向对象中常用逻辑的,例如继承以及开发人员在使用过程中会遇到的一些普遍问题。

1.2.2 原型编程
面向对象编程语言的目的,是为了创建一些可以交互的虚拟对象,从而完成特定的任务。通常,这意味着使用代码来表示实体,然后编写软件来完成开发人员的目标。虽然前面的定义听起来有些直白,但实际上,在对象的数据和状态的组织和交换中,总会有不可避免的麻烦。在把一个复杂的现实世界的领域问题,转换到一系列彼此依赖的对象时,这个问题格外突出。一般来说,使用面向对象编程语言,在移植这些实体到代码中时,通过高阶概念的应用,包括抽象、封装、继承和多态,会使用到一些组织结构非常复杂的继承结构。在大多数面向对象编程语言中,这些技术都使用类来实现。

在诸如C++、JAVA和Ruby这样的语言中,类是对象的描述,但并不是对象本身。用同样的方式,你可能并不会因为吃了一堆冰激凌而把脑子冻住,也不会使用类来执行你的工作。类是有目的的抽象,因为它们必须要定义所有的特征、能力,要会创建潜在对象的功能。基于类语言的支持者说,类可以提供结构和状态间清晰的描述。相反的观点是,类强制使用不必要的、死板的实体论(ontology)来分类对象。

在JavaScript中,并不存在类的定义。对象通过原型链(如果需要的话)从其他对象中继承功能。这些原型链接可以转换形式,形成彼此依赖的链,从而通过组合来完成复杂的行为。本节会详细解释原型概念的错综复杂之处,以及如何在JavaScript中有效地使用它们。

为了充分解释在编程中使用原型的好处,你首先需要理解抽象、封装、继承和多态被应用到JavaScript中的目的。为了解释这四个概念,我会使用程序实例进行清晰的描绘,JavaScript中的原型和很多其他程序员可能熟悉的面向对象,在解决问题方法上的一些不同之处。

1.抽象
编程中的抽象,是把真实世界的对象或过程转化为可计算的模型。抽象给程序员提供了一种把复杂的大问题,拆解成小的、离散的问题的机制。在大多数面向对象编程语言中,这个过程被称为解耦。使用类或者原型是使用抽象的观点来思考一个问题,这给了我们一个方便的隐喻来组织我们的程序,同时隐藏了与机器交流的低阶代码。关于抽象的一个常见误解是,它们只是为了隐藏信息,把内容解耦成模块,或者定义对象之间清晰的接口。虽然这些都是抽象的战略上的目标,但战术上的实现可以根据语言的不同而不同。在JavaScript中,所有的抽象都是通过原型的使用来完成的,这也是它实现封装、继承和多态的一种机制。

2.封装
在软件设计中的封装有三个目标:隐藏实现、提升模块化以及保护对象内部的状态。设计良好的对象对使用者隐藏不需要的或者需要特权访问的信息。封装通过定义一个公共接口,在程序员使用对象时,提供恰到好处的信息,同时隐藏对象工作的具体细节。通过封装来隐藏信息,也允许业务逻辑可以随着时间推移来改变实现细节,而不需要去影响暴露给用户的公共接口。这就好比用户学习驾驶汽车:一旦他们了解如何使用方向盘和踏板,根本不需要了解发动机有多少阀门。

如果我们扩展前面的例子,我可以打赌,你在汽车之间交换发动机,司机都无需重新学习如何驾驶这辆汽车。他们可能会察觉汽车表现得有些不同,但接口却是保持一致的。从这个例子可以看出封装的另一个好处,就是它提升了代码设计中的模块性。

真正的封装还提供了第三个好处,就是它可以防止私有的逻辑被其他对象访问或修改。在这种工作方式下,封装在类之间如同保护层一般,确保对象内部的工作不受干扰。

在基于类的语言中,一种常用的隐藏实现方式,是通过私有(private)或者公共(public)函数的使用。函数这种私有的特质是被语言强制限定的,使某些代码对类的实例可用,但在对象外部不可访问。下面是Java中的例子:

screenshot

正如你在前面例子中看到的,通过变量的形式直接访问name或wheelCount是不可能的,因为Java允许它们被声明为私有的。要想访问它们,你必须使用类中的公有方法。通常情况下,这些代理方法被称为getter和setter方法​​。使用这种方法,变量就可以被使用了,虽然是通过控制接口的方式。

JavaScript基于原型的实现方式的一个结果是,你不能把对象属性设置为private,这使封装的实现变的困难(但并不是不可能!)。

screenshot

在这段代码中,你可以看到,函数体内部定义了两个局部变量。由于JavaScript函数级的作用域,使得这两个变量隐式地成为私有变量。若想要将它们的值暴露出来,你可以创建自己的getter和setter方法。关键点在于使用局部变量代替对象的属性,这样从外部就不可访问了。

这种实现方式提供了很好的封装,它通过信息隐藏提升了模块化,它能保护对象的内部状态,从而避免全局访问。

3.多态
多态描述了在某个特定的上下文中,对象的表现类似其他对象的一种能力。在面向对象编程语言中,有很多类型的多态,但“运行时多态”(ad hoc polymorphism)4在JavaScript中特别的普遍和有用。本节会探讨一下这种即时多态是如何工作的。

运行时多态
运行时多态给对象提供了使用上下文来形成输出结果的能力。这种上下文可能包含正在调用的对象,或者给方法提供的参数。运行时多态有时指函数的重载或者运算符重载,因为这些技术是实现这种形式多态的一种通用方式。

(1)函数重载
在诸如C++这种静态类型的语言中,函数重载允许开发者使用相同的名字定义多个函数,只要它们的方法签名不同。这些函数之间的差别是使用了不同数量的参数或者不同类型的参数。一旦方法被实现,编译器就会基于提供的参数的数量和类型选择正确的函数。

JavaScript的函数不进行强制类型检查,可以接受随意数量的参数。这种灵活性意味着函数重载立即可以使用,而不需要对同一函数有多种风格的声明。

(2)运算符重载
很多语言支持运算符重载,借此开发人员可以重新定义运算符的功能。JavaScript不支持这个层级的重载,但可以允许运算符基于它们所使用的上下文不同,来调整它们的行为。看一下“+”运算符的行为,使用情景不同,行为也不同。
screenshot

4.继承
继承通过允许子类创建特别的、一般的或者与父类有差异的特质,来定义对象间语义化的层级5。

继承字面上的意思,是向另一个群体传递权利、财产和债务(通常是死后)6。在基于类的语言中,继承被描述成一种对象间“is-a”7的关系(Dog是Mammal的一个子类,Animal是Mammal的一个超类)。

子类可以从父类继承一些特性,这让很多开发人员认为继承是复用代码的方法。直观上讲,这是有道理的,可以想象成对象集合彼此共享特性。通过把公共的特性抽取到基类中,每个子类都将自动从这些特性中受益,而不必在内部重新定义这些功能。

然而,通过继承来做代码复用被严重弱化了,因为在大多数语言中,一个子类只能从一个父类继承。这种限制可能会导致子类继承它不需要的代码,或者需要覆写父类的某些特性。Angus Croll简要描述了使用继承来进行代码复用的这个问题。他这样写道:

使用继承作为代码复用手段,类似于为了塑料玩具,而点一份儿童套餐。确实,圆圈是一个形状,狗是一个哺乳动物,但一旦我们跳出这些教科书的例子之外,在构建一些操作行为时,即时我们假装正在表现现实世界,我们大部分的层级结构还是随意和牵强的。历代子类背负着越来越多的意想不到的或不相关的行为,仅仅是为了重用少量的代码8。

由于经常需要改变从父类继承的特性,使得在父类和子类之间is-a的关系变得一团糟。此外,通过略过或覆写父类的某些方法,子类还会打破封装,由于紧耦合9的关系,让代码变得更加脆弱。

JavaScript中的继承也绝不是完美的。JavaScript使用差分继承(differential inheritance)10,所有的对象都从一个一般的基本对象继承,而不是某个父类。每个被创建的对象,都会保存一个创建它的对象的引用,也就是这个对象的原型。基于类的继承方式使用相似性来定义对象之间的关系,差分继承使用原型和后代的区别作为分界线。

5.强大的原型
包括JavaScript在内的基于原型的语言,允许一个对象通过原型链引用另一个对象来构建对象中的复杂性。JavaScript使用原型链这种机制来实现动态代理。当试图去引用某一个属性时,它会遍历整个原型链,直到最后的节点。实际上,原型为开发人员提供了一种灵活的工具,以此来组织和重用代码。本节将探索如何访问和增强对象的原型链。

理解原型
在JavaScript中,原型可以通过三种方式进行访问。

-Foo.prototype使用new运算符在对象的初始化中定义原型;例如,new Foo()。 - Object.getPrototypeOf(foo)返回给定对象的原型引用。 -Foo.__proto__是一个属性,它指向对象构造方法自己的原型对象。这不是一个标准的属性引用,但古老的的引擎可能依赖于它。因此,__proto__现已编入最新版本的ECMAScript(ES6)中。我仅仅是为了完整性而提及这个属性。如果你想要引用对象的原型,你应该使用标准化的Object.getPrototypeOf()而不是__proto__。

下面的代码示范读取原型对象的几种方式:

screenshot

似乎拥有一个原型对象有些危险,因为如果一个对象无意识地修改了原型中的属性,那么会发生什么呢?事实证明,JavaScript会避免这种事情发生;任何试图去设置原型对象属性的行为,都会在对象实例上产生一个新的属性,从而避免访问到原型链中的属性。继续car的例子,你就会发现这一点:

screenshot

使用这种解决方法有几个好处。

  • 通过被链接对象访问原型的属性,仅仅是一种浅引用,这种方式可以增加一层防御机制,避免不想要的变更。
  • 浅属性引用节省内存,因为仅有一个拥有给定属性或者函数的实例。
  • 被加到原型对象中的属性会立即影响原型链下层的对象。

在最后一个例子中,car的构造函数试图在运行时重置odometer的值,希望可以重置全部实例的值。但是它失败了,因为odometer属性被定义在构造函数中。然而如果你已经使用定义drive方法的方式,在原型中定义了odometer属性,那么只要对象实例没有给自己定义odometer属性,就像发生在drive()函数中那样,那么这种改变是会生效的。

screenshot

6.类的转换
JavaScript没有正式的基于类的结构。尽管上一节已经证明了这个事实,但有些读者的脑海中可能还是有一大堆的疑问。或许这种不信任是因为JavaScript中到处都是类的引用或者基于类的术语。让事情变得更混乱的是,语言有一个保留的class关键字,但它什么也不做!Douglas Crockford 把JavaScript称为伪类(pseudoclassical)语言,他认为类的设计在JavaScript中是一种“不必要的间接等级”(Crockford,2008),因为对象是由构造函数产生的。每当人们在JavaScript中谈论类时,都把这当做一种惯例,而不是语言的特性。

把这一点区分清楚很重要,因为有些人对其他语言的类概念非常熟,会让他们产生误解。这些误解可能会让那些认为类在JavaScript中有同样行为的开发人员掉到坑里。接下来我们会讨论一下,用类来思考问题的JavaScript开发者,如何使用设计模式实现像类一样的行为。这种模式是内置语言特性和代码惯例的融合。

在基于类的面向对象语言中,一般来说,状态由实例来持有,方法由类来持有,继承的仅仅是结构和行为。在ECMAScript中,状态和方法由对象持有,而结构、行为和状态都会被继承11。

(1)构造函数
构造函数直观上讲,目的似乎应该是构造一个对象。而在JavaScript中,构造函数仅仅是一个函数,当调用new运算符时返回一个实例对象。在JavaScript中,当使用new()运算符时调用的那个函数就是构造函数。构造函数的目的,是用合理的默认值来初始化新创建的对象。比较好的方法是,仅仅把那些所有实例都需要的属性和方法,定义到构造函数中去。

screenshot

不是所有的内置函数都能在不使用new运算符的情况下调用。通常这是因为内置对象没有合理的默认值可以返回。调用Date()函数会返回一个代表当前日期时间的字符串,但调用Math()函数会返回一个错误。
screenshot

如果可能的话,最好从构造函数中返回一个类似的结果,不管是否用new运算符的上下文来调用。David Herman在他的主题为“Make Your Constructors new-Agnostic”(Herman,2013)的文章中介绍了更多的细节。然而,许多JavaScript的内置对象不遵守这个约定。

screenshot


 JavaScript没有正式的类,但它确实遵循了其他语言中的命名规范,它也使用首字母大写来命名像类一样的对象(例如以“Foo”命名像类一样的对象)。
(2)实例属性
实例属性是公开的可访问的变量,用来描述对象实例的特征。实例属性是用来区分对象的一些值。在前面的例子中,this.running是一个实例属性。实例属性可以被定义在构造函数中,或者单独被定义成原型对象的一部分。

screenshot

(3)实例方法
实例方法给对象实例提供了一些有用的功能,并且可以访问实例的属性。实例方法可以通过两种方式来定义:通过引用this关键字来扩展实例,或者直接在原型链上设置属性。

screenshot

(4)类属性
类属性是属于类对象自己的一些变量。它们一般用于不会改变的属性,例如常量。核心Math对象有一个类属性PI,其默认值是3.141592653589793。在JavaScript中,类属性可以直接在构造函数中设置。

screenshot

(5)类方法
类方法,有时被称为静态方法 ,是仅仅对类本身可用的函数。类方法可以访问类属性,但不能访问对象实例属性。类方法典型的用途,是对传入参数计算后返回一个结果。以核心对象Math为例来看一下它的类方法。类方法和类属性用同样的方法来定义。如果你想要给内置的String对象添加一个reverse类方法,可以用如下方法:

screenshot


 你事实上不应该像这样扩展JavaScript的核心对象,即使这是被允许的。这虽然只是个坏习惯,但可能潜在地给你或别人的代码引入一些错误。这个规则有一个例外,就是当一个对象需要填充一些其他代码需要的功能的时候。

登录 后评论
下一篇
云栖号资讯小编
20048人浏览
2020-07-13
相关推荐
[从C到C++] 1.2 C++概述
1733人浏览
2017-11-20 11:19:42
0
0
0
1904