無標題文檔

What is "this"? - Part.1

呃,这次的标题有些「装逼」,首先我们从道题目开始。

这是道有趣的题目:考虑下面的代码,考虑 console.log 输出的值:

var x = 10;
var foo = {
    x: 20,
    bar: function () {
        var x = 30;
        return this.x;
    }
};

console.log(
    foo.bar(),             // 1.
    (foo.bar)(),           // 2.
    (foo.bar = foo.bar)(), // 3.
    (foo.bar, foo.bar)()   // 4.
);

-- Split --

实际上,题目真正意思求 console.log 中依次四条语句的值。为了方便讲述,上面的语句分别标注为 1-4。

首先,是「1.」这条语句相对来说比较明朗(其实我们也经常这样写)。调用 foo 的 bar 方法,因此 bar 的 this 指向(作用域)为 foo,因此就等于是取 foo 上面的 x 属性(也就是 foo.x)的值,那么语句返回的值就是 20 。

然后是「2.」这条语句。我们可能对 Grouping Operator (也就是「()」)犹豫不决,那 么我们找找 ECMA 中相关定义

11.1.6 The Grouping Operator

The production PrimaryExpression : ( Expression ) is evaluated as follows:

    1. Evaluate Expression. This may be of type Reference.
    2. Return Result(1).

NOTE 

This algorithm does not apply GetValue to Result(1). The principal 
motivation for this is so that operators such as delete and typeof 
may be applied to parenthesised expressions.

因此,由于 foo.bar 是个引用「Reference」,所以使用组操作符 (foo.bar) 返回 的引用是和 foo.bar 是一样的。

如果还不理解,那么考虑下面的表达式:

console.log(foo.bar === (foo.bar))

因此,

(foo.bar)();

返回的也是 foo.x 的值,也就是 20 。

目前为止上面的前两个语句的解释,想必已经了解。细心的读者会发现,其实题目的重点是在 3. 和 4. 两条语句。

要解释后面的两个语句恐怕这篇文章会塞不下,请留给我点喝水的时间,我们下篇继续。

-- To be continued --

PS, Part 2. 在这里

Re:新手虚心求教

经常有朋友邮件过来咨询技术问题,本人不才经常不是忘记回复就是回复的比较迟。原因是一来本人比较懒(或者可以托词为忙),二来是怕「误人子弟」。

当回复这位仁兄的邮件时,不觉得就发现回复的字数越来越长,于是就将其整理为篇 Blog。我的意图是不仅仅帮忙解决问题,而且能提供个解决类似问题的思路。

邮件原文

来自: Captain
时间: 2010-02-28 20:41
话题: 新手虚心求教

  你好,我是刚刚入门学习Javascript的新手。我想向你请教一下我在学习过程中所
遇到的一个问题。恕我愚昧,但是我一直无法理解,恳请赐教。 
  我在学习中碰到过一条这样的声明: var !myVariable != variableValue, 因为在
以前的学习中知道双重否定等于可定,那这则声明是否可以理解成 var myVariable = 
variableValue ?
       若非如此解释,恳求正解。冒昧打扰请多多包涵。

我的回复

不好意思,收到你的来信到现在才回复。第一眼看见您提供的代码,个人觉得它不合乎语法规则

var !myVariable != variableValue

根据 ECMA 相关的文档,var 关键字的定义在 12.2 章节(我看的是第五版),它的语法定义是:

var VariableDeclarationList ;

而 VariableDeclarationList 包括

VariableDeclarationList , VariableDeclaration

上述两个定义又包含多种情况,说起来有些复杂,所以我画了张草图(可能不准确,以文档为准):

https://friable.rocks/_/2010_03_10/1268192678.png

所以,上面的表达式不符合 var 语句的定义要求,因此 Javascript 解析器会报语法错误。

如果是非赋值定义语句,也就是

!myVariable != variableValue

那么就是个条件判断语句,其中有两个条件判断操作符「非(!)」以及「不等于(!=)」,那么我们就要了解它们的结合优先级。

根据相关的参考文档 ,我们可以得知「非」操作符的优先级比「不等于」要高,因此语句先计算 !myVariable 这块,然后再计算「不等于」后的值。

至于能否改成其他等同的逻辑情况,这里还有个问题就是 值取「非」的时候会有多种情况 ,这里有个讨论:

明城: 
问: !a != b 是否等于 b == !!a

崇厚: 
这个还要看数据类型吧?
比较运算符的优先级是最低的

渔隐: 
照这样推理就是a == b 了

崇厚: 
a = ''
b = 0
console.log(!a != b)
console.log(b == !!a)
true true

明城: 
关键还有个「非」操作符,所以 B 始终是和布尔值对比的

崇厚: 
我试出来三个结果
t t 
t f
f f

痴灵: 
var a = 0;
console.log(!a);console.log(typeof(!a),typeof(a));
true boolean number
类型发生了变化了

渔隐: 
用!== 和===吧

明城: 
不纠结了,以后记得逻辑表达式里千万不要加一元操作符
或者加括号

