没坏的话就不要修

这里讲的系统应该能够治愈。不过在现实场景中不太可能,即使可能在时效性上也不允许。

如果这里我们把这句话理解成:不要瞎折腾,没事找事,显然有一种消极的态度,如果一直安于现状,就会阻断创新。

从咨询师的角度来看更容易理解这句话,咨询师是系统的“外来物”,不是对于系统出现的所有问题都要横加干涉,适当的放任反而有益于系统的成长。可以借着下一句话来深入理解:

反复治疗一个可以治愈的系统最终让它不能治愈

引用书中的一句话就能轻松解释:

那些孩子四岁多了还帮忙擦鼻子的父母,以及靠为同一个客户反复解决同一个问题混饭吃的顾问,都应该牢记这个秘密。

那是那句话,不是都有问题都要横加干涉。否则最终会超出自己的能力范畴。

每个处方都包含两部分:药品和正确使用它的方法

我能把它理解成:“文档很重要,是产品的一部分”么?

我们在提供服务的时候,除了服务实体本身之外,还需提供使用说明。某些领域会提到以代码自注释能力来衡量代码的优劣,我觉得有一定道理但不是全部。

简单的系统确实可以做到轻松上手的目的,但也仅限于简单的系统。

如果已经做过的事情没能解决问题,就告诉他们做点别的

好打浑的说辞,不过确实有效。系统的某些问题往往出乎所有人意外,如果不能亲身实地地验证,仅仅依赖现有经验的人脑逻辑推断,确实不一定能够做出最终的诊断。

所以,在走投无路的时候,要学会接纳那些看似绝对无效的方案,说不定有意外的收获。

务必让他们付给你足够多的钱,这样他们才会照你说的去做

就像练习书法用廉价的草纸会比用昂贵的字帖更不容易练好一样,只有你付出的成本更高,你才会更加专注和认真,因为失败的损失更大。

有另一种基于责任归属的解释,如果用字帖还练不好书法,那么将无法将责任落在纸的种类身上。

因此,通过提高任务的成本和消耗,将更有可能让执行脉络贴近自己的设计。

来得早不如来得巧

时机很重要。

【伯登法则】要是你不能改掉缺点,就把它变成特点

这一法则将适用很多行业黑话,比如公司提供晚餐意味着加班更多。产品的特性从不同的视角可能会有截然相反的描述,核心原则就是避重就轻。这不一定就是一种欺骗,因为很可能那些未提及的缺陷对客户压根没有影响,或者打一个时间差,等客户发现了的时候,缺陷已经被修复,如果是可能甚至还可以是付费升级。

【镀金法则】要是没法当成特点来宣扬,那就冒充一下

这是“扒瞎”的意思吗?也许有作用,只要没人拆台和较真。对于聚集了大量耿直的人的行业中,可能风险性较大。

如果仅仅是电视广告在那里“镀金”的话,相信很少有人会真的打电话投诉;如果在一个小型的会议现场胡说八道,我想大概率会被“怼”。

这种缺乏货真价实干货的纯语言技巧,肯定不能撑太久。因此才会有下一句:

所有镀金的东西都得改正

eslint 早已取代 jslint 成为了 JavaScript 标准的风格检查工具,提供了大量规则(rule)。这些规则部分是可以支持 fixable 的,也就是说可以自动修复代码。但是现在来看,eslint 提供的修复功能还是太弱。因此 Facebook 开发了 prettier,一个专门司职代码格式化的工具,它不仅仅支持 JavaScript 语法,甚至还支持 Markdown。因此,把它们配合起来是一种自然而然的想法,首先我们有一个 eslint-config 定义,我们希望经过 format 之后的代码能100%通过 lint 校验。我们也希望在 git hook 中能自动 format 代码。

在实际应用中,让 prettier 完美配合 eslint 并不容易。根本原因在于 prettier 格式化代码的一些行为是不可配置的,而这些配置极有可能与 eslint 配置是冲突的。

例如下面的代码:

1
a === 1 || b === 2 || c ===3

在 eslint 中,我们经常配置在折行时把符号写在前边:

1
2
3
a === 1
|| b === 2
|| c ===3

