1. object
var rabbit = {};
rabbit.speak = function(line) {
console.log("The rabbit says '" + line + "'");
};
rabbit.speak("I'm alive.");
2. this
function speak(line) {
console.log("The " + this.type + " rabbit says '" + line + "'");
}
var whiteRabbit = {type: "white", speak:speak};
var fatRabbit = {type: "fat", speak: speak};
whiteRabbit.speak("Oh my ears and whiskers, how late it's getting!");
fatRabbit.speak("I could sure use a carrot right now.");
3. apply & call
speak.apply(fatRabbit, ["Burp!"]);
speak.call(fatRabbit, ["Burp!"]);
//The fat rabbitsays 'Burp!'
speak.apply({type: "old"}, ["Oh my."]);
speak.call({type: "old"}, "Oh my.");
//The old rabbit says 'Oh my.'
调用speak函数的applay或者call功能时,传入的第一个参数做为speak函数中引用的this,如果speak函数中没有引用到this,那么第一个参数是没用的。
apply 和 call 的区别:apply 的第二个参数必须是个数组,其中包含了该function的所有参数。call 的第二个参数(或者更多)不必写成数组,后面不论有多少参数,都会传给该function。4. prototypes
prototype就相当于其他语言中的class。
JavaScript 共有六种数据类型: Number, Boolean, Array, Object, String, Null。除了Null,都有各自的prototype。用Object.getPrototypeOf( ) 看一下每种类型。可以看到,每种类型的prototype都是 Object.prototype 。或者说,Object.prototype是所有对象的基类。这类似于其他面向对象语言中的 inheritance,例如:C++。
var num = 10; 这里定义的num是个Number类型的对象(实例),Object.prototype 是基类,Number.prototype 是子类。
所以,本质上来讲,Number、Boolean和String都是Object。当访问一个变量的属性时,如果它本身没有这个属性,程序会自动搜索它的prototype,如果还没有,继续搜索它的基类的prototype。
Object.prototype
Object.getPrototypeOf({})
Function.prototype
Object.getPrototypeOf(isNaN)
Array.prototype
Object.getPrototypeOf([ ])
5. Object.create
利用object做为原型,创建新的对象。新的对象和原型对象有相同的属性。
var protoRabbit= {
speak: function(line) {
console.log("The " + this.type + " rabbit says '" + line + "'");
}
};
var killerRabbit = Object.create(protoRabbit);
killerRabbit.type = "killer";
killerRabbit.speak("SKREEEE!");
6. constructor
function Rabbit(type) {
this.type = type;
}
Rabbit.prototype.speak = function(line) {
console.log("The " + this.type + " rabbit says '" + line + "'");
}
var killerRabbit = new Rabbit("killer");
var blackRabbit = new Rabbit("black");
console.log(blackRabbit.type);
blackRabbit.speak("Doom ...");
JavaScript中没有定义class关键字。而是用function做为构造函数,构造函数不能有返回值,它默认返回一个对象(Rabbit.prototype类型的)。
7. Overriding Derived Properties
Rabbit.prototype.teeth = "small";
console.log(killerRabbit.teeth);
killerRabbit.teeth = "long, sharp, and bloody";
注意,这是对象层面的Overriding,不是子类的Overriding。
Array是Object.prototype的子类,它定义了自己的toString方法。这才是类层面的Overriding。
[1,2].toString();
// -> 1,2
Array.prototype.toString == Object.prototype.toString
// -> false
在一个Array上,调用Objet的toString方法。
Object.prototype.toString.call([1,2]);
8. Prototype Interference
给Rabbit.prototype添加method
Rabbit.prototype.dance = function() {
console.log("The " + this.type + " rabbit dances a jig.");
};
killerRabbit.dance();
这是给类定义成员函数的最通用的写法。
还是觉得有些别扭,因为,在C++中,通常把class定义命名为为 Rabbit 。但在js中 Rabbit.prototype 才是那个class。
var map = { };
function storePhi(event, phi) {
map[event] = phi;
}
storePhi("pizza", 0.068);
storePhi("touched tree", -0.081);
for( var name in map) {
console.log(name);
}
Object.prototype.nonsense = "hi";
for(var name in map) {
console.log(name);
}
"nonsense" in map
"toString" in map
delete Object.prototype.nonsense;
在13行,我们给Object.prototype增加了一个property,而Object.prototype是map的类,那么,map也就具有了nonsense这个property。当然,toString也是map的property。这可有点儿麻烦了,我们不能把event做为property存储在map中了吗? 先来看几个概念:
enumerable 属性:我们自己添加的属性,都是可枚举的。 用for(var name in map) 可以列举出来。
nonenumerable 属性:js本身(系统的)属性,是不可枚举的。
所以,Object.prototype.toString 不可枚举,但用in操作符可识别。Object.prototype.nonsense 可枚举,会干扰map的访问。
如何添加不可枚举的property?用Object.defineProperty( )
Object.defineProperty(Object.prototype, "hiddenNonsense",
{enumerable: false, value: "hi"});
for (var name in map)
console.log(name);
// → pizza
// → touched tree
console.log(map.hiddenNonsense);
// → hi
如何 判断是否自己的property,而不是"基类"的? hasOwnProperty( )
console.log(map.hasOwnProperty("toString"));
// → false
9. Prototype-less Objects
没有类的对象var map = Object.create(null);
map["pizza"] = 0.069;
console.log("toString" in map);
console.log("pizza" in map);
这个map对象没有 "类" ,它和Object.prototype没有任何关系。
10. PolymorphismRabbit.prototype.toString = function() {
return "I'm " + this.type + " rabbit.";
}
这是类层面的函数重载。
11. Laying Out a Table
这真不是一个好例子,里面大量用到了高阶函数,而且,作者叙述的顺序也不太合适,让我硬着头皮看了好久才明白。做为复习材料还是挺好的。
需求:给定一个表格的数据,把它按照如下格式打印到console。注意,是打印到console,不是html,console是等宽字体,所以所有的排版都是用空格和换行来实现。
name height country ------------ ------ ------------- Kilimanjaro 5895 Tanzania Everest 8848 Nepal Mount Fuji 3776 Japan Mont Blanc 4808 Italy/France Vaalserberg 323 Netherlands Denali 6168 United States Popocatepetl 5465 Mexico分析一下,输出的是个table,可以分成三个概念:行(row)、列(column)、单元格(cell)。要实现上面的对齐格式,我们需要知道每个cell的宽度,column的宽度就等于这一列最大的cell的宽度。还要注意,cell包含的字符串可能会换行,它的高度可能是多行,所以,row的高度等于这一行最高的那个cell的高度。
我们先来定义一个TextCell类:
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));
}
return result;
};
function repeat(string, times) {
var result = "";
for (var i = 0; i< times; i++) {
result += string;
}
return result;
}
注意几点:
1. draw函数并不是真正的绘制的屏幕上,而是生成一个二维数组,数组的每一行包含了这个cell的每一行文字。其中的每一行都需要用空格补齐到参数width指定的宽度。
2. minWidth和minHeight返回的数值是字符个数。不用考虑像素对齐,姑且认为console都是等宽字体。
3. this.text 保存的是二维数组,包含了cell的每一行数据。
接下来,把数据转换成TextCell,按照行/列存成一个二维数组:
var MOUNTAINS = [
{name: "Kilimanjaro", height: 5895, country: "Tanzania"},
{name: "Everest", height: 8848, country: "Nepal"},
{name: "Mount Fuji", height: 3776, country: "Japan"},
{name: "Mont Blanc", height: 4808, country: "Italy/France"},
{name: "Vaalserberg", height: 323, country: "Netherlands"},
{name: "Denali", height: 6168, country: "United States"},
{name: "Popocatepetl", height: 5465, country: "Mexico"}
];
function dataTable(data) {
var keys = Object.keys(data[0]);
var headers = keys.map(function (name) {
return new TextCell(new TextCell(name));
});
var body = data.map(function (row) {
return keys.map(function (name) {
var value = row[name];
return new TextCell(String(value));
});
});
return [headers].concat(body);
}
var rows = dataTable(MOUNTAINS);
接下来,计算每一行的高度,存成一个数组:
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);
});
}
以上代码有几点需要注意:
1. 外层循环用 i 来循环每一列。内层循环利用这个 i 作为列的编号,遍历每一行的第 i 列,获取cell的宽度,得到最大值。
2. 参数写成下划线,表示这个参数在函数体内不需要使用。(和swift语言一致哈。)
3. map, filter, forEach 等高阶函数都有第二个参数,是当前元素在数组中的index。
4. reduce的第二个参数是 0,别忘了写。因为,上一章讲到,这个参数是start,就是上面的函数第一次执行时的max。如果不写,默认是数组的第一个元素,而这里的数组元素是TextCell,不能用来和数字比大小。
最后,输出table:
function drawTable(rows) {
var heights = rowHeights(rows);
var widths = colWidths(rows);
function drawLine(blocks, lineNo) {
return blocks.map(function (block) {
return block[lineNo];
}).join(" ");
}
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");
}
return rows.map(drawRow).join("\n");
}
这里的 block 是TextCell的draw函数返回的二维数组,我们认为它是多行字符串。drawLine函数就是把一个row中多个cell的blocks一行一行连接起来,以换行符相连。
这个地方有些费解,需要仔细看,仔细看。
好了,在console下运行 drawTable(rows) 就可以打印出来table了。 哦,除了标题行的下划线。
要打印下划线,需要把标题那一行的cell 定义成一种新类型的cell: UnderlinedCell
function UnderlinedCell(inner) {
this.inner = inner;
}
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)]);
};
dataTable 也需要做相应修改:
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);
}
好了,这个例子终于完工了。
12. Getters and Setters
给一个对象添加getter和setter:
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);
}
};
console.log(pile.height);
// → 3
pile.height = 100;
// → Ignoring attempt to set height to 100
给一个类添加getter/setter:
Object.defineProperty(TextCell.prototype, "heightProp", {
get: function() { return this.text.length; }
});
var cell = new TextCell("no\nway");
console.log(cell.heightProp);
// → 2
cell.heightProp = 100;
console.log(cell.heightProp);
// → 2
13. Inheritance
继承TextCell,定义一个右对齐的Cell类型:
function RTextCell(text) {
TextCell.call(this, text); //调用父类的构造函数
}
RTextCell.prototype = Object.create(TextCell.prototype); // 继承父类,这一行是最关键的
RTextCell.prototype.draw = function(width, height) {
var result = [];
for (var i = 0; i < height; i++) {
var line = this.text[i] || "";
result.push(repeat(" ", width - line.length) + line);
}
return result;
};
14. instanceof
console.log(new RTextCell("A") instanceof RTextCell);
// → true
console.log(new RTextCell("A") instanceof TextCell);
// → true
console.log(new TextCell("A") instanceof RTextCell);
// → false
console.log([1] instanceof Array);
// → true
这个没什么好说的,所有面向对象语言都有这个操作符吧。
15. Exercise: A Vector Type
function Vector(x, y) {
this.x = x;
this.y = y;
}
Vector.prototype.plus = function (vec) {
return new Vector(this.x + vec.x, this.y + vec.y);
};
Vector.prototype.minus = function (vec) {
return new Vector(this.x - vec.x, this.y - vec.y);
};
Object.defineProperty(Vector.prototype, "length", {
get: function () {
return Math.sqrt(this.x*this.x + this.y*this.y);
}
});
16. Exercise: Another Cell
function StretchCell(inner, width, height) {
this.inner = inner;
this.minestWidth = width;
this.minestHeight = height;
}
StretchCell.prototype.minWidth = function () {
return Math.max(this.minestWidth, this.inner.minWidth());
};
StretchCell.prototype.minHeight = function () {
return Math.max(this.minestHeight, this.inner.minHeight());
};
StretchCell.prototype.draw = function(width, height) {
return this.inner.draw(width, height);
};