Eloquent JavaScript #06# class
Notes
① 隐式传入this参数
function speak(line) { console.log(`The ${this.type} rabbit says '${line}'`); } let whiteRabbit = {type: "white", speak}; let hungryRabbit = {type: "hungry", speak};
② 通过call显式传入this参数
speak.call(hungryRabbit, "Burp!");
③ 对于箭头函数而言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关键词定义该函数然后传入,或者把箭头函数独立定义在外面,上面的代码都不会正常运行:
const f = n => n / this.length; function normalize() { console.log(this.coords.map(f)); } normalize.call({coords: [0, 2, 3], length: 5}); // → [NaN, Infinity, Infinity]
对象接到调用后首先搜索自身的属性,找不到就在原型中查找。
js中的原型构成一个三角关系,最根部是Object.prototype,而Function.prototype和Array.prototype则是两个分支。
console.log(Object.getPrototypeOf({}) == Object.prototype); // → true console.log(Object.getPrototypeOf(Object.prototype)); // → null console.log(Object.getPrototypeOf(Math.max) == Function.prototype); // → true console.log(Object.getPrototypeOf([]) == Array.prototype); // → true
可以用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方法是所有兔子共有的,而type属性则是杀手兔子私有的。
在java中兔子类的构造方法应该是这样的:
public class Rabbit { public String type; public void speak() {} // 构造器 public Rabbit(String type) { this.type = type; } }
然而在js里却要这样实现:
function makeRabbit(type) { let rabbit = Object.create(protoRabbit); rabbit.type = type; return rabbit; }
正常的兔子都应该通过该函数实例化,这样才可以保证兔子们共享speak,又具备自己独立的type。
js提供了一种方式让定义这类函数更加简单:
function Rabbit(type) { this.type = type; } Rabbit.prototype.speak = function(line) { console.log(`The ${this.type} rabbit says '${line}'`); }; let weirdRabbit = new Rabbit("weird");
事实上,js中的所有函数都具有一个prototype属性,它是一个派生自Object.prototyp的空对象。你可以用其它对象覆盖它,或者像上面那样改造它。构造函数、构造函数的prototype
属性指向的对象、通过构造函数创建的对象之间的关系如下:
PS. 独家制作↑
类实质上就是一个函数,而函数其实也是个对象。
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");
相对于3只是换了一种书写方式,且在class定义中只允许添加函数到原型。
匿名类:
let obj = new class { constructor(word) { this.word = word; } speak() { console.log('speak.'); } } ("hello"); obj.word; // → "hello" obj.speak(); // → speak.
类似java,接收到调用之后,js总是搜索当前对象,找不到才会逐个地搜索上一级原型对象,因此,如果子对象和父对象有同名的属性,那么父对象属性理所当然要被子对象的属性“覆盖”掉。
如果用对象实现map:
let obj = { a: "abc", b: "bca", c: "cab" };
然而它存在一些问题:
let obj = { a: "abc", b: "bca", c: "cab" }; console.log("a" in obj); // -> true console.log("toString" in obj); // -> true
原型对象的属性也会被作为键。针对这个问题,有两个比较容易的解决方案——创建没原型的对象:
let obj = Object.create(null); obj.a = "abc"; console.log("a" in obj); // -> true console.log("toString" in obj); // -> false
或者用Object.keys或者hasOwnProperty代替in,它们都不会把原型属性包含在范畴内。
不过还存在一个问题,对象的属性只能是字符串,这就意味着没有办法用对象作为键,因此最佳的方案还是采用js内建的Map类:
let myMap = new Map(); let obj1 = {}; let obj2 = {}; let obj3 = null; myMap.set(obj1, "a"); myMap.set(obj2, "b"); myMap.set(obj3, "c"); console.log(myMap.get(obj1)); // → a console.log(myMap.has(obj2)); // → true console.log(myMap.get(obj3)); // → c console.log(myMap.get(null)); // → c
参考文章:js-ES6学习笔记-Symbol
let sym = Symbol("name"); console.log(sym == Symbol("name")); // → false Rabbit.prototype[sym] = 55; console.log(blackRabbit[sym]); // → 55
/ 示例2
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
/ 示例3
let stringObject = { [toStringSymbol]() { return "a jute rope"; } }; console.log(stringObject[toStringSymbol]()); // → a jute rope
/ 示例4
let sy1 = Symbol("just a description"); let sy2 = Symbol("just a description"); console.log(sy1 == sy2); // → false let obj = { sy1: "a", [sy1]: "b", [sy2]: "c", sy3: Symbol("just a description") }; console.log(obj.sy1); // → a console.log(obj["sy1"]); // → a console.log(obj[sy1]); // → b console.log(Object.keys(obj)); // → ["sy1", "sy3"]
在你仅仅只是需要一个独一无二的符号而不论它是什么的时候可以用它。
优点:降低代码(由于语义产生的)耦合性。
注意点:symbol作为属性,必须像上面那样用方括号包含对symbol的绑定进行声明(方括号会导致计算),并且只能用方括号+对symbol的绑定访问那个属性。
所有可迭代(可以通过for
/of
遍历)对象其实都包含一个由Symbol.iterator定义的方法。该方法返回一个实现了next方法的(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}
自定义实现该接口的矩阵对象(课本实例):
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; } } 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}; } } Matrix.prototype[Symbol.iterator] = function() { return new MatrixIterator(this); }; 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
9、Getters, setters, and statics
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.fromFahrenheit(100)
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
封装和多态可以将增强代码的独立性,而继承却强化了类之间的关系,制造更多的纠纷。当采用继承的时候,你必须知道它是如何工作的,而不仅仅简单的去用它。继承是有用的工具,但不应当作为解决问题的首选。
function X() { } let obj = new X(); console.log(obj instanceof Object); // → true console.log(obj instanceof X); // → true
Exercises
class Vec { constructor(x, y) { this.x = x; this.y = y; } plus(v) { return new Vec(v.x + this.x, v.y + this.y); } minus(v) { return new Vec(this.x - v.x, this.y - v.y); } get length() { return Math.sqrt(this.x * this.x + this.y * this.y); } } 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
- -- - - - -- - - - - -- - - - - -- - - - -- - - - - -- - - - -- - - - - -- - - - -- - - -
class Group { // Your code here. constructor() { this.container = []; } static from(iterable) { let group = new Group(); for (let x of iterable) { group.add(x); } return group; } add(x) { if (!this.container.includes(x)) { this.container.push(x); } } delete(x) { this.container = this.container.filter(a => !(a === x)); } has(x) { return this.container.includes(x); } } 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
- -- - - - -- - - - - -- - - - - -- - - - -- - - - - -- - - - -- - - - - -- - - - -- - - -
实践证明,class和function一样可以无视在文本中定义的位置。
class Group { constructor() { this.container = []; } static from(iterable) { let group = new Group(); for (let x of iterable) { group.add(x); } return group; } add(x) { if (!this.container.includes(x)) { this.container.push(x); } } delete(x) { this.container = this.container.filter(a => !(a === x)); } has(x) { return this.container.includes(x); } [Symbol.iterator]() { return new GroupIterator(this); } } class GroupIterator { constructor(x) { this.index = 0; this.container = x.container; } next() { if (this.index === this.container.length) return {done: true}; let value = this.container[this.index++]; return {value, done: false}; } } for (let value of Group.from(["a", "b", "c"])) { console.log(value); } // → a // → b // → c
- -- - - - -- - - - - -- - - - - -- - - - -- - - - - -- - - - -- - - - - -- - - - -- - - -
let map = {one: true, two: true, hasOwnProperty: true}; // Fix this call console.log(Object.hasOwnProperty.call(map, "one");); // → true