为什么this变了

在微博和知乎上都见过这个问题,就把我在知乎的回答整理搬运过来吧。
先看以下代码:

var name = "The Window";
var object = {
name : "My Object",
getName : function(){
console.log(this.name);
}
};
// 以下输出
object.getName(); // "My Object"
(object.getName)(); // "My Object"
(object.getName = object.getName)(); // "The Window"

第一行的结果大家应该都知道,不多说了。第二行可能有人会有些疑问,先跳过,我先说第三行的。

第三行 『(object.getName = object.getName) 』圆括号里的内容是AssignmentExpression,它的返回值是『GetValue(等号右边的表达式)』。注意这个GetValue是规范中的抽象操作,object.getName经过这个GetValue操作之后就不是Reference type了,丢失了它的this信息。

而第二行实际和第一行是一样的。这个圆括号在规范中叫grouping operator,它不执行GetValue操作,所以函数调用的时候函数知道自己是通过object进行调用的。
(实际上你可以试一下,『(object.foo) = 1)』是合法的语句)

规范中的相应部分:

[1] 关于函数调用的部分,在这里你可以看出来function call在不同情况下对this的处理
http://www.ecma-international.org/ecma-262/7.0/index.html#sec-function-calls-runtime-semantics-evaluation
[2] AssignmentExpression的执行过程 http://www.ecma-international.org/ecma-262/7.0/index.html#sec-assignment-operators-runtime-semantics-evaluation
[3] Grouping operator的执行过程 http://www.ecma-international.org/ecma-262/7.0/index.html#sec-grouping-operator-runtime-semantics-evaluation

js代码在什么位置可以换行

一行过长的代码会影响阅读体验. 实际项目中,我们往往把过长的代码分成多行去写.
比如在js中, promise链经常要分成多行写. 对此本人想到一个问题, 就是js中, 在什么地方换行是合法并且不破坏原有代码逻辑的.

探究这个问题过程中本人造了一个小玩具, breakline, 顾名思义, 它的作用就是在不破话代码的逻辑的情况下把js代码拆成多行. 项目地址:https://github.com/flowmemo/breakline

我们接下来还是谈换行的问题. 为了简单起见, 本文不讨论注释中的换行符.

换行符的作用

