来源: ApacheCN『JavaScript 编程精解 中文第三版』翻译项目原文:The Secret Life of Objectsjavascript
译者:飞龙html
协议:CC BY-NC-SA 4.0java
自豪地采用谷歌翻译git
部分参考了《JavaScript 编程精解(第 2 版)》程序员
抽象数据类型是经过编写一种特殊的程序来实现的,该程序根据可在其上执行的操做来定义类型。github
Barbara Liskov,《Programming with Abstract Data Types》apache
第 4 章介绍了 JavaScript 的对象(object)。 在编程文化中,咱们有一个名为面向对象编程(OOP)的东西,这是一组技术,使用对象(和相关概念)做为程序组织的中心原则。编程
虽然没有人真正赞成其精肯定义,但面向对象编程已经成为了许多编程语言的设计,包括 JavaScript 在内。 本章将描述这些想法在 JavaScript 中的应用方式。数组
面向对象编程的核心思想是将程序分红小型片断,并让每一个片断负责管理本身的状态。安全
经过这种方式,一些程序片断的工做方式的知识能够局部保留。 从事其余方面的工做的人,没必要记住甚至不知道这些知识。 不管何时这些局部细节发生变化,只须要直接更新其周围的代码。
这种程序的不一样片断经过接口(interface),函数或绑定的有限集合交互,它以更抽象的级别提供有用的功能,并隐藏它的精确实现。
这些程序片断使用对象建模。 它们的接口由一组特定的方法(method)和属性(property)组成。 接口的一部分的属性称为公共的(public)。 其余外部代码不该该接触属性的称为私有的(private)。
许多语言提供了区分公共和私有属性的方法,而且彻底防止外部代码访问私有属性。 JavaScript 再次采用极简主义的方式,没有。 至少目前尚未 - 有个正在开展的工做,将其添加到该语言中。
即便这种语言没有内置这种区别,JavaScript 程序员也成功地使用了这种想法。 一般,可用的接口在文档或数字一中描述。 在属性名称的的开头常常会放置一个下划线(_
)字符,来代表这些属性是私有的。
将接口与实现分离是一个好主意。 它一般被称为封装(encapsulation)。
方法不过是持有函数值的属性。 这是一个简单的方法:
let rabbit = {}; rabbit.speak = function(line) { console.log(`The rabbit says '${line}'`); }; rabbit.speak("I'm alive."); // → The rabbit says 'I'm alive.'
方法一般会在对象被调用时执行一些操做。将函数做为对象的方法调用时,会找到对象中对应的属性并直接调用。当函数做为方法调用时,函数体内叫作this
的绑定自动指向在它上面调用的对象。
function speak(line) { console.log(`The ${this.type} rabbit says '${line}'`); } let whiteRabbit = {type: "white", speak: speak}; let fatRabbit = {type: "fat", speak: speak}; whiteRabbit.speak("Oh my ears and whiskers, " + "how late it's getting!"); // → The white rabbit says 'Oh my ears and whiskers, how // late it's getting!' hungryRabbit.speak("I could use a carrot right now."); // → The hungry rabbit says 'I could use a carrot right now.'
你能够把this
看做是以不一样方式传递的额外参数。 若是你想显式传递它,你可使用函数的call
方法,它接受this
值做为第一个参数,并将其它处理为看作普通参数。
speak.call(hungryRabbit, "Burp!"); // → The hungry rabbit says 'Burp!'
这段代码使用了关键字this
来输出正在说话的兔子的种类。咱们回想一下apply
和bind
方法,这两个方法接受的第一个参数能够用来模拟对象中方法的调用。这两个方法会把第一个参数复制给this
。
因为每一个函数都有本身的this
绑定,它的值依赖于它的调用方式,因此在用function
关键字定义的常规函数中,不能引用外层做用域的this
。
箭头函数是不一样的 - 它们不绑定他们本身的this
,但能够看到他们周围(定义位置)做用域的this
绑定。 所以,你能够像下面的代码那样,在局部函数中引用this
:
function normalize() { console.log(this.coords.map(n => n / this.length)); } normalize.call({coords: [0, 2, 3], length: 5}); // → [0, 0.4, 0.6]
若是我使用function
关键字将参数写入map
,则代码将不起做用。
咱们来仔细看看如下这段代码。
let empty = {}; console.log(empty.toString); // → function toString(){…} console.log(empty.toString()); // → [object Object]
我从一个空对象中取出了一个属性。 好神奇!
实际上并不是如此。我只是掩盖了一些 JavaScript 对象的内部工做细节罢了。每一个对象除了拥有本身的属性外,都包含一个原型(prototype)。原型是另外一个对象,是对象的一个属性来源。当开发人员访问一个对象不包含的属性时,就会从对象原型中搜索属性,接着是原型的原型,依此类推。
那么空对象的原型是什么呢?是Object.prototype
,它是全部对象中原型的父原型。
console.log(Object.getPrototypeOf({}) == Object.prototype); // → true console.log(Object.getPrototypeOf(Object.prototype)); // → null
正如你的猜想,Object.getPrototypeOf
返回一个对象的原型。
JavaScript 对象原型的关系是一种树形结构,整个树形结构的根部就是Object.prototype
。Object.prototype
提供了一些能够在全部对象中使用的方法。好比说,toString
方法能够将一个对象转换成其字符串表示形式。
许多对象并不直接将Object.prototype
做为其原型,而会使用另外一个原型对象,用于提供一系列不一样的默认属性。函数继承自Function.prototype
,而数组继承自Array.prototype
。
console.log(Object.getPrototypeOf(Math.max) == Function.prototype); // → true console.log(Object.getPrototypeOf([]) == Array.prototype); // → true
对于这样的原型对象来讲,其自身也包含了一个原型对象,一般状况下是Object.prototype
,因此说,这些原型对象能够间接提供toString
这样的方法。
你可使用Object.create
来建立一个具备特定原型的对象。
let protoRabbit = { speak(line) { console.log(`The ${this.type} rabbit says '${line}'`); } }; let killerRabbit = Object.create(protoRabbit); killerRabbit.type = "killer"; killerRabbit.speak("SKREEEE!"); // → The killer rabbit says 'SKREEEE!'
像对象表达式中的speak(line)
这样的属性是定义方法的简写。 它建立了一个名为speak
的属性,并向其提供函数做为它的值。
原型对象protoRabbit
是一个容器,用于包含全部兔子对象的公有属性。每一个独立的兔子对象(好比killerRabbit
)能够包含其自身属性(好比本例中的type
属性),也能够派生其原型对象中公有的属性。
JavaScript 的原型系统能够解释为对一种面向对象的概念(称为类(class))的某种非正式实现。 类定义了对象的类型的形状 - 它具备什么方法和属性。 这样的对象被称为类的实例(instance)。
原型对于属性来讲很实用。一个类的全部实例共享相同的属性值,例如方法。 每一个实例上的不一样属性,好比咱们的兔子的type
属性,须要直接存储在对象自己中。
因此为了建立一个给定类的实例,你必须使对象从正确的原型派生,可是你也必须确保,它自己具备这个类的实例应该具备的属性。 这是构造器(constructor)函数的做用。
function makeRabbit(type) { let rabbit = Object.create(protoRabbit); rabbit.type = type; return rabbit; }
JavaScript 提供了一种方法,来使得更容易定义这种类型的功能。 若是将关键字new
放在函数调用以前,则该函数将被视为构造器。 这意味着具备正确原型的对象会自动建立,绑定到函数中的this
,并在函数结束时返回。
构造对象时使用的原型对象,能够经过构造器的prototype
属性来查找。
function Rabbit(type) { this.type = type; } Rabbit.prototype.speak = function(line) { console.log(`The ${this.type} rabbit says '${line}'`); }; let weirdRabbit = new Rabbit("weird");
构造器(其实是全部函数)都会自动得到一个名为prototype
的属性,默认状况下它包含一个普通的,来自Object.prototype
的空对象。 若是须要,能够用新对象覆盖它。 或者,你能够将属性添加到现有对象,如示例所示。
按照惯例,构造器的名字是大写的,这样它们能够很容易地与其余函数区分开来。
重要的是,理解原型与构造器关联的方式(经过其prototype
属性),与对象拥有原型(能够经过Object.getPrototypeOf
查找)的方式之间的区别。 构造器的实际原型是Function.prototype
,由于构造器是函数。 它的prototype
属性拥有原型,用于经过它建立的实例。
console.log(Object.getPrototypeOf(Rabbit) == Function.prototype); // → true console.log(Object.getPrototypeOf(weirdRabbit) == Rabbit.prototype); // → true
因此 JavaScript 类是带有原型属性的构造器。 这就是他们的工做方式,直到 2015 年,这就是你编写他们的方式。 最近,咱们有了一个不太笨拙的表示法。
class Rabbit { constructor(type) { this.type = type; } speak(line) { console.log(`The ${this.type} rabbit says '${line}'`); } } let killerRabbit = new Rabbit("killer"); let blackRabbit = new Rabbit("black");
class
关键字是类声明的开始,它容许咱们在一个地方定义一个构造器和一组方法。 能够在声明的大括号内写入任意数量的方法。 一个名为constructor
的对象受到特别处理。 它提供了实际的构造器,它将绑定到名称"Rabbit"
。 其余函数被打包到该构造器的原型中。 所以,上面的类声明等同于上一节中的构造器定义。 它看起来更好。
类声明目前只容许方法 - 持有函数的属性 - 添加到原型中。 当你想在那里保存一个非函数值时,这可能会有点不方便。 该语言的下一个版本可能会改善这一点。 如今,你能够在定义该类后直接操做原型来建立这些属性。
像function
同样,class
能够在语句和表达式中使用。 当用做表达式时,它没有定义绑定,而只是将构造器做为一个值生成。 你能够在类表达式中省略类名称。
let object = new class { getWord() { return "hello"; } }; console.log(object.getWord()); // → hello
将属性添加到对象时,不管它是否存在于原型中,该属性都会添加到对象自己中。 若是原型中已经有一个同名的属性,该属性将再也不影响对象,由于它如今隐藏在对象本身的属性后面。
Rabbit.prototype.teeth = "small"; console.log(killerRabbit.teeth); // → small killerRabbit.teeth = "long, sharp, and bloody"; console.log(killerRabbit.teeth); // → long, sharp, and bloody console.log(blackRabbit.teeth); // → small console.log(Rabbit.prototype.teeth); // → small
下图简单地描述了代码执行后的状况。其中Rabbit
和Object
原型画在了killerRabbit
之下,咱们能够从原型中找到对象中没有的属性。
覆盖原型中存在的属性是颇有用的特性。就像示例展现的那样,咱们覆盖了killerRabbit
的teeth
属性,这能够用来描述实例(对象中更为泛化的类的实例)的特殊属性,同时又可让简单对象从原型中获取标准的值。
覆盖也用于向标准函数和数组原型提供toString
方法,与基本对象的原型不一样。
console.log(Array.prototype.toString == Object.prototype.toString); // → false console.log([1, 2].toString()); // → 1,2
调用数组的toString
方法后获得的结果与调用.join(",")
的结果十分相似,即在数组的每一个值之间插入一个逗号。而直接使用数组调用Object.prototype.toString
则会产生一个彻底不一样的字符串。因为Object
原型提供的toString
方法并不了解数组结构,所以只会简单地输出一对方括号,并在方括号中间输出单词"object"
和类型的名称。
console.log(Object.prototype.toString.call([1, 2])); // → [object Array]
咱们在上一章中看到了映射(map)这个词,用于一个操做,经过对元素应用函数来转换数据结构。 使人困惑的是,在编程时,同一个词也被用于相关而不一样的事物。
映射(名词)是将值(键)与其余值相关联的数据结构。 例如,你可能想要将姓名映射到年龄。 为此可使用对象。
let ages = { Boris: 39, Liang: 22, Júlia: 62 }; console.log(`Júlia is ${ages["Júlia"]}`); // → Júlia is 62 console.log("Is Jack's age known?", "Jack" in ages); // → Is Jack's age known? false console.log("Is toString's age known?", "toString" in ages); // → Is toString's age known? true
在这里,对象的属性名称是人们的姓名,而且该属性的值为他们的年龄。 可是咱们固然没有在咱们的映射中列出任何名为toString
的人。 似的,由于简单对象是从Object.prototype
派生的,因此它看起来就像拥有这个属性。
所以,使用简单对象做为映射是危险的。 有几种可能的方法来避免这个问题。 首先,可使用null
原型建立对象。 若是将null
传递给Object.create
,那么所获得的对象将不会从Object.prototype
派生,而且能够安全地用做映射。
console.log("toString" in Object.create(null)); // → false
对象属性名称必须是字符串。 若是你须要一个映射,它的键不能轻易转换为字符串 - 好比对象 - 你不能使用对象做为你的映射。
幸运的是,JavaScript 带有一个叫作Map
的类,它正是为了这个目的而编写。 它存储映射并容许任何类型的键。
let ages = new Map(); ages.set("Boris", 39); ages.set("Liang", 22); ages.set("Júlia", 62); console.log(`Júlia is ${ages.get("Júlia")}`); // → Júlia is 62 console.log("Is Jack's age known?", ages.has("Jack")); // → Is Jack's age known? false console.log(ages.has("toString")); // → false
set
,get
和has
方法是Map
对象的接口的一部分。 编写一个能够快速更新和搜索大量值的数据结构并不容易,但咱们没必要担忧这一点。 其余人为咱们实现,咱们能够经过这个简单的接口来使用他们的工做。
若是你确实有一个简单对象,出于某种缘由须要将它视为一个映射,那么了解Object.keys
只返回对象的本身的键,而不是原型中的那些键,会颇有用。 做为in
运算符的替代方法,你可使用hasOwnProperty
方法,该方法会忽略对象的原型。
console.log({x: 1}.hasOwnProperty("x")); // → true console.log({x: 1}.hasOwnProperty("toString")); // → false
当你调用一个对象的String
函数(将一个值转换为一个字符串)时,它会调用该对象的toString
方法来尝试从它建立一个有意义的字符串。 我提到一些标准原型定义了本身的toString
版本,所以它们能够建立一个包含比"[object Object]"
有用信息更多的字符串。 你也能够本身实现。
Rabbit.prototype.toString = function() { return `a ${this.type} rabbit`; }; console.log(String(blackRabbit)); // → a black rabbit
这是一个强大的想法的简单实例。 当一段代码为了与某些对象协做而编写,这些对象具备特定接口时(在本例中为toString
方法),任何类型的支持此接口的对象均可以插入到代码中,而且它将正常工做。
这种技术被称为多态(polymorphism)。 多态代码能够处理不一样形状的值,只要它们支持它所指望的接口便可。
我在第四章中提到for/of
循环能够遍历几种数据结构。 这是多态性的另外一种状况 - 这样的循环指望数据结构公开的特定接口,数组和字符串是这样。 你也能够将这个接口添加到你本身的对象中! 但在咱们实现它以前,咱们须要知道什么是符号。
多个接口可能为不一样的事物使用相同的属性名称。 例如,我能够定义一个接口,其中toString
方法应该将对象转换为一段纱线。 一个对象不可能同时知足这个接口和toString
的标准用法。
这是一个坏主意,这个问题并不常见。 大多数 JavaScript 程序员根本就不会去想它。 可是,语言设计师们正在思考这个问题,不管如何都为咱们提供了解决方案。
当我声称属性名称是字符串时,这并不彻底准确。 他们一般是,但他们也能够是符号(symbol)。 符号是使用Symbol
函数建立的值。 与字符串不一样,新建立的符号是惟一的 - 你不能两次建立相同的符号。
let sym = Symbol("name"); console.log(sym == Symbol("name")); // → false Rabbit.prototype[sym] = 55; console.log(blackRabbit[sym]); // → 55
将Symbol
转换为字符串时,会获得传递给它的字符串,例如,在控制台中显示时,符号能够更容易识别。 但除此以外没有任何意义 - 多个符号可能具备相同的名称。
因为符号既独特又可用于属性名称,所以符号适合定义能够和其余属性共生的接口,不管它们的名称是什么。
const toStringSymbol = Symbol("toString"); Array.prototype[toStringSymbol] = function() { return `${this.length} cm of blue yarn`; }; console.log([1, 2].toString()); // → 1,2 console.log([1, 2][toStringSymbol]()); // → 2 cm of blue yarn
经过在属性名称周围使用方括号,能够在对象表达式和类中包含符号属性。 这会致使属性名称的求值,就像方括号属性访问表示法同样,这容许咱们引用一个持有该符号的绑定。
let stringObject = { [toStringSymbol]() { return "a jute rope"; } }; console.log(stringObject[toStringSymbol]()); // → a jute rope
提供给for/of
循环的对象预计为可迭代对象(iterable)。 这意味着它有一个以Symbol.iterator
符号命名的方法(由语言定义的符号值,存储为Symbol
符号的一个属性)。
当被调用时,该方法应该返回一个对象,它提供第二个接口迭代器(iterator)。 这是执行迭代的实际事物。 它拥有返回下一个结果的next
方法。 这个结果应该是一个对象,若是有下一个值,value
属性会提供它;没有更多结果时,done
属性应该为true
,不然为false
。
请注意,next
,value
和done
属性名称是纯字符串,而不是符号。 只有Symbol.iterator
是一个实际的符号,它可能被添加到不一样的大量对象中。
咱们能够直接使用这个接口。
let okIterator = "OK"[Symbol.iterator](); console.log(okIterator.next()); // → {value: "O", done: false} console.log(okIterator.next()); // → {value: "K", done: false} console.log(okIterator.next()); // → {value: undefined, done: true}
咱们来实现一个可迭代的数据结构。 咱们将构建一个matrix
类,充当一个二维数组。
class Matrix { constructor(width, height, element = (x, y) => undefined) { this.width = width; this.height = height; this.content = []; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { this.content[y * width + x] = element(x, y); } } } get(x, y) { return this.content[y * this.width + x]; } set(x, y, value) { this.content[y * this.width + x] = value; } }
该类将其内容存储在width × height
个元素的单个数组中。 元素是按行存储的,所以,例如,第五行中的第三个元素存储在位置4 × width + 2
中(使用基于零的索引)。
构造器须要宽度,高度和一个可选的内容函数,用来填充初始值。 get
和set
方法用于检索和更新矩阵中的元素。
遍历矩阵时,一般对元素的位置以及元素自己感兴趣,因此咱们会让迭代器产生具备x
,y
和value
属性的对象。
class MatrixIterator { constructor(matrix) { this.x = 0; this.y = 0; this.matrix = matrix; } next() { if (this.y == this.matrix.height) return {done: true}; let value = {x: this.x, y: this.y, value: this.matrix.get(this.x, this.y)}; this.x++; if (this.x == this.matrix.width) { this.x = 0; this.y++; } return {value, done: false}; } }
这个类在其x
和y
属性中跟踪遍历矩阵的进度。 next
方法最开始检查是否到达矩阵的底部。 若是没有,则首先建立保存当前值的对象,以后更新其位置,若有必要则移至下一行。
让咱们使Matrix
类可迭代。 在本书中,我会偶尔使用过后的原型操做来为类添加方法,以便单个代码段保持较小且独立。 在一个正常的程序中,不须要将代码分红小块,而是直接在class
中声明这些方法。
Matrix.prototype[Symbol.iterator] = function() { return new MatrixIterator(this); };
如今咱们能够用for/of
来遍历一个矩阵。
let matrix = new Matrix(2, 2, (x, y) => `value ${x},${y}`); for (let {x, y, value} of matrix) { console.log(x, y, value); } // → 0 0 value 0,0 // → 1 0 value 1,0 // → 0 1 value 0,1 // → 1 1 value 1,1
接口一般主要由方法组成,但也能够持有非函数值的属性。 例如,Map
对象有size
属性,告诉你有多少个键存储在它们中。
这样的对象甚至不须要直接在实例中计算和存储这样的属性。 即便直接访问的属性也可能隐藏了方法调用。 这种方法称为读取器(getter),它们经过在方法名称前面编写get
来定义。
let varyingSize = { get size() { return Math.floor(Math.random() * 100); } }; console.log(varyingSize.size); // → 73 console.log(varyingSize.size); // → 49
每当有人读取此对象的size
属性时,就会调用相关的方法。 当使用写入器(setter)写入属性时,能够作相似的事情。
class Temperature { constructor(celsius) { this.celsius = celsius; } get fahrenheit() { return this.celsius * 1.8 + 32; } set fahrenheit(value) { this.celsius = (value - 32) / 1.8; } static fromFahrenheit(value) { return new Temperature((value - 32) / 1.8); } } let temp = new Temperature(22); console.log(temp.fahrenheit); // → 71.6 temp.fahrenheit = 86; console.log(temp.celsius); // → 30
Temperature
类容许你以摄氏度或华氏度读取和写入温度,但内部仅存储摄氏度,并在fahrenheit
读写器中自动转换为摄氏度。
有时候你想直接向你的构造器附加一些属性,而不是原型。 这样的方法将没法访问类实例,但能够用来提供额外方法来建立实例。
在类声明内部,名称前面写有static
的方法,存储在构造器中。 因此Temperature
类可让你写出Temperature.fromFahrenheit(100)
,来使用华氏温度建立一个温度。
已知一些矩阵是对称的。 若是沿左上角到右下角的对角线翻转对称矩阵,它保持不变。 换句话说,存储在x,y
的值老是与y,x
相同。
想象一下,咱们须要一个像Matrix
这样的数据结构,可是它必需保证一个事实,矩阵是对称的。 咱们能够从头开始编写它,但这须要重复一些代码,与咱们已经写过的代码很类似。
JavaScript 的原型系统能够建立一个新类,就像旧类同样,可是它的一些属性有了新的定义。 新类派生自旧类的原型,但为set
方法增长了一个新的定义。
在面向对象的编程术语中,这称为继承(inheritance)。 新类继承旧类的属性和行为。
class SymmetricMatrix extends Matrix { constructor(size, element = (x, y) => undefined) { super(size, size, (x, y) => { if (x < y) return element(y, x); else return element(x, y); }); } set(x, y, value) { super.set(x, y, value); if (x != y) { super.set(y, x, value); } } } let matrix = new SymmetricMatrix(5, (x, y) => `${x},${y}`); console.log(matrix.get(2, 3)); // → 3,2
extends
这个词用于表示,这个类不该该直接基于默认的Object
原型,而应该基于其余类。 这被称为超类(superclass)。 派生类是子类(subclass)。
为了初始化SymmetricMatrix
实例,构造器经过super
关键字调用其超类的构造器。 这是必要的,由于若是这个新对象的行为(大体)像Matrix
,它须要矩阵具备的实例属性。 为了确保矩阵是对称的,构造器包装了content
方法,来交换对角线如下的值的坐标。
set
方法再次使用super
,但此次不是调用构造器,而是从超类的一组方法中调用特定的方法。 咱们正在从新定义set
,可是想要使用原来的行为。 由于this.set
引用新的set
方法,因此调用这个方法是行不通的。 在类方法内部,super
提供了一种方法,来调用超类中定义的方法。
继承容许咱们用相对较少的工做,从现有数据类型构建稍微不一样的数据类型。 它是面向对象传统的基础部分,与封装和多态同样。 尽管后二者如今广泛被认为是伟大的想法,但继承更具争议性。
尽管封装和多态可用于将代码彼此分离,从而减小整个程序的耦合,但继承从根本上将类链接在一块儿,从而产生更多的耦合。 继承一个类时,比起单纯使用它,你一般必须更加了解它如何工做。 继承多是一个有用的工具,而且我如今在本身的程序中使用它,但它不该该成为你的第一个工具,你可能不该该积极寻找机会来构建类层次结构(类的家族树)。
instanceof
运算符在有些时候,了解某个对象是否继承自某个特定类,也是十分有用的。JavaScript 为此提供了一个二元运算符,名为instanceof
。
console.log( new SymmetricMatrix(2) instanceof SymmetricMatrix); // → true console.log(new SymmetricMatrix(2) instanceof Matrix); // → true console.log(new Matrix(2, 2) instanceof SymmetricMatrix); // → false console.log([1] instanceof Array); // → true
该运算符会浏览全部继承类型。因此SymmetricMatrix
是Matrix
的一个实例。 该运算符也能够应用于像Array
这样的标准构造器。 几乎每一个对象都是Object
的一个实例。
对象不只仅持有它们本身的属性。对象中有另外一个对象:原型,只要原型中包含了属性,那么根据原型构造出来的对象也就能够当作包含了相应的属性。简单对象直接以Object.prototype
做为原型。
构造器是名称一般以大写字母开头的函数,能够与new
运算符一块儿使用来建立新对象。 新对象的原型是构造器的prototype
属性中的对象。 经过将属性放到它们的原型中,能够充分利用这一点,给定类型的全部值在原型中分享它们的属性。 class
表示法提供了一个显式方法,来定义一个构造器及其原型。
你能够定义读写器,在每次访问对象的属性时秘密地调用方法。 静态方法是存储在类的构造器,而不是其原型中的方法。
给定一个对象和一个构造器,instanceof
运算符能够告诉你该对象是不是该构造器的一个实例。
可使用对象的来作一个有用的事情是,为它们指定一个接口,告诉每一个人他们只能经过该接口与对象通讯。 构成对象的其他细节,如今被封装在接口后面。
不止一种类型能够实现相同的接口。 为使用接口而编写的代码,自动知道如何使用提供接口的任意数量的不一样对象。 这被称为多态。
实现多个类,它们仅在一些细节上有所不一样的时,将新类编写为现有类的子类,继承其一部分行为会颇有帮助。
编写一个构造器Vec
,在二维空间中表示数组。该函数接受两个数字参数x
和y
,并将其保存到对象的同名属性中。
向Vec
原型添加两个方法:plus
和minus
,它们接受另外一个向量做为参数,分别返回两个向量(一个是this
,另外一个是参数)的和向量与差向量。
向原型添加一个getter
属性length
,用于计算向量长度,即点(x,y)
与原点(0,0)
之间的距离。
// Your code here. console.log(new Vec(1, 2).plus(new Vec(2, 3))); // → Vec{x: 3, y: 5} console.log(new Vec(1, 2).minus(new Vec(2, 3))); // → Vec{x: -1, y: -1} console.log(new Vec(3, 4).length); // → 5
标准的 JavaScript 环境提供了另外一个名为Set
的数据结构。 像Map
的实例同样,集合包含一组值。 与Map
不一样,它不会将其余值与这些值相关联 - 它只会跟踪哪些值是该集合的一部分。 一个值只能是一个集合的一部分 - 再次添加它没有任何做用。
写一个名为Group
的类(由于Set
已被占用)。 像Set
同样,它具备add
,delete
和has
方法。 它的构造器建立一个空的分组,add
给分组添加一个值(但仅当它不是成员时),delete
从组中删除它的参数(若是它是成员),has
返回一个布尔值,代表其参数是否为分组的成员。
使用===
运算符或相似于indexOf
的东西来肯定两个值是否相同。
为该类提供一个静态的from
方法,该方法接受一个可迭代的对象做为参数,并建立一个分组,包含遍历它产生的全部值。
// Your code here. class Group { // Your code here. } let group = Group.from([10, 20]); console.log(group.has(10)); // → true console.log(group.has(30)); // → false group.add(10); group.delete(10); console.log(group.has(10)); // → false
使上一个练习中的Group
类可迭代。 若是你不清楚接口的确切形式,请参阅本章前面迭代器接口的章节。
若是你使用数组来表示分组的成员,则不要仅仅经过调用数组中的Symbol.iterator
方法来返回迭代器。 这会起做用,但它会破坏这个练习的目的。
若是分组被修改时,你的迭代器在迭代过程当中出现奇怪的行为,那也没问题。
// Your code here (and the code from the previous exercise) for (let value of Group.from(["a", "b", "c"])) { console.log(value); } // → a // → b // → c
在本章前面我提到,当你想忽略原型的属性时,对象的hasOwnProperty
能够用做in
运算符的更强大的替代方法。 可是若是你的映射须要包含hasOwnProperty
这个词呢? 你将没法再调用该方法,由于对象的属性隐藏了方法值。
你能想到一种方法,对拥有本身的同名属性的对象,调用hasOwnProperty
吗?
let map = {one: true, two: true, hasOwnProperty: true}; // Fix this call console.log(map.hasOwnProperty("one")); // → true