但 prettier 则强行写在后面:

1
2
3
a === 1 ||
b === 2 ||
c ===3

prettier 提供了与 eslint 配合使用的官方方法。但这种方式的本质是使用 eslint-config-prettier 来抹平 prettier 与 eslint 之间不可调和的冲突。一旦我们定义的私有 eslint-config 与 eslint-config-prettier 有影响格式化结果的冲突,那么 lint 必然失败。不过这种方式的一大好处是仅仅对接 eslint 就能完成 lint 与 fix 的操作,集成性很强。

理解这种现象很容易,eslint 的 rules 是没有顺序的,因此在 fix 阶段,极有可能 prettier 的 rule 执行在最后阶段,结果显然与我们定义的最终 eslint 配置是冲突的。


相比于引用一系列 eslint-config,并维护它们的顺序,我们索性只维护我们自己的一份 eslint-config,反而更清晰。有这一份配置来执行 fix,产出一定可以通过lint。

但我们肯定不想直接抛弃 prettier,那么就把它的执行放在最前面,让它的产出再通过 eslint 即可。

已经存在这种现成的解决方案,就是 prettier-eslint。虽然看起来比较畸形,但可能是唯一一种比较能用且清晰的方案了。

但也有缺点,一旦将来某些 prettier 的不可配置的行为也不能被 eslint 所 fix,工作流就 gg 了。整合的过程就是这样,本来是相互分离的两个系统,让它们一起工作,总要失去点什么。

Code ➡️ prettier ➡️ eslint –fix ➡️ Formatted Code ✨

Q:如果能保证 prettier 提供的 rule 能先行执行呢?

继了解完“==”的原理后,再来了解一下加号“+”的 ECMAScript 实现原理。

例如:

1
a + b

第一步,对 a 和 b 执行 *toPrimitive()*,不指定 hint

toPrimitive(a) 为例,先看 a 有没有定义 Symbol.toPrimitive,比如:

1
2
3
4
5
6
7
8
9
10
11
12

a[Symbol.toPrimitive] = customToPrimitive;
function customToPrimitive(hint) {
switch (hint) {
case 'number':
...
case 'string':
...
default: // 'default'
}
};

如果定义了 Symbol.toPrimitive 属性,则执行:

1
customToPrimitive.call(a, 'default')

否则执行原生 *toPrimitive(“number”)*,也就是要依次调用 a 的 valueOftoString 方法。

如果执行 a.valueOf() 返回不是 Object,则为 toPrimitive(a) 的终值,否则执行 *a.toString()*。

接下来,看 a 有没有定义 toString,如果有,则执行 *a.toString()*,为 toPrimitive(a) 的终值。

如果没有,则执行原型链顶端的 Object.prototype.toString,这首先要看 a 有没有定义 Symbol.toStringTag

如果定义了,比如:

1
2
3
4
5
6
7
8

function customStringTag() {
...
}

Object.defineProperty(a, Symbol.toStringTag, {
get: customStringTag
});

那么返回 *”[object “ + customStringTag.apply(a) + “]”*,否则返回 *”[object Object]”*,为 toPrimitive(a) 的终值。

如果发现 toPrimitive(a) 或者 toPrimitive(b) 任一为字符串,则执行字符串拼接,否则执行数字加法,即 *toNumber(a) + toNumber(b)*,这也能解释 true + true = 2 的问题。


可见 a + b 核心就是 toPrimitive 操作,只不过从 ES2015 以来,toPrimitive 受到 Symbol 的影响,变得越来越复杂。

相比之下,减号“-”就很简单了,就是执行 *toNumber(a) - toNumber(b)*。

对于社会主义的中国人来讲,经过初中、高中、大学的思想政治课的洗礼,理解矛盾并非难事,我们辩证唯物主义的内容要更丰富得多。

第二章 培养矛盾的思维框架

不要理性,要合理

这两个相似的词忽然让我很难揣摩这句只包含6个字的话。什么样的事情是合理但不理性的呢?我理解,理性是对事件真实客观的描述,是能用逻辑严格证明的,往往获取“理性”需要很大的成本,甚至在有意义的时间内无法获取;而合理则是一种模棱两可的适当妥协,在有些事情无法获取“理性”之时,委曲求全的权宜之计都可能是一种合理。合理是一种“反牛角尖的”行为,似乎更倾向于“糊弄”。

在作者涉猎的软件咨询行业,很难想象不求甚解的方案也能行得通。从技术上来讲,确实再隐晦的故障都是可以绝对定位的,但对于项目来说,往往最初的设计和真正的落地会有很大差距,如果没有绝对的经验积累,那么项目在循循渐进的过程中往往都会带着猜的成份。换句话说,没有理性,只要比上一版本又说改进,即为合理。从这角度上来说,一味得追求最佳既是不可能的任务,也不能称之为理性的抉择,毕竟世界是个矛盾体,在矛盾中寻求理性的过程收获利益,才是上上之选。

从这里可以衍生出一种经典的产品进化模型,比如想造一辆汽车,下面有两种方案:

  1. 第一种,先造轮子、发动机、车架,最终拼接成完善的汽车;
  2. 第二种,先造一辆自行车、摩托车、三轮车,最后一辆汽车

第二种方案看似费时费力,往往更容易被人接受,也更容易实现。

自以为无所不知的人最容易上当

过于自信就是自负,往往容易对客观事实视而不见。这和“撞车的都是老司机”、“淹死的都是会水的”是类似的道理。

生活太重要,所以不能太较真

这论题太大了,似乎已经上升到了哲学和三观的高度上来了。我想作者要表达的意思应该是,对于无法解释的事物不必过于在意,毕竟你的无知正是矛盾世界里的必不可少的成份。

不付出就什么也得不到

正是矛盾的合理体现,要想得到什么,不可避免地要失去什么,当然,反过来不一定成立,往往都不成立。

提升一方面,就要牺牲另一方面

同上

费舍基本定理——你越适应现状,就越难适应变化

显然,与现有系统耦合越大,与其它系统兼容越小。抽象与具象。

伴随着年纪的增长,我觉得自己就越来越难以适应变化。

顾问一般在解决你提出的第三个问题时最有成效

这也解释了为什么人在20多岁的时候最富创造性,因为年老后才更“善于”和这个“平庸”的世界打交道。

始终关注自己在什么时候拥有最高效的进步,在合适的时候,应当勇于舍弃舒适的温床。

我确实应该换个工作了🍟

我们能做,这是所需的费用

没有成本的改变是不现实的,懂得这个道理,避免在愚蠢的任务上浪费时间与口舌。

元素在文档流中的位置应与页面滚动量无关,指的是该元素左上角(包括border,但不包括margin)距离整个页面左上角的水平和垂直位置。

获取此位置有两种方法,观察 jQuery 2.2 源码

1
2
3
4
5
6
var rect = elem.getBoundingClientRect();
var win = elem.ownerDocument.defaultView;
return {
top: rect.top + win.pageYOffset,
left: rect.left + win.pageXOffset
};

getBoundingClientRect 是一个十分高效的方法,用来获取元素相对于可见的视口的位置,而这个位置是与滚动量有关的,只有这个位置加上滚动量,即是与文档左上角的距离。

如果 getBoundingClientRect 不存在,我们依然可以通过遍历计算出结果。首先需要了解 DOM 元素的几个属性:

  1. offsetParent 向上祖先中第一个定位元素;
  2. offsetLeft 距离 offsetParent 左边界的水平位置,与滚动量无关
  3. offsetTop 距离 offsetParent 上边界的垂直位置,与滚动量无关
  4. clientLeft 一般为左边框宽度
  5. clientTop 一般为上边框宽度
  6. scrollLeft 水平滚动距离
  7. scrollTop 垂直滚动距离

因为我们要得到的距离是当时的绝对距离,与该元素的各个祖先元素的滚动量是有关的,因此我们不能简单地通过加和 offsetLeftoffsetTop 来得到最后的值,必须减去每一级祖先的滚动量。