在一般的语言中, 换行符一般是意味着一个statement1的结束, 分号也通常有这个作用. 但是js中换行符并没有代表statement的结束的意思. 事实上在规范中, 换行符(规范中称为line terminator)在多数情况下和空格、tab的作用是一样的, 是为了分隔token以及方便阅读, 除了…

  1. 换行符在某些情况下会触发自动分号插入(Automatic Semicolon Insertioin, 以下简称ASI)
  2. 换行符本身是token的一部分, 比如在StringLiteral(单/双引号字符串, 必须通过\来escape), Template和TemplateSubstitutionTail(这两个就是`包起来的字符串)中.

以下为ECMA-262 7.0(也就是ES7、ES2016)规范中相应的描述:

A line terminator cannot occur within any token except a StringLiteral, Template, or TemplateSubstitutionTail. Line terminators may only occur within a StringLiteral token as part of a LineContinuation.

有了这些信息, 我们就应该知道在什么情况下可以换行了:只要你的新加的换行符不在上述的两个规则内就行.

我们再继续探究这两个规则.

ASI

EMCA规范中有一部分专门讲了ASI, 在http://www.ecma-international.org/ecma-262/7.0/index.html#sec-automatic-semicolon-insertion

对于ASI这里就不展开细讲了, 这里只说ASI和换行相关的部分.

Offending Token

一个情况是, 换行符后的token和之前的token一起进行parse会出错, 那么换行符后的token叫做offend token. Offend token前会自动插入一个分号.
举例:

var foo
foo = 2

一共5个token, 按先后顺序是var, foo, foo, =, 2. parser先解析了前两个token, 很好, 没错, 然后接着parse下一个token foo, 出错了(因为没有一个js语法中并没有一条production包含var foo foo的形式). 于是ASI被触发, 在换行符和offend token间加了个分号. var foo就被parse成了Variable Statement, 然后后面的foo = 2也被成功parse了2.

Restricted Production

Restricted production规定了触发ASI的几种特殊情况.

以下几个位置出现换行符, 即使之后的token可以继续被合法parse, 也会触发ASI

  1. 后缀自增/自减运算符之前
  2. continue, break, return, throw, yield之后
  3. arrow function的=>之前

所以我们要尽量避免在上述位置加换行符(除非你知道自己是在做什么).

Template和TemplateSubstitutionTail

这部分简而言之就是不要在表示字符串内容的部分换行. 这个很好理解了.
需要注意的是对于有替换的部分, 也就是${}的花括号内部,是可以当作普通的expression换行的
也就是说

var s = `hello ${person.name}!`

var s = `hello ${
person
.
name
}!`

是相同的.

玩具:breakline

在探究这个过程中本人写了breakline. 这个工具的目的就是把你的js代码拆成多行, 同时又不对代码的功能和逻辑造成实质的影响.

总结

在实际写代码过程中, 符合普通人逻辑的“正常”的换行是没有问题的. 如果说有需要注意的地方,也就是restricted production的第二条了, continue, break, return, throw, yield这几个关键字以及=>前不要换行, 其他的地方换行的话…可以说也没什么美感了, 一般人不会那么干的.


[1] 关于statement和expression的区别, 可以参考http://www.2ality.com/2012/09/expressions-vs-statements.html
[2] 实际上在foo = 2后也自动插入了一个分号.

Can I Reach Google —— 一个查找可用谷歌域名的小工具

由于教育网的一些玄学原因, Google还是偶尔可以上的.
所以写了一个小网页来检测Google的众多域名的可到达性. 点开就能用,地址在这里:
https://flowmemo.github.io/can-i-reach-google/

原理很简单, 访问Google桌面版网页会请求一个名为tia.png的文件, 是个图标, 大小为497B, 地址为
https://www.google.com/textinputassistant/tia.png. 这个地址根据Google的域名不同而不同, 但是相对路径是一样的. 我写的工具就是向所有的Google域名请求这个图片, 成功加载就判定该域名为可到达. 至于为什么不用ajax是因为ajax还要考虑跨域问题, 而加载图片不用. Google域名来自https://www.google.com/supported_domains

请求图片的地址加了动态的querystring(时间戳 + ID), 主要是防止cache.

如果你找到了可用地址, 建议按照我工具中给出的格式进行访问, 不要把https, www, 还有/ncr去掉, 这涉及到一些DNS和地区跳转之类玄学, 不细说了, 说不定明天就变了.

这个小工具的地址: https://flowmemo.github.io/can-i-reach-google/
源码地址: https://github.com/flowmemo/can-i-reach-google

String.prototype.repeat在V8和Chakra中的实现

最近一个left-pad事件搞得javascript圈沸沸扬扬的. 我们暂且把这个事情放一边, 来看看left-pad本身的实现.

left-pad的源码如下:

module.exports = leftpad;
function leftpad (str, len, ch) {
str = String(str);
var i = -1;
if (!ch && ch !== 0) ch = ' ';
len = len - str.length;
while (++i < len) {
str = ch + str;
}
return str;
}

这段程序的作用是, 给一个字符串str(或可以转成str的变量), 用字符ch在左边补位, 将其补到长度为len.
当然这个程序没做充足的参数检查, 这个就不细说了. 我们分析一下它的效率:
如果要补n位, 字符串加法也就是String.prototype.concat的执行次数是n次, 也就是O(n).

我们来看一下如何用ES6的String.prototype.repeat来实现这个功能:
假定str是字符串, len是非负整数, ch参数可选(如果有的话必须是长度为1的字符串) 所以我更喜欢强类型语言.

function leftpadES6 (str, len, ch) {
ch = ch||' '
return ch.repeat(len - str.length) + str
}

当然还没完, 这么写的效率怎么样呢, 我们得看一下js引擎对String.prototype.repeat的实现.

V8

下面是Chrome/Chromuim的js引擎V8的实现, 直接用js写的
源码地址:
https://code.google.com/p/chromium/codesearch#chromium/src/v8/src/js/string.js&l=672

// ES6, section 21.1.3.13
function StringRepeat(count) {
CHECK_OBJECT_COERCIBLE(this, "String.prototype.repeat");
var s = TO_STRING(this);
var n = TO_INTEGER(count);
if (n < 0 || n === INFINITY) throw MakeRangeError(kInvalidCountValue);
// Early return to allow an arbitrarily-large repeat of the empty string.
if (s.length === 0) return "";
// The maximum string length is stored in a smi, so a longer repeat
// must result in a range error.
if (n > %_MaxSmi()) throw MakeRangeError(kInvalidCountValue);
var r = "";
while (true) {
if (n & 1) r += s;
n >>= 1;
if (n === 0) return r;
s += s;
}
}

忽略参数检查, 我们来关注算法本身. 这个算法的核心是, 使用字符串重复次数count的二进制表示, 通过字符串的自加来减少concat的次数. 这个算法和快速幂算法非常像. 详细的算法解释可以看这篇文章: http://www.2ality.com/2014/01/efficient-string-repeat.html

举例个例子, 如果count = 6 , 那个它的二进制表示就为1102 = 4*1 + 2*1 + 1*0. 也就是说对于长度为6的字符串s, 有
s.repeat(6) = s.repeat(4) + s.repeat(2)

注意到每次循环最多有两次concat的操作, 而循环次数约等于logn, 所以按concat的次数来记它的复杂度为O(logn)

Firefox的实现类似, 地址在这里https://dxr.mozilla.org/mozilla-central/source/js/src/builtin/String.js#159 .

Chakra

好了, 我们来看看微软的Edge浏览器所使用的js引擎, Chakra对String.prototype.repeat的实现, 它是用的C++.
源码地址: https://github.com/Microsoft/ChakraCore/blob/master/lib/Runtime/Library/JavascriptString.cpp#L2395

Chakra中实现repeat分了两个函数, 一个是JavascriptString::EntryRepeat, 它的主要是做一些初始化工作, 参数检查, 特殊情况的处理. 核心算法是JavascriptString::RepeatCore, 代码如下

JavascriptString* JavascriptString::RepeatCore(JavascriptString* currentString, charcount_t count, ScriptContext* scriptContext)
{
Assert(currentString != nullptr);
Assert(currentString->GetLength() > 0);
Assert(count > 0);
const char16* currentRawString = currentString->GetString();
int currentLength = currentString->GetLength();
charcount_t finalBufferCount = UInt32Math::Add(UInt32Math::Mul(count, currentLength), 1);
char16* buffer = RecyclerNewArrayLeaf(scriptContext->GetRecycler(), char16, finalBufferCount);
if (currentLength == 1)
{
wmemset(buffer, currentRawString[0], finalBufferCount - 1);
buffer[finalBufferCount - 1] = '\0';
}
else
{
char16* bufferDst = buffer;
size_t bufferDstSize = finalBufferCount;
for (charcount_t i = 0; i < count; i += 1)
{
js_wmemcpy_s(bufferDst, bufferDstSize, currentRawString, currentLength);
bufferDst += currentLength;
bufferDstSize -= currentLength;
}
Assert(bufferDstSize == 1);
*bufferDst = '\0';
}
return JavascriptString::NewWithBuffer(buffer, finalBufferCount - 1, scriptContext);
}

看起来很长是吗? 不要被吓到了, 我们只关心核心算法, 其实就这一小段:

if (currentLength == 1)
{
wmemset(buffer, currentRawString[0], finalBufferCount - 1);
buffer[finalBufferCount - 1] = '\0';
}
else
{
char16* bufferDst = buffer;
size_t bufferDstSize = finalBufferCount;
for (charcount_t i = 0; i < count; i += 1)
{
js_wmemcpy_s(bufferDst, bufferDstSize, currentRawString, currentLength);
bufferDst += currentLength;
bufferDstSize -= currentLength;
}
Assert(bufferDstSize == 1);
*bufferDst = '\0';
}

大意是如果字符串长度本身为1, 也就是一个字符, 那就直接用wmemset(类似于memset)将一块内存全用这个字符填充; 如果不为字符串长度不为1, 就一次连接一个字符串. 忽略js_wmemcpy_s

结语

我们现在来考虑一下, 为什么V8使用了优化过的算法, 而Chakra使用了朴素/naive/trival的算法.

我们之前说了, V8实现的字符串加法的操作次数是O(logn)的, 但是, 我们要把一个字符串重复n次, 一定要得在要对O(n)的内存进行写操作.
update1: 经过@哦胖茶巨巨的提醒(http://weibo.com/2451315930/DowQCo6wN), V8、ChakraCore 和 Rhino 底层实现字符串拼接用的都是rope(tree). 对于rope来说字符串连接不需要为新字符串开辟被内存并把内容写进去, 而是合并两个二叉树, 详见这里: https://en.wikipedia.org/wiki/Rope_%28data_structure%29#Concat .

这么看的话, 在不考虑rope摊平的情况下, 仅从算法复杂度的角度来看V8的rope连接和快速幂实现是比Chakra的要好. Chakra里字符串也用了rope, 但是对repeat的实现没有用rope, 直接就是复制内存. 字符串在内存是就是以连续内存的形式存储的话, 把一个字符串重复n次, 一定要得在要对O(n)的内存进行写操作, 所以快速幂优化意义不大.

至于V8的这种字符串加法用rope、js实现用快速幂的方法好, 还是Chakra这种直接复制内存的方法好, 我没有跑benchmark, 就不下结论了.

最后想说的是, 虽然这篇文章关注的是实现算法, 但是参数检查、边界条件的处理, 也非常的重要, 千万不能觉得无所谓. 可以说工具函数中edge case的处理经常比算法更为头疼…

P.S.1 我在知道V8, Chakra的字符串实现用了rope后对本文进行过修改
P.S.2 我之前搞错了, V8中字符串“+”操作符不依赖String.prototype.concat, 实际正好相反,是String.prototype.concat的实现直接用了字符串“+”运算. 源码: https://code.google.com/p/chromium/codesearch#chromium/src/v8/src/js/string.js&l=84