沉鱼: 
有个比较有效的做法,不太记得操作符的优先级时,我都
加(),这样虽然比较挫,不过保险

崇厚: 
不管记得不记得优先级,表达式比较复杂时最好都加括号
这样看起来清晰一些

举一反三,以后如果再碰到类似的问题,我们可以:

  1. 查文档。 ECMAScript 规范MDC 中的文档 、甚至 Google 都是我们的好帮手
  2. 如果还是无法理解,可以「偷懒」先看运行结果,然后逆向推断
  3. 团队的力量胜过一个人闭门造车

-- EOF --

acookie 改进心得

这是工作记录,慢慢的不知不觉就形成篇文章了,就发在这里吧。

-- Split --

acookie 就是段统计代码,它的原理就是发送个 GET 请求到统计服务器,并附上本地用户的浏览器类型、分辨率等其他信息。

问题

有人报告 acookie 的代码出现 XSS ,原理是利用某 GET 参数 bypass 未过滤完全的字符。这里是原先的代码摘录:

(function() {
    function akrand(num) {
        return Math.floor(Math.random() * num) + 1
    }
    var P = location.pathname;
    if ((parent === self) || P.indexOf('{...}') != - 1 || P.indexOf('{...}') != - 1) {
        var R = escape(document.referrer);
        var id = "" + akrand(9999999) + akrand(9999999);
        var title = escape(document.title);
                document.write('<img src="http://foo/1.gif?acookie_load_id=' + id + '&title=' + title + '&pre=' + R 
                       + '&param0={...}&param1={...}&param2={...}"  width="0" height="0" style="display:none;" />');
    }
})();

这段代码「年事已高」每次的修修补补都是治标不治本,于是干脆考虑重写这段统计代码。

目标

在开始重写之前,为将要写的代码列了几条目标:

  1. 流量方面的考虑,代码要尽可能的简短
  2. 尽可能的优化性能
  3. 「绿色」,不干扰页面的其他脚本
  4. 脚本尽可能得做到安全

思考

  1. 使用原先的 document.write 会在页面中加入 DOM,直接使用 new Image 更好
  2. 原先的判断条件很冗余,完全可以考虑个函数搞定
  3. 安全方面虽然可以写个简短的过滤函数,但这样代码又会很长

解决

下面是反复修改后的最终代码:

(function(){
    var M = function(n) {
        return Math.floor(Math.random() * n) + 1;
    },
    I = function() {
        for (var i = 0, P = location.pathname, args = arguments, len = args.length; i < len; i++) {
            if (P && P.indexOf(args[i]) !== -1) {
                return 1;
            }
        }
        return 0;
    },
    D = document, R = escape(D.referrer), S = screen, T = escape(D.title);

    if (parent === self || I('{...}') || I('{...}')) {
        try {
            return new Image().src = [
                "http://foo/1.gif?cache=" + M(9999999),
                '&pre='+ R +'&scr='+ S.width +'x'+ S.height + '&title=' + T,
                '&param0=' + '{...}',
                '&param1=' + '{...}',
                '&param2=' + '{...}'
            ].join('');
        } catch (e) {}
    }
})();

本来想使用 ~function() {}; 这样的闭包, 结果发现效率方面还是原先的理想 (当然不会差很多),加之可能以后阅读的同事会有困扰,还是采用原先的吧。

在这个案例中,其实返回 0 和 1 同比返回 false 和 true 是同样的道理,处于节省代码量考虑,直接使用整型值。

安全方面出于简单原则考虑,没有考虑使用转义函数,而是采用数组拼贴的方式,这样就算有注入也会造成语法报错而不能指定此段脚本,前面加 return 也是这样考虑。

后记

考虑以后的代码可维护性很重要,这个例子中代码虽然简单,当然还会有更极端的写法,不过处于以后同事的合作考虑,尽量不要写得过于的「专业」。

采用 document.write 的考虑是想直接使用脚本在页面上输出,因此在本案例中不是很适用,同时才用这一方法在安全的角度上考虑需要过滤的字符太多,因此如果了解需求(比如仅仅是生成个带参数的 URL)还是避免使用它

XSS 和 CSRF 等虽说后台处理是治本的,但如果使用不当前后台考虑不周全就算加入了对应的过滤函数,也有可能造成注入。也从另个角度考虑,有时候完善的代码也能从一定程度上加大注入的难度,尤其是 Javascript 等这种「每个人都可以看见得脚本」。

-- EOF --

匿名函数的写法

这是 小马 在交流会中的分享,可能有些朋友还不曾了解,同时也为了自己温故而知新,就整理下。

多种方式

可以想像得到,有很多方法立即执行匿名函数,简单的整理就有下面三种的典型方式(还有其他方式的,欢迎告知):

方式一

(function() {
    // …
})();

方式二

void function() {
  // …
}();

方式三

~function() {
   // …
}();