具体逻辑大概就是,向该元素的上面遍历,减去每个元素的滚动量,一旦遇到是 offsetParent,则加上 offsetLeftoffsetTop。具体可参考 jQuery 1.4.4 源码。简单来讲就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function offset(ele) {
var top = ele.offsetTop;
var left = ele.offsetLeft;

var offsetParent = ele.offsetParent;

while((ele = ele.parentNode) && ele !== document) {
left -= (ele.scrollLeft);
top -= (ele.scrollTop);
if (offsetParent === ele) {
top += ele.offsetTop + ele.clientTop;
left += ele.offsetLeft + ele.clientLeft;
offsetParent = offsetParent.offsetParent;
}
}

return {top,left};
}

当然 clientTop 与 clientLeft 并非永远代表的是上边框和左边框的宽度。

上一篇我们熟悉了一下 Inferno.js 的 DOM Diff 算法。今天我们来看 Vue.js 框架的 DOM Diff 算法。

Vue.js 的作者没有编写自己的算法,而是使用了 Snabbdom,并做了适当的修改。

DOM Diff 的关键算法在 https://github.com/snabbdom/snabbdom/blob/v0.7.1/src/snabbdom.ts#L179

同样,我们依然假设有原始的 DOM 集合A为 “dfibge”,更新后的集合B为 “igfheb”。

创建4个指针 oldStartIdxoldEndIdxnewStartIdxnewEndIdx,初始分别指向A的起始点、结束点和B的起始点、结束点。显然:

1
2
3
4
oldStartIdx=0
oldEndIdx=5
newStartIdx=0
newEndIdx=5

第一步,比较 A[oldStartIdx] 和 B[newStartIdx],如果相同,则 oldStartIdx++、newStartIdx++。
第二步,比较 A[oldEndIdx] 和 B[newEndIdx],如果相同,则 oldEndIdx++、newEndIdx++。

在本例中,以上两步全部不满足,跳过。

第三步,比较 A[oldStartVnode] 和 B[newEndVnode],如果相同,则节点发生了右移。
第四步,比较 A[oldEndIdx] 和 B[newStartIdx],如果相同,则节点发生了左移。

在本例中,以上两步全部不满足,跳过。

第五步,在A中搜索 B[newStartIdx],即 i,找到则把A中的 i 移到 A[oldStartVnode] 前面并 newStartIdx++、oldStartVnode++,否则则创建它。

在本例中,A变成 idfbge

返回第一步。


可以看到这个算法类似于优化后的插入排序。按照此算法,A的变换路径为:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
A:dfibge
B:igfheb

=>

A:idfbge
B:igfheb
^

=>

A:igdfbe
B:igfheb
^

=>

A:igfdbe
B:igfheb
^

=>

A:igfhdbe
B:igfheb
^

=>

A:igfhedb
B:igfheb
^

=>

A:igfhebd
B:igfheb
^
=>

A:igfheb
B:igfheb
^

用一句话概括此算法的核心就是:依次遍历B集合,在A集合中找到对应项,放到与在B集合中相同的位置上。只不过 Snabbdom 使用了双向同时遍历来进行优化。

事实上,React 的 DOM Diff 算法与此也是非常类似的,只不过受限于 Fiber,只进行了单向搜索。但是即便如此, React 也引入了优化策略,尽量使得更多的元素不必移动。从本质上来看,Inferno 和 React 都利用递增子序列来进行了优化,但是 Inferno 使用算法来保证是最大递增子序列,而 React 的子序列是一定从第一个元素开始的,因此不一定是最大子序列。这在尾部元素移动到首部的时候,差异表现得更明显。

已经关注 Inferno.js 有两年的时间,终于在刚刚不久的过去建立起了官网和文档。

值得特别关注的是,Inferno.js 提供了类 React API 的同时,在 DOM Diff 算法上借鉴了 ivijs,取得了更高的效率,从而有较强的性能表现。

优秀的 DOM Diff 算法的核心都是在将一个 DOM 集合转换成另一个 DOM 集合的同时,尽可能地复用已有 DOM 并具有较少的 DOM 移动操作,这是由于 DOM 操作(甚至访问)的成本较高。

Inferno.js 在两个 VDom 之间进行比较,从而避免了频繁访问 DOM 的性能开销。下面我们看一下它实现的 DOM Diff 算法。

我们假设有原始的 DOM 集合A为 “dfibge”,更新后的集合B为 “igfheb”。

首先得到集合B中元素在A中的原始位置,如果在A中不存在则为-1,得到:

1
2
3
var A       = [d, f, i,  b, g, e];
var B = [i, g, f, h, e, b];
var sources = [2, 4, 1, -1, 5, 3];

现在我们获取该数组的最大递增子序列的序列位置,为:

1
var seq = [0, 1, 4];// [2, 4, 5]

这个数组的意义在于:“新数组中第0、1、4位置的元素在原始数组中是无需移动的”,这是我们能获取最小移动步数的关键。

一共6个成员,3个无需移动,那么一定有3个需要操作

现在我们从后往前遍历集合B,观察每个成员的位置是否存在于 seq 中,存在则不必操作,不存在则需移动。

首先我们需要先删除B中存在,但A中不存在的元素 d,此时:

1
A=[f, i, b, g, e]

观察B中最后一个元素:

1
2
3
A=[f, i, b, g, e]
B=[i, g, f, h, e, b]
^ pos=5

位置是5,不存在于 seq 中,需要把集合A中的 b 移到最后,此时:

1
A=[f, i, g, e, b] --- (1)

观察B中倒数第二个元素:

1
2
3
A=[f, i, b, g, e]
B=[i, g, f, h, e, b]
^ pos=4

位置是4,存在于 seq 中,无需操作。

观察B中倒数第三个元素:

1
2
3
A=[f, i, b, g, e]
B=[i, g, f, h, e, b]
^ pos=3

在A中不存在,我们需要创建 h,并放到 e 的前面,此时:

1
A=[f, i, g, h, e, b] --- (2)

观察B中倒数第四个元素:

1
2
3
A=[f, i, g, h, e, b]
B=[i, g, f, h, e, b]
^ pos=2

位置是2,不存在于 seq 中,需要把集合A中的 f 移到 h 的前面,此时:

1
A=[i, g, f, h, e, b] --- (3)

观察B中倒数第五个元素:

1
2
3
A=[i, g, f, h, e, b]
B=[i, g, f, h, e, b]
^ pos=1

位置是1,存在于 seq 中,无需任何操作。

观察B中倒数第六个元素,也是最后一个元素:

1
2
3
A=[f, i, b, g, e]
B=[i, g, f, h, e, b]
^ pos=0

位置是0,存在于seq中,也无需任何操作。

此时,所有操作都已结束,仅需3步,集合A已经转换成了集合B。其中,对 d 的移除和对 h 的创建是不可避免的,除此之外,仅进行了两次DOM移动,也印证了上面提到的 6-3=3 的操作步骤。

我们在移动 DOM 的时候都是执行的“插在XXX之前”,是因为 DOM 中的 insertBefore 方法,如果存在 insertAfter,那么从前往后操作也是等效的。

我们都知道 JavaScript 是通过原型链继承的,在不支持 class、extends 语法的环境里,继承又该如何实现呢?

首先我们来整理实现继承后的效果表征,假设存在两个类(函数)A 和 B,我们要实现 B 继承于 A,那么需要满足:

  1. new B() instanceof A 为真;
  2. B.prototype.constructor === B 为真;
  3. 如果 A.foo = 1,则 B.foo = 1 为真,即静态变量可继承;
  4. 在普通方法 foo 中可以调用 super.foo() 调用基类方法;
  5. 在构造方法中可以调用 super() 调用基类构造方法

在很多年前,jQuery 作者 John Resig 曾写过一个继承的实现,在那个还在 ES3 语法的时代已经非常难得。在今天看来,这种实现方式的语义已经远远落后,同时也不能实现静态变量。


下面我们来一步一步地去理解 babel 的实现方式。

首先定义两个类(函数),以及子类的实例:

1
2
3
4
function A() {}
function B() {}

var b = new B();

想实现 B 继承于 A,那边必然 b instanceof B 为真,根据 instanceof 的意义,一定有:

1
b.__proto__.__proto__... === A.prototype

我们知道:

1
b.__proto__ === B.prototype

那么我们只需要:

1
B.prototype.__proto__... === A.prototype

于是我们覆写 B.prototype

1
B.prototype = Object.create(A.prototype)

这样一定有 A.prototype.isPrototypeOf(B.prototype) 为真。

现在我们已经实现了:

1
b instanceof A === true

也就是说第一条我们已经实现了,下面看第二条,只需要:

1
B.prototype.constructor = B;

或者更优雅一些:

1
2
3
4
5
6
7
8
B.prototype = Object.create(A.prototype, {
constructor: {
value: B,
enumerable: false,
writable: true,
configurable: true
}
});

现在我们要实现静态成员的继承:

1
Object.setPrototypeOf(B, A);

目前为止,我们定义的是两个空类,现在我们为他们增加 name 属性:

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
28
29
30
31
32
33
34
35
36
37
38
39
function A() {}
function B() {}

A.prototype = Object.create(null, {
constructor: {
value: A,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
return 'A';
},
enumerable: true,
configurable: true
}
});

B.prototype = Object.create(A.prototype, {
constructor: {
value: B,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
return 'B';
},
enumerable: true,
configurable: true
}
});

Object.setPrototypeOf(B, A);

console.log(new A().name); // => A
console.log(new B().name); // => B

现在我们要在 B 的 name 中获取父类的 name 属性,那么我们就需要找到 B.prototype 中的 name。

在 B 环境中,灵活的做法是通过 B.prototype 找到 A.prototype,根据 *B.prototype = Object.create(A.prototype)*,我们知道:

1
A.prototype === Object.getPrototypeOf(B.prototype)

于是:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function A() {}
function B() {}

A.prototype = Object.create(null, {
constructor: {
value: A,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
return 'A';
},
enumerable: true,
configurable: true
}
});

B.prototype = Object.create(A.prototype, {
constructor: {
value: B,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
var sup = Object.getPrototypeOf(B.prototype);
var desc = Object.getOwnPropertyDescriptor(sup, 'name');
return desc.get.call(this) + 'B';
},
enumerable: true,
configurable: true
}
});

Object.setPrototypeOf(B, A);

console.log(new A().name); // => A
console.log(new B().name); // => AB

这就实现了 super.name 的效果。当然,我们假设 B 的父类是定义了 name 的,如果没有找到 name,那应该沿着原型链继续向上寻找。因此更健壮的写法是:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function A() {}
function B() {}

A.prototype = Object.create(null, {
constructor: {
value: A,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
return 'A';
},
enumerable: true,
configurable: true
}
});

B.prototype = Object.create(A.prototype, {
constructor: {
value: B,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
var sup = B.prototype, desc;
do {
sup = Object.getPrototypeOf(sup);
if (!sup) {
break;
}
desc = Object.getOwnPropertyDescriptor(sup, 'name');
} while(!desc);

return (desc ? desc.get.call(this) : undefined) + 'B';
},
enumerable: true,
configurable: true
}
});

Object.setPrototypeOf(B, A);

console.log(new A().name); // => A
console.log(new B().name); // => AB

这里没有考虑 name 不是 getter 的情形,不过原理类似,不再冗述。

最后我们来实现在构造方法中调用 super()。显然,实质是在 B 函数中找到函数 A,根据 *Object.setPrototypeOf(B, A)*,我们有:

1
2
3
function B() {
Object.getPrototypeOf(B).call(this)
}

我们为 A 添加一个 age 构造参数:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function A(age) {
this.age = age;
}
function B(age) {
Object.getPrototypeOf(B).call(this, age);
}

A.prototype = Object.create(null, {
constructor: {
value: A,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
return 'A';
},
enumerable: true,
configurable: true
}
});

B.prototype = Object.create(A.prototype, {
constructor: {
value: B,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
var sup = B.prototype, desc;
do {
sup = Object.getPrototypeOf(sup);
if (!sup) {
break;
}
desc = Object.getOwnPropertyDescriptor(sup, 'name');
} while(!desc);

return (desc ? desc.get.call(this) : undefined) + 'B';
},
enumerable: true,
configurable: true
}
});

Object.setPrototypeOf(B, A);

console.log(new B(18).age); // => 18

于是,一个基本的继承的手写版本就实现了,当然,babel 还考虑了更多细节,比如构造方法有返回值的情况等等,但基本的继承原理就是这个样子。

下图很好地反应了诸多对象的关系。值得一提的是, b 也可能成为下一级类的 prototype,你可以清晰地看到通过 __proto__ 搜索原型链的轨迹。

js-inherit


在现代浏览器的不断迭代下,已经有大部分版本都实现了对 class 关键字和继承的原生支持,相信在不久的将来,就无需在这么麻烦地实现继承了。无论如何,JavaScript 继承的原理仍然是不变的,上面这些内容有助于理解原型链是怎么样工作的。

开头一个问题是:什么是 Plain Object

并没有看到有官方去专门定义它,更可能它只不过是业界的一种通俗叫法,因此也没有严格的定义。但我们在汉语环境里通常叫它“纯对象”。

业界解释:https://www.quora.com/What-is-a-plainObject-in-JavaScript

下面我们来看一下常见 Library 对 isPlainObject 函数的实现。

jQuery

jQuery 3.3 版本中的 isPlainObject 定义在这里

为便于阅读,核心代码经过整理后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function isPlainObject(obj) {
var proto, Ctor;

// (1) null 肯定不是 Plain Object
// (2) 使用 Object.property.toString 排除部分宿主对象,比如 window、navigator、global
if (!obj || ({}).toString.call(obj) !== "[object Object]") {
return false;
}

proto = Object.getPrototypeOf(obj);

// 只有从用 {} 字面量和 new Object 构造的对象,它的原型链才是 null
if (!proto) {
return true;
}

// (1) 如果 constructor 是对象的一个自有属性,则 Ctor 为 true,函数最后返回 false
// (2) Function.prototype.toString 无法自定义,以此来判断是同一个内置函数
Ctor = ({}).hasOwnProperty.call(proto, "constructor") && proto.constructor;
return typeof Ctor === "function" && Function.prototype.toString.call(Ctor) === Function.prototype.toString.call(Object);
}

lodash

lodash 4.0.6 版本中的 isPlainObject 定义在这里

基本与 jQuery 版本相同,多了一个 Ctor instanceof Ctor 的条件,满足此条件的仅有 FunctionObject 两个函数。

1
2
3
4
5
6
7
8
9
10
11
function isPlainObject(value) {
if (!value || typeof value !== 'object' || ({}).toString.call(value) != '[object Object]' ) {
return false;
}
var proto = Object.getPrototypeOf(value);
if (proto === null) {
return true;
}
var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor;
return typeof Ctor == 'function' && Ctor instanceof Ctor && Function.prototype.toString.call(Ctor) === Function.prototype.toString.call(Object);
}

redux

redux 从 4.0.0 开始在测试中使用了 isPlainObject,代码在这里

它的实现比较简单。

1
2
3
4
5
6
7
8
9
10
function isPlainObject(obj) {
if (typeof obj !== 'object' || obj === null) return false

let proto = obj
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
// proto = null
return Object.getPrototypeOf(obj) === proto
}

我们并没有一个能判断 Plain Object 的清晰逻辑,大概能理出来的思路是:

  1. 先判断 obj 本身是否满足我们熟悉的合法对象概念;
  2. 判断 obj 的构造函数是不是 Object

至于判断 prototype 是不是 null,无非是一种 shortcut 罢了。

已经不关注社区有大半年了,或者起码有一年了,毕竟被持续增长的业务缠了也有一年了,虽然很“充实”,但毫无疑问地说,也是一无所获。

因为这一年来始终在 React Naitve 体系上工作,传统 Web 的知识也忘得大半了。还记得去年专门写过一篇文章来总结2016年前端社区的现状,一年过去了,如果我不是特别孤陋寡闻的话,那么,我认为,现状并没有改变多少。

框架

React

React 经历了一次许可证的“惊心动魄”,虽然新版本的许可证已经是安全的,但有一定研发实力的公司都已经在造自己的轮子。当然,从开发者的角度,肯定是使用成熟的方案更舒服,显然近在咫尺的理想也不见得能容易实现。

