From 6de3c02f662f4d71a33118070187f0603a78b0d5 Mon Sep 17 00:00:00 2001 From: wizardforcel <562826179@qq.com> Date: Fri, 4 May 2018 18:09:52 +0800 Subject: [PATCH] 6. --- 6.md | 312 +++++++++++++++++++++++++---------------------------------- 1 file changed, 132 insertions(+), 180 deletions(-) diff --git a/6.md b/6.md index 77ec09e..e08ec37 100644 --- a/6.md +++ b/6.md @@ -349,227 +349,179 @@ console.log(blackRabbit[sym]); // → 55 ``` -我们的表格绘制程序的构建函数会根据用户输入的宽度和高度来确定每列的宽度和每行的高度。接着,构建函数会根据之前输入的参数来绘制表格,并将最终结果用字符串输出。 +将`Symbol`转换为字符串时,会得到传递给它的字符串,例如,在控制台中显示时,符号可以更容易识别。 但除此之外没有任何意义 - 多个符号可能具有相同的名称。 -排版程序将会通过定义良好的接口来操作单元格对象。这样一来,程序就可以支持程序自定义的单元格类型。我们日后可以为程序增加新的单元格类型。比如说,如果想支持表头中的下划线单元格,我们不需要修改排版程序的源代码,只要这些单元格支持我们提供的接口即可。接口的定义如下: - -·minHeight():该方法返回一个数字,表示单元格所需的最小高度(行高)。 - -·minWidth():该方法返回一个数字,表示单元格所需的最小宽度(字符宽度)。 - -·draw(width,height):该方法返回一个数组,其长度为height,数组中每个元素是一个字符串,每个字符串宽度为width。该数组表示的是单元格内容。我会在程序中多次使用数组提供的高阶函数,这些函数能够为我们的程序提供良好的支持。 - -首先,程序会计算每列的最小宽度和每行的最大高度,并保存到数组中。变量rows是一个二维数组,其中的每个数组元素用来表示一个单元格组成的行。 +由于符号既独特又可用于属性名称,因此符号适合定义可以和其他属性共生的接口,无论它们的名称是什么。 ``` -function rowHeights(rows) { - return rows.map(function(row) { - return row.reduce(function(max, cell) { - return Math.max(max, cell.minHeight()); - }, 0); - }); -} - -function colWidths(rows) { - return rows[0].map(function(_, i) { - return rows.reduce(function(max, row) { - return Math.max(max, row[i].minWidth()); - }, 0); - }); -} +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 ``` -以下划线(_)开头或只包含单个下划线的变量名表示(告诉代码阅读者)代码中不会使用该参数。 - -rowHeights函数应该很好理解,该函数使用map逐行处理rows数组,在map的函数中使用reduce计算每个单元格数组中单元格的最大高度。 - -colWidths函数则稍微复杂一些,因为rows数组的数组元素表示一行,而非一列。我之前并未提到过map函数的第二个参数(forEach、filter以及其他类似的数组方法都有该参数),该参数表示当前元素的索引编号。colWidths函数调用了第一行单元格数组的map方法,并且只使用了map函数的第二个参数(表示列索引编号)来创建一个数组,数组中每个元素表示表格每一列的最大宽度。随后调用rows数组的reduce方法进行逐行遍历,计算出当前这一列单元格的最大宽度并填入map函数创建的数组中。 - -下面是绘制表格的代码: +通过在属性名称周围使用方括号,可以在对象表达式和类中包含符号属性。 这会导致属性名称的求值,就像方括号属性访问表示法一样,这允许我们引用一个持有该符号的绑定。 ``` -function drawTable(rows) { - var heights = rowHeights(rows); - var widths = colWidths(rows); +let stringObject = { + [toStringSymbol]() { return "a jute rope"; } +}; +console.log(stringObject[toStringSymbol]()); +// → a jute rope +``` - function drawLine(blocks, lineNo) { - return blocks.map(function(block) { - return block[lineNo]; - }).join(" "); +## 迭代器接口 + +提供给`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); + } + } } - - function drawRow(row, rowNum) { - var blocks = row.map(function(cell, colNum) { - return cell.draw(widths[colNum], heights[rowNum]); - }); - return blocks[0].map(function(_, lineNo) { - return drawLine(blocks, lineNo); - }).join("\n"); + get(x, y) { + return this.content[y * this.width + x]; } - - return rows.map(drawRow).join("\n"); -} -``` - -drawTable函数中使用了一个辅助函数drawRow来绘制表格的行,然后将每一行绘制好表格用换行符链接起来。 - -drawRow函数先将行中的单元格对象转换成块(block),每个块是一个字符串数组,表示单元格内容,每个字符串表示单元格中的一行。一个只包含数字3776的单元格可能表示成一个单元素数组:[“3776”],而附加下划线的单元格需要两行,使用数组表示就是["name","---"]。 - -每行中所有块的高度需要保证一致,而且在最终的输出结果中,这些块应该彼此紧挨才对。drawRow中的第二个map调用会对最左侧的块进行映射,并通过map的第二个参数获得每一行的索引,接着使用drawLine函数绘制出每行(所有块中的某一行)的内容,逐行构建出最终输出。drawRow使用换行符来连接绘制好的每行文本,然后返回连接结果。 - -drawLine函数每次从所有块中提取出属于同一行的文本,并将彼此相邻的块用一个空格字符连接起来,这样可以在表格的每一列之间创建一个字符宽度的间隔。 - -现在,我们来编写用于创建文本单元格的构造器,实现表格的单元格接口。构造器使用split方法将字符串分割成数组,数组中每个元素是一行文本。我们可以给split方法指定一个字符串,每当遇到该字符串时,split方法会使用该参数分割字符串,并返回分割后的字符串片段数组。minWidth方法用于找出数组中字符串的最大宽度。 - -``` -function repeat(string, times) { - var result = ""; - for (var i = 0; i < times; i++) - result += string; - return result; -} - -function TextCell(text) { - this.text = text.split("\n"); -} -TextCell.prototype.minWidth = function() { - return this.text.reduce(function(width, line) { - return Math.max(width, line.length); - }, 0); -}; -TextCell.prototype.minHeight = function() { - return this.text.length; -}; -TextCell.prototype.draw = function(width, height) { - var result = []; - for (var i = 0; i < height; i++) { - var line = this.text[i] || ""; - result.push(line + repeat(" ", width - line.length)); + set(x, y, value) { + this.content[y * this.width + x] = value; } - return result; -}; -``` - -代码中使用了repeat这个辅助函数将string参数连接起来,重复的次数是times次。draw方法向每行文本中填充空格,使得每行文本都能满足最小长度的需求。 - -让我们来使用编写好的程序来创建一个5x5的棋盘。 - -``` -var rows = []; -for (var i = 0; i < 5; i++) { - var row = []; - for (var j = 0; j < 5; j++) { - if ((j + i) % 2 == 0) - row.push(new TextCell("##")); - else - row.push(new TextCell(" ")); - } - rows.push(row); } -console.log(drawTable(rows)); -// → ## ## ## -// ## ## -// ## ## ## -// ## ## -// ## ## ## ``` -成功了!由于每个单元格的尺寸都相同,因此我们的代码并不需要执行多么复杂的逻辑。 +该类将其内容存储在`width × height`个元素的单个数组中。 元素是按行存储的,因此,例如,第五行中的第三个元素存储在位置`4 × width + 2`中(使用基于零的索引)。 -你可以从本书网站中沙箱([http://eloquentjavascript.net/code/](http://eloquentjavascript.net/code/))的数据集列表下载有关山脉的数据,数据存储在MOUNTAINS变量中。 +构造函数需要宽度,高度和一个可选的内容函数,用来填充初始值。 `get`和`set`方法用于检索和更新矩阵中的元素。 + +遍历矩阵时,通常对元素的位置以及元素本身感兴趣,所以我们会让迭代器产生具有`x`,`y`和`value`属性的对象。 -现在我们准备在每个格子下方添加一系列破折号作为下划线,将包含列名在内的首行内容进行高亮。没问题,我们只需编写一个下划线类型的单元格即可。 ``` -function UnderlinedCell(inner) { - this.inner = inner; +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}; + } } -UnderlinedCell.prototype.minWidth = function() { - return this.inner.minWidth(); -}; -UnderlinedCell.prototype.minHeight = function() { - return this.inner.minHeight() + 1; -}; -UnderlinedCell.prototype.draw = function(width, height) { - return this.inner.draw(width, height - 1) - .concat([repeat("-", width)]); +``` + +这个类在其`x`和`y`属性中跟踪遍历矩阵的进度。 `next`方法最开始检查是否到达矩阵的底部。 如果没有,则首先创建保存当前值的对象,之后更新其位置,如有必要则移至下一行。 + +让我们使`Matrix`类可迭代。 在本书中,我会偶尔使用事后的原型操作来为类添加方法,以便单个代码段保持较小且独立。 在一个正常的程序中,不需要将代码分成小块,而是直接在`class`中声明这些方法。 + +``` +Matrix.prototype[Symbol.iterator] = function() { + return new MatrixIterator(this); }; ``` -下划线类型的单元格中会包含另一个单元格。该单元格的最小尺寸与内部单元格相同(直接调用内部单元格的minWidth和minHeight方法即可),只不过这里为了容纳一行下划线,需要将高度加1。 - -绘制这种单元格非常简单,我们只需要获取内部单元格的内容,并与一行全是破折号的字符串连接在一起即可。 - -既然已经支持带下划线的单元格,那么我们现在可以编写一个函数,根据我们的数据集构建出对应的表格。 +现在我们可以用`for/of`来遍历一个矩阵。 ``` -function dataTable(data) { - var keys = Object.keys(data[0]); - var headers = keys.map(function(name) { - return new UnderlinedCell(new TextCell(name)); - }); - var body = data.map(function(row) { - return keys.map(function(name) { - return new TextCell(String(row[name])); - }); - }); - return [headers].concat(body); +let matrix = new Matrix(2, 2, (x, y) => `value ${x},${y}`); +for (let {x, y, value} of matrix) { + console.log(x, y, value); } - -console.log(drawTable(dataTable(MOUNTAINS))); -// → name height country -// ------------ ------ ------------- -// Kilimanjaro 5895 Tanzania -// … etcetera +// → 0 0 value 0,0 +// → 1 0 value 1,0 +// → 0 1 value 0,1 +// → 1 1 value 1,1 ``` -标准Object.keys函数会返回一个数组,该数组中存储了对象中的所有属性名称。表格的首行需要包含带下划线的单元格,用于展示列名。在首行下方,我们将数据中的所有对象的值包装成一般的单元格,使用map来处理对象的keys数组,然后从对象中提取数据,确保每行单元格的列顺序相同。 +## 读写器和静态 -最后产生的表格与之前的示例类似,但是height列中的数字没有右对齐。我们稍后再去实现右对齐的功能。 +接口通常主要由方法组成,但也可以持有非函数值的属性。 例如,`Map`对象有`size`属性,告诉你有多少个键存储在它们中。 -### 6.10 Getter与Setter - -在定义接口的时候,我们可以在对象中增加一些非方法属性。我们可以定义minHeight和minWidth属性来存储数字值。但这就需要我们在构造器中加入计算高度与宽度的逻辑,这样会在构造器中添加与构造对象无关的代码,并导致一些问题。比如带下划线单元格的内部单元格改变时,带下划线单元格的尺寸也需要改变。 - -这就促使一些人采用了一种原则,即接口中的所有属性必须都是方法。我们并不直接访问简单的值属性,而是使用get和set方法读取修改属性。使用这种原则的缺点是我们需要额外编写大量用于修改和读取属性的方法。 - -幸运的是,JavaScript提供了一种技术,兼具两种方法的优点。我们可以像普通属性一样从外部访问属性,只不过将与属性关联的方法悄悄隐藏起来了。 +这样的对象甚至不需要直接在实例中计算和存储这样的属性。 即使直接访问的属性也可能隐藏了方法调用。 这种方法称为读取器(getter),它们通过在方法名称前面编写`get`来定义。 ``` -var pile = { - elements: ["eggshell", "orange peel", "worm"], - get height() { - return this.elements.length; - }, - set height(value) { - console.log("Ignoring attempt to set height to", value); +let varyingSize = { + get size() { + return Math.floor(Math.random() * 100); } }; - -console.log(pile.height); -// → 3 -pile.height = 100; -// → Ignoring attempt to set height to 100 +console.log(varyingSize.size); +// → 73 +console.log(varyingSize.size); +// → 49 ``` -在对象中,get或set方法用于指定属性的读取函数和修改函数,读取或修改属性时会自动调用这些函数。你也可以向已存在的对象中添加这类属性,比如使用Object.defineProperty函数向原型添加属性(这与我们之前使用该函数创建不可枚举属性是一样的)。 +每当有人读取此对象的`size`属性时,就会调用相关的方法。 当使用写入器(setter)写入属性时,可以做类似的事情。 + ``` -Object.defineProperty(TextCell.prototype, "heightProp", { - get: function() { return this.text.length; } -}); +class Temperature { + constructor(celsius) { + this.celsius = celsius; + } + get fahrenheit() { + return this.celsius * 1.8 + 32; + } + set fahrenheit(value) { + this.celsius = (value - 32) / 1.8; + } -var cell = new TextCell("no\nway"); -console.log(cell.heightProp); -// → 2 -cell.heightProp = 100; -console.log(cell.heightProp); -// → 2 + 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 ``` -类似的,你也可以在传递给defineProperty的对象中使用set属性指定设置器方法。当定义了获取器但未定义设置器时,JavaScript会简单地忽略所有的属性修改操作。 +`Temperature`类允许您以摄氏度或华氏度读取和写入温度,但内部仅存储摄氏度,并在`fahrenheit`读写器中自动转换为摄氏度。 -### 6.11 继承 +有时候你想直接向你的构造函数附加一些属性,而不是原型。 这样的方法将无法访问类实例,但可以用来提供额外方法来创建实例。 + +在类声明内部,名称前面写有`static`的方法,存储在构造函数中。 所以`Temperature`类可以让你写出`Temperature.fromFahrenheit(100)`,来使用华氏温度创建一个温度。 + +## 继承 我们还差一些工作才能完成绘制表格的程序。我们希望将数字列右对齐以增强表格的可读性。我们应该创建另一个类似于TextCell的单元格类型,只不过不是右侧填补空格,而是在左侧填补空格,这样就可以将内容右对齐。