方式一 和 方式二 大家可能都看到过,这里主要说明下 方式三。在说明 方式三 之前,我们现回顾下运算符「~」(位取反)在 EMCAScript 中的定义(第五版的第 72 页),简单的翻译下:

  1. 按运算符结合语句
  2. 将旧值转换为 32 位整型
  3. 执行运算符后的语句
  4. 转换之行结果为 32 位整形并返回

从上面可以了解,其实位运算符都能立即返回后面表达式的值。其实其他位运算符都可以达到这样的目的,例如

!function() {
    // …
}();

等都可以达到我们的目的。所以其实用「~」也并无其他的原因,仅仅是代码「看着好看」而已 :^)

效率

与其说三种方式执行匿名函数的效率,到不如直接分析个运算符的执行效率。那么我们对比再来看看「()」以及「void」在 EMCA 规范中的定义

组运算符(第五版 66 页)

  1. 返回表达式的执行结果

void(第五版 70 页)

  1. 按运算符结合语句
  2. 执行
  3. 返回 undefined

由于组运算符还需要执行语句并返回语句块返回的值,对比 void 会多个获取语句块的操作(虽然消耗不了多少的性能),因此在这种情况下 void 的性能要优于组运算符。

对比两者,那么 方式三 的性能对比很明显要低于前两者。总结起来就是光从语法的角度上分析,在上面三者匿名函数的方式中 方式二 优于 方式一,方式三 的效率最低。

总结

思考下三者之间的优异

那么

这里要说明下,其实三种方式效率之间的差异非常小。因此单纯拿效率来考虑采用那种方式,几乎是站不住脚的。

具体采用何种方案,需要根据实际情况来考虑。比如我自己,经常会使用方式三,是因为 1、方便(加一个字符即可) 2、在函数长的情况下匹配括号会很头晕 3、用起来很酷,但 方式三 会经常让看我代码的组员感到困扰。

如果在些类库等框架性质的基础代码,用 方式一 最保险同时大家都容易看懂,是最保险的选择。

看见 阮一峰 的 Blog 上有篇文章说避免使用 void 运算符 。其实在 Javascript 中, void 的使用方面还是比较常见的。

例如,我们会强制让某些调用返回 undefined 以阻止浏览器等默认行为(我们经常看到的就比如很多 Javascript Bookmark 前面就经常会加上 void 运算符)。

所以,还是「物尽其用」吧。

-- EOF --

关联的 script 标签

从 James Padolsey 这里 得到个好的点子。

在实际写脚本过程中可能有段 Javascript 和 HTML 非常相关(比如实例化 Slider 等这样组件),那么通常我们会将它紧放到 HTML 的后面。

「传统」的做法需要顾虑的点有很多。因为脚本是立即被执行的,所以要考虑例如调用的组件是否已经声明,以及如果有 Ajax 请求是否会堵死浏览器等等。

下面的代码就是本篇 Blog 提供的另个思路,但愿我看起来不是那么的火星:

<div id="some-div">
    <script type=":contextual">
        alert(this.id); // "some-div" is alerted
    </script>
</div>

原文 作者的想法是改变 script 标签的 this 指向到父节点的 Element,从而关联上下文 HTML 结构。

看它的实现代码:

<script type="text/javascript">
~function() {
    var scripts = document.getElementsByTagName('script'),
        script, i = 0;
    while ((script = scripts[i++])) {
        if (/:contextual$/.test(script.type)) {
            (new Function(script.innerHTML)).call(script.parentNode);
        }
    }
}();
</script>

不过如原作者所说的外,其实还有很多顺带的好处

  1. 将 this 指向关联到父节点,遍历查找 DOM 非常的方便
  2. 相关的 script 标签和 HTML 结合紧密,很清楚就能明白这段脚本需要做什么
  3. 统一调用,可以考虑懒加载
  4. 方便复制粘贴 :^)

当然,上面的代码仅仅是个想法而已,在实际编码中还需谨慎应用。滥用此方法可以预料到的 些问题,比如:

  1. 脚本执行顺序改变
  2. 弄乱作用域,如果你的代码严重依赖 this ,那么将会是个噩梦(当然,这本身也不是个好习惯)
  3. 让不了解此机制的其他开发者迷惑

正如原作者所言,在我们写代码的时候能「Thinking outside the box」,那才是最重要的 :^)

-- EOF --

我的照片

嗨!我叫「明城」,八零后、码农、宁波佬,现居杭州。除了这里,同时也欢迎您关注我的 GitHub (2) 、 TwitterInstagram 等。

这个 Blog 原先的名字叫 Gracecode.com 、现在叫 「無標題文檔」 。 其实无所谓叫什么名字,作为码农知道取名是件很难的事情。最后想到的这个名字,其实都没啥特别的含义,系统默认的文件名而已。

作为八零后,自认为还仅存点傲娇式的幽默感,以及对平淡生活的追求和向往。 为了免得对号入座和不必要的麻烦,声明本站点所持观点仅代表个人意见,不代表自己所服务公司的立场。

如果您想联系我,可以发我邮件 `echo bWluZ2NoZW5nQG91dGxvb2suY29tCg== | base64 -d`

文章

项目