React 16 启用了新的引擎,这些细节是值得玩味的。

除此之外,React 依然拥有大量的开发者用户,就像去年一样。

Angular

未曾想到的是,Angular 2 还立足未稳,Angular 4 就出现了。刚刚看到一篇文章,作者抱怨自己的项目使用了 Angular 2 的 beta 开发之后,暂且不表 TypeScript 语法的绑定,光是要快速 follow 短时间内几十个版本的升级就已经相当心累了。

当然你可以说这位开发者在一开始就不应该使用 beta 版本,但是要等到正式版本发布的话,究竟是什么时候呢?我还记得 Angular 2 的核心包去掉了 beta 后缀的时候,其官方维护的一众功能包,比如动画等等,依旧是在 alpha/beta 状态。好吧,即使等到所有包都达到了起码的 stable 状态,Angular 4 的出现又是几个意思呢?

可以预见,Angular 1 时代的用户,相当一部分会放弃继续使用 Angular 的升级版本。曾经也一味推崇 Angular 2 的我,也渐渐失去了耐心。说到底,还是 Angular 的开发团队自己玩死的。

Vue

其实去年的这个时候,我是比较看衰 Vue 的,语法不喜欢、个人维护难以为继,和 React、Angular 甚至 Ember 完全没法比。

不过 Vue.js 的流行程度经过这一年来,毫无减弱的趋势,我想至少有一下几方面原因:

  1. 稳定,用户不必在业务迭代的同时,额外烦恼于持续升级基础框架;
  2. 简单,上手容易,具有普通开发经验的人,也可以在十分钟内写出一个较为复杂的应用;
  3. 生态工具较为丰富,特别是重要工具由作者维护,质量有保证且唯一,不用担心选择恐惧症

说白了,对于真正的业务开发环境,稳定和简单是特别重要的两个特性,不求能有多激进的特性,但求省心。

另外,Vue 的 ssr 方面也有足够的能力和性能支持,这在今天的 web 应用中,还是比较有吸引力的。React 生态也仅有一个简单的 next.js 还算勉强能用,而 Angular 和 Ember 基本都属于残次品。

Ember

Ember 属于比较小众的框架,我已经关注它有两年了,毕竟推广不多,很多人不知道,社区发展也有限。过去这一年依旧表现平平。

React-Native

如果说去年上半年我起码还在 React Native 上积累了点经验的话,那么下半年基本就达到了瓶颈了。许可证是一方面,公司环境是另一方面,但这都不重要。重要的是,我依旧不看好这个东西。

“行百里者半九十”,我们已经写了成千上万行代码,过亿的用户,以及良好的开发上线流程,但依旧避免不了要时刻面对框架底层缺陷带给我们的额外工作量与业务损失。作为前端开发者,过去面对浏览器兼容性起码还有前辈多年的经验积累,但 React Native 带给我们的就是大大提升了自身的技术钻研能力。

工程化

Webpack

Webpack 也从 2 升级到了3,但变化俨然远远不如从 1 到 2。使用配置难度依旧居高不下,想我这种上了年纪的人,没得商量,每次使用都要再看一遍 doc。

Rollup

Rollup 的定位十分明确,与 Webpack 不构成竞争关系。写 library 用 Rollup,写 app 用 Webpack。

npm

jspmbower 几乎已成骨灰,yarn 依旧不支持 scope。

新方向

webassembly

在下一代人上小学之前能看到定稿的 webassembly 我就已经满足了。

webcomponents

在下一代人上小学之前能看到普及的 webcomponents 我就已经满足了。

数据

断断续续学习 D3,但明显精力不如从前。

微信小程序

如我所料,半温不火。令人鄙视的是,支付宝和百度都还在模仿,简直了。

我的工作

去年我还在研究一种解决方案,能够兼顾的开发效率、运行时性能这两个看似简单却互为矛盾的指标。最后确实有了一个雏形。今年,我已经不再感兴趣,没有别的,就一个原因,想的太多,跟本没有那么复杂的使用场景。

总体来看过去一年,对于个人来讲是毫无建树的,今年上半年,我可能需要拓展一下视野,找一找我不会做的事情做一做。

0%