这部分其实应该叫做吐槽合集,作者在这部分大量吐槽了把 js 中原型理解成类的做法。现在的前端社区中,如 React,Vue 也是更少利用类的一些特性,而是使用了混入,类似于该书中讲的行为委托模式;而在 React Hooks 和 Vue composition api 中,则更激进的几乎完全去掉了 Class 风格的代码。
读了这部分,就能了解社区为什么不太喜欢 Class 风格的代码。
从 this 讲起
this
this 也是 js 中新手很头疼的问题了,各种归纳总结 this 的方法随处可见。这里也贴下书中对 this 的定义
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。
this 就是这个记录的一个属性,会在函数执行的过程中用到。
借助 this 我们可以很方便的来传递上下文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const Foo = { name: "Foo", speak() { console.log(this.name); }, };
Foo.speak();
const Boo = { name: "Boo", };
Foo.speak.call(Boo);
function speak(ctx) { console.log(ctx.name); }
speak(Foo); speak(Boo);
|
书里归纳了五种 this 的绑定方式,就以上面的例子为例
- 默认绑定 speak() this = window
- 隐式绑定 Foo.speak() this = Foo
- 显式绑定 Foo.speak.call(Boo) this = Boo
- 硬绑定 bind Foo.speak.bind(Boo) this = Boo
- new 绑定 new Foo.speak this = {}(es5)
当然,其实也可以用符合直觉的方式来理解,使用函数是手动传入 this,而在其它情况下,则是调用方,也就是指执行上下文,如果没有,则认为是 window。
可以看下一些经典的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const foo = { name: "foo", logThis: function () { console.log(this); }, };
foo.logThis();
const anthorLogThis = foo.logThis;
anthorLogThis();
function returnLogFunction() { return function () { console.log(this); }; }
returnLogFunction()();
(false || foo.logThis)();
|
这里可能最让人疑惑的可能是最后一个例子,他们看起来是具有执行上下文的
1
| (false || foo.logThis)();
|
这里要注意在使用了括号运算符后,返回的是函数的值,而返回的并不是执行上下文和函数的整体,也就自然变为了默认的 Window。
如果想具体如何判断 this,可以参考这篇博客
这篇博客从规范实现上面阐述如果判断 this,当然,绝大多数情况都可以以符合直觉的方式,判断它的调用方是谁就好了。
this 与属性描述符
属性描述符是 ES5 时出现的,可以给对象设置数据描述符和存取描述符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const o = {};
Object.defineProperty(o, "a", { value: 37, writable: true, enumerable: true, configurable: true, });
o.a;
const name = ""; Object.defineProperty(o, "name", { get: function () { console.log("get!"); return name; }, set: function (value) { console.log("set!"); name = value; }, });
|
在对象属性访问中,也有类似的[[GET]]与[[PUT]]。
默认的内置[[GET]]操作会首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性的值,如果没有找到,则会遍历原型链上的属性,如果没有则会返回 undefined,
而默认的[[PUT]]方法则由于原型链的存在复杂很多
在对象自身存在属性时,[[PUT]]方法很简单,如果有定义的 setter 就调用 setter,如果没有,就检查属性描述中定义是否可写,如果可写就写入。
而如果对象属性不存在,则会涉及到复杂的原型上属性的处理了。
- 如果原型上存在对应属性,并且是数据描述符,并且 writable 为 true,那么会直接在当前对象上添加对应的属性和值。
- 如果原型上存在对应属性,并且是数据描述符,如果 writable 为 flase 那么这次赋值默认会失败,严格模式下则会报错。
- 如果原型上存在对应属性,并且是存取描述符,则会调用对应的 setter
第二点类似于 java 中的 final 标识符,表示不可重写,然而可能大多数场景开发者会认为这是一个 BUG,因为结果出人意料。
类与原型
在很多基于类的语言中,类的继承行为都是复制的,对类的重新定义和运行时改写是不允许的,非常安全。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Person { private String name; private int age;
public String getName() {...} public void setName(String name) {...} public int getAge() {...} public void setAge(int age) {...} }
class Student extends Person { private int score;
public int getScore() { … } public void setScore(int score) { … } }
|
而在 js 中,类似的概念是以原型为基础的,在使用 new 关键字时,产生的对象会自动指向函数的[[prototype]]属性
1 2 3 4 5 6 7 8 9 10 11
| function Person(name, age) { this.name = name; this.age = age; }
Person.prototype.sayName = function () { console.log(this.name); };
const person = new Person("st", 34); person.sayName;
|
指的注意的是,由于 js 的动态性,原型也是可以更改的,并且所有属性是共享的,基于原型的方法会很脆弱
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function Person(name, age) { this.name = name; this.age = age; }
Person.prototype.sayName = function () { console.log(this.name); };
const person = new Person("st", 34); person.sayName;
Person.prototype.sayName = function () { console.log(this.name + "new"); }; person.sayName;
|
由于在 es6 之前,js 没有提供相关类的写法,就有各种对继承的实现,使用最多的就是寄身组合式继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| function Parent(name) { this.name = name; this.colors = ["red", "blue", "green"]; }
Parent.prototype.getName = function () { console.log(this.name); };
function Child(name, age) { Parent.call(this, name); this.age = age; }
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
var child1 = new Child("kevin", "18");
console.log(child1);
|
而在 ES6 中,继承只需要简单的 extnds 关键字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Parent { constructor(name) { this.name = name; this.colors = ["red", "blue", "green"]; }
getName() { console.log(this.name); } }
class Child extends Parent { constructor(name, age) { super(name); this.age = age; }
getName() { console.log(`${this.name} ${this.age}`); } }
const child = new Child("kk", 18); child.getName();
|
指的注意的是,super 是静态绑定的,super 在声明时就已经确定了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class P { foo() { console.log("P.foo"); } }
class C extends P { foo() { super.foo(); } }
const c1 = new C(); c1.foo();
const D = { foo() { console.log("D.foo"); }, };
const E = { foo: C.prototype.foo, };
Object.setPrototypeOf(E, D); E.foo();
|
实际上,在 Vue 和 React 的代码中,继承的代码很少被使用,更多倾向于使用混入来复用代码,比如 Vue 中的 混入代码
1 2 3 4 5 6 7 8 9
|
export function extend(to: Object, _from: ?Object): Object { for (const key in _from) { to[key] = _from[key]; } return to; }
|
混入可以更方便的组合多种对象,而不必实现复杂的继承关系。
社区
实际上社区也有很多基于类关系的框架和代码,比如 angular、nest,实际上它们工作、维护的相当好,并没有太过不堪,选择基于类、原型、函数式更多是一种编程风格的选择,都有或多或少的优点缺点。