Eloquent JavaScript 笔记 六:The Secret Life of Objects

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]);


这个写法看起来有些怪哈。但通过上面对apply的学习,还是比较容易理解的。 一定要记住这个写法,以后会经常遇到。


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。 


还记得上一章的那个map吗?我们把没一个event做为它的一个property存起来,然后,可以用 for .. in 来遍历,可以用in操作符访问。再把它拿来看一下:

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. Polymorphism
Rabbit.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);
};








  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值