精华 javascript吐槽系列
发布于 3个月前 作者 BinChang 1180 次浏览 来自 分享

老码农,javascript/nodejs新手上路,在看一些书,写一些吐槽贴,自娱自乐,增强学习效果。大家权当娱乐贴看。

javascript的scope控制比较奇葩,function scope以外就是global scope了,所以function scope就被各种滥用,用它加closure来做module, 用它来强制限制using strict的影响范围,各种hack,这些都应该是语言来做显示支持的。 Screen Shot 2014-11-14 at 12.44.02 PM.png

36 回复

javascript的数值没有整形,浮点之分,只有一个类型Number,定义成64位双精度浮点数,而且就着这种浮点数,javascript还支持位运算bit operation。 所以我们就有了下面的奇葩代码 Screen Shot 2014-11-14 at 2.15.23 PM.png

#javascript 的隐式类型转换各种惊喜频繁出现,当string 和 number相加的时候,toString() 和valueOf()的优先级让人昏厥,NaN,isNaN互不相认,各种混乱,可以造成让人吐血的隐藏错误,想想也是醉了。

javascript这个名字是因为它号称师从于java,java的string设计基本是完美的(你可以说它性能不如c),javascript这一套确没有学到家,搞了一个primitive string和一个string object,解释器还会帮你把primitive string隐式转换成string object,下面这个结果想想也是醉了。 Screen Shot 2014-11-14 at 3.04.29 PM.png

javascript允许你在一个语句的末尾省略掉";", 解释器会帮你自动插入一个";", 这个功能想想就让人醉了。想想看图中分割线上下两段的执行效果式不一样的。

Screen Shot 2014-11-14 at 3.29.33 PM.png

勘误表

  • javascript这个名字是因为它号称师从于java(毛关系没有)
  • java的string设计基本是完美的(你可以说它性能不如c) 这句括号里的是错误的
  • scope你可以不写,你只要不怕代码污染就行
  • javascript的数值没有整形,浮点之分(没听说float和int?parseInt方法等都是干啥的呢 ?)
  • 隐式类型转换各种惊喜频繁出现 (确定你用对了方法?基础不过关吧?)
  • 搞了一个primitive string和一个string object(这句很搞笑,new本身代表的是啥意思呢?自己用typeof测测吧)
  • javascript允许你在一个语句的末尾省略掉";", 解释器会帮你自动插入一个";"(老程序员不看编译原理的么?哈哈)

哈哈,本身js就是2周写出来的语言,就不完美,捡好的用吧《javascript good part》

欢迎补充

@i5ting

的确没有整形。你所谓的操作都是 js 计算的时候强制转化为整形然后计算完成后还是以浮点数返回。

javascript之所以这样叫是有点要营销的味道,别拍我 (书上这样说的

"foo".baz = “shit” console.log("foo".baz)

这就是要基础知识了

@coolicer 就是营销。那个时候刚出 java,js 借这个噱头取名 javascript。

@xadillax 我说的不是没有整形,而是用parseInt这样的方法来处理

@coolicer 最早是网景公司的livescript,后来和sun战略合作,才改名的

多看点c,少去碰java,你现在的思想受到的毒害不浅

晕,用 java 和 javaScript 比较啊

  • 本人写了多年的java, c++和python,大部分工作是backend,靠近storage一层,所以观点肯定有偏颇。大家看看一乐就可以了,不要太认真。

  • 这里的例子基本上就是抄的 《effective javascript》里面的,

  • 关于javascript和java的关系,大家也都知道。javascript的最初作者Brendan Eich给《effective javascript》写了序,第一段是这么写的,As is well known at this point, I created JavaScript in ten days in May 1995, under duress and conflicting management imperatives—“make it look like Java,” “make it easy for beginners,” “make it control almost everything in the Netscape browser.”

  • 说说string的实现,写过一些c/c++的人都知道,混用并且同时支持 c string 和std::string是多累的累赘和麻烦。可以看看google 的StringPiece实现,https://code.google.com/p/re2/source/browse/re2/stringpiece.h#17, 17行google sanjay大神的感叹“// Arghh! I wish C++ literals were "string".”

  • 相对而言,java一开始就把string定义成immutable,而且取消c string这样形式,这个决定有performance penalty, 但是实际使用中是非常便利的。

  • javascript在设计实现的时候,作为一种上层的脚本语言,java是一个非常好的string 实现的参照例子,但是javascript却把c的那一套继承了过来,同时支持primitive string和String object, 这不是一个好的决定。

接着开灌。 再声明一下,吐槽是为了更加了解这个语言,新项目用的是nodejs,所以需要系统地下点功夫。

这次说说javascript里面关于global scope的设计。global scope的设计有不少瑕疵,这些观点在各种书籍里面也都提到了,比如神书《javascript the good part》。

第一个瑕疵就是程序员太容易在global scope里面添加成员,一个新变量如果没有用var前缀就会default到global object上,这个设计比如没有道理,会造成各种意想不到的引用冲突。

看图中的例子,在函数内使用的一个变量被添加到了global object里面了。

Screen Shot 2014-11-15 at 11.48.56 AM.png

[] + {}; // "[object Object]"
{} + []; // 0

[] + {} == {} + []; // true
{} + [] == [] + {}; // false

补充隐式转换的惊喜

这算是好的不学,学坏的么

语言总有优缺点的,在语言没法改进的情况下,克服这些问题一方面靠良好的编码习惯,另一方面就是依靠自己的取舍,是要性能,还是可读性,可维护性。很多时候优雅的实现并不能满足性能需求,代码改到最后,自己都觉得恶心,但是不得不那么做,这就是妥协,没有任何一个技术能够完美的解决所有问题。

觉得不好,就自己改,欢迎尝鲜fibjs。贡献,你的想法。不要在node上吊死。

javascript不支持block scope

一个function里面只有一个scope在block里面声明的变量等同于在function开始声明该变量,这个会让人记起老版本的c语言,强制要求所有local变量在函数开始就声明了。javascript的实现比老版c语言还要差一点,现在用户可以在block里面声明变量,但是这个变量会被当作function scope里面的。下面这个例子就能说明这个问题。

(function testBlockScopeVariable () {                                                                                                                                                         
    {    
        var foo = "in block scope";
    }    
    console.log(foo);
})();

这段代码的结果就是

"in block scope"

javascript允许一个变量被反复声明多次,解释器会把它们映射到同一个变量。

(function testBlockScopeVariable () {
    var foo = "in function scope";
    {
        var foo = "in block scope";
    }
    console.log("test in function scope " + foo);
})();

输出的结果是

test in function scope => in block scope

@fengmk2 已经把《javascript the good part》简单翻了几遍,可能还是需要在实际应用中消化揣摩。 这一段时间准备仔细揣摩一下这个语言的细节,不代表一定要用这些特性,至少做到了解。

@coordcn 赞同你的观点。

实际的工程质量很多时候跟语言选择没有绝对联系,大部分创业公司都是从简单的类似LAMP架构起步,等产品起步上规模后,再优化架构,考虑service oriented architecture. 这些公司中,从PHP起家的就有很多,php语言的槽点也是很多的。但这个不妨碍公司发展。

再大一点说,公司能不能起来,有时间技术都不会最关键的,产品的idea才是最关键的,好的产品,好的执行团队,总是能够找到恰当的技术团队来实现。

javascript因为各种机缘,做大到今天的地步,跟它的几个好的设计决定是有关系的。 但还是不能否认它有各种各样的奇怪的,让人想想都要醉了的设计决定。 选一个好的语言,适合本团队的语言,对团队的工作效率会有很大的帮助。 在决定用一门语言写工业级代码的时候,深入了解这么语言的特性,也是一个好的开始。

@ngot 难道你是传说中的西祠响马。

再说javascript不支持block scope, 这一点不仅仅适用与变量,同样还适用与命名函数的声明。 在一个函数内定义的另外一个函数都被当作这个函数最开始声明的函数,也会覆盖其他的外围的同名函数。

看看下面这个例子:

function f() { return "global"; }          

function test(x) {                         
    var result = [];                       
    if (x) {                      
        function f() { return "local"; } // block-local ??
        result.push(f());                  
    }                                      
    result.push(f());                      
    return result;                         
}                                          
console.log("test(true) => " + test(true));
console.log("test(false) =>" + test(false));

上面这个代码的运行结果是

test(true) => local,local
test(false) =>local

JavaScript坑固然多…但只要比较深入的了解JavaScript,再加上良好的编程习惯…也感觉不到有什么不好的.可能对新手来说,就会觉得那些坑怪怪的…

好多坑

如果能加上各种应对或者如何避免就好了。而不仅是纯吐槽。

@newghost 对的。浮点数的那几个例子在python里面完全可以再现,这个是浮点格式自己的问题。 javascript里面整型和浮点放在一起用一个类型处理了,就让一些问题变得非常的复杂。

@l3ve

javascript的一个卖点是灵活,没有什么东西不能改的,没有public ,private等等限制,这也使得javascript的一些特性可能会被不健康地滥用。再加上javascript的这些设计的瑕疵,使得javascript被用坏了几率大大增加。

以上这些都已经是既成事实,个人和小团队无法暂时改变,那么剩下的选择就是小心地适用这个语言了,一般来说就是严格科学的code style guide,和规范化地code review,以及一些自动化地code lint tools. Code style guide需要工程师学习理解其缘由,并加以实践,这些都算是工程师脑力负担。

如果现在四大浏览器能够精诚合作,选一门语法更加靠谱点的脚本语言,比如dart,工程师们会少死好多脑细胞。这只能是一个美梦了,到现在位置,各位大佬为了一个video tag的视频编码还是打个不停。

下面开始说说javascript的函数。

函数作为first class citizen一直是javascript的一大卖点,

写过c/c++ async代码的人都知道callback hell的痛苦,真正能够不查文档把c/c++函数指针语法写对的人不多,加上class以后就更加复杂,语法简直是天书。c11的lambda, std::bind,std::function大致可以缓解这种情况。 写过java async代码的人都知道用java做closure比较啰嗦,一般都是专门做一个anonymous class来实现一个Function interface,比如Runnable, Callable, 语义是清楚的,但就是啰嗦。 这些在javascript里面都不是问题,因为function自己就是一个object,传递函数跟传递object一样的语义。

以上是javascript的函数的好东西,在各种书籍里面也已经反复提及,神书《javascript the good part》对此大赞特赞。javascript的最初作者Brendan Eich自己也说javascript他做对了两件事,first class functions and object prototypes. 简单易用的function语法应该是促使工程师选择javascript作为一个event driven的nodejs架构的上层脚本的主要原因之一。

javascript的函数也有不好的东西。首先就是this指针的定义跟上下文有关,搞清楚这个就要弄死很多脑细胞,而且解释器对很多错误都很宽容,需要大家人肉找错。 一般来说javascript function有三种调用场景, function call, method callconstructor call. 从传统的OO语言起家的人对这个决定会深感困惑,在实际工作中,把一个函数在这三种场景混用的例子估计基本不存在,而代价就是工程师在写代码的时候要判断this到底是指向哪一个object,在function call里面把this指向global object也不是一个好的决定。 在constructor call的时候隐式替换函数返回值为新object这个决定也是违背常理的。

用下面这个函数来做例子:

function functionMethodOrConstructor(name) {
    this.name = name;        
    return this.name;        
}  

这样一个函数定义,在javascript可以当作function来用,也可以当作object method来用, 也可以当作constructor来用.

function call的例子

var name = "global name";    
                             
console.log("use as function =>" + functionMethodOrConstructor("function_test"));
console.log("global name => " + functionMethodOrConstructor("function_test"));

这段代码里面this指向了global object, 输出结果是

use as function =>function_test
global name => function_test

method call的例子

var obj = {                  
    name : "object name",    
};                           
obj.testMethod = functionMethodOrConstructor;
                             
console.log("obj.name =>" + obj.name);
console.log("use as method =>" + obj.testMethod("method_test"));
console.log("obj.name =>" + obj.name);

这段代码里面this指向了obj, 输出结果是

obj.name =>object name
use as method =>method_test
obj.name =>method_test

constructor call的例子,

var instance = new functionMethodOrConstructor("constructorTest");
console.log("instance.name =>" + instance.name);

这段代码里面,js解释器把this绑定到一个新的object,并且把这个object当作函数返回值, 输出结果是

instance.name =>constructorTest

说说Function.prototype.bind(), 这个需求也是缘起于Functionreceiver的动态绑定,同一个函数体,在作为object method被调用的时候,得到object作为this, 但是object和function之间的这种对应在callback里面需要另外单独绑定,这个时候就需要其他的方法来维持。一帮可以定义另外一个function wrapper,或者用Function.prototype.bind().

这样的应用场景在c++的async调用也可能会出现,同样的也是用lambda wrapper 或者std::bind()来维护函数体和this指针的对应。在java里面,这样的问题就不存在,那是因为java只允许传interface/object reference,没有函数指针这个东西,所以this的context就不会混淆,从这一点上来讲,java是定义清晰的一家。

下面这个例子简单演示了 Function wrapperFunction.prototype.bind的用法。

var panda = {
    name : "PanPan",
    getName : function getName() {
        return this.name;
    },         
}              
               
var invalidGetName = panda.getName;
console.log("run invalidGetName => " + invalidGetName());
               
var bindedGetName = panda.getName.bind(panda);
console.log("run bindedGetName => " + bindedGetName());
               
var getNameWrapper = function() {
    return panda.getName();                                                                                                                                                                                                           
};             
console.log("run getNameWrapper => " + getNameWrapper());

上面这段代码的运行结果是

run invalidGetName => undefined
run bindedGetName => PanPan
run getNameWrapper => PanPan

神奇的caller filed.

这应该算是高阶解释型脚本的一个福利吧,不是每一个语言都能让你用eval()动态地解释一段语句,也不是每一个语言能让你检查当前调用的callstack. 不够这样的操作带来的开销也是很神奇的。 面试的时候大家还在为各种数据结构的复杂度争论不休,不停地应用hashmap神器,但是实际应用中,javascript根本不管这些,php也根本不管这些,一个array,一个object通行无阻。所以书本上的东东,和实际生产环境的开销还是不同的。

下面这段代码能够dump出整个nodejs的执行入口过程:

function getCallStack() {                                                                                                                                                                                                                  
    var stack = [];          
                             
    for (var f = getCallStack.caller; f; f = f.caller) {
        stack.push(f);   
    }                        
    return stack;            
}                            
                             
function f1() {              
    return getCallStack();
}                            
                             
function f2() {              
    return f1();             
}                            
                             
var trace = f2();            
console.log("trace => " + trace); // [f1, f2]

这段代码的输出结果是

trace => function f1() {
    return getCallStack();
},function f2() {
    return f1();
},function (exports, require, module, __filename, __dirname) { function getCallStack() {
    var stack = [];

    for (var f = getCallStack.caller; f; f = f.caller) {
        stack.push(f);
    }
    return stack;
}

function f1() {
    return getCallStack();
}

function f2() {
    return f1();
}

var trace = f2();
console.log("trace => " + trace); // [f1, f2]

},function (content, filename) {
  var self = this;
  // remove shebang
  content = content.replace(/^\#\!.*/, '');

 // ....
 // million lines of code.
// ...

  startup();
}

关于prototype

prototype是搭建javascript代码共用的一个桥梁。 而prototype一般会有三种典型场景。 在这三个场景里面,比较有趣的是Function作为constructor和传给function variable的不同处置。 new XXX 这样一个语句对prototype做了一些有意思的设置。

  1. 简单的object instance,链接到Object.prototype. 比如下面这个例子
// Simple object links to Object.prototype
var obj = {} 
console.log(Object.getPrototypeOf(obj) == Object.prototype);

输出

true
  1. Function variable 会链接到Function.prototype
function Panda(name) {                 
    this.name = name;                  
}                                      
                                       
Panda.prototype.getName = function () {
    return this.name;                  
}                                      
                                       
// Function variable links to Function.prototype
var panda_function = Panda;            
console.log(Object.getPrototypeOf(panda_function) == Function.prototype);
console.log(Object.getPrototypeOf(Panda) == Function.prototype);  

输出

true
true
  1. 使用new Panda创建的object instance, 链接到Panda.prototype, 而 Panda.prototype自己也是一个object,链接到 Object.prototype.
function Panda(name) {                                  
    this.name = name;                                   
}                                                       
                                                        
Panda.prototype.getName = function () {                 
    return this.name;                                   
}                                                       

// object created by "new Panda" links to the prototype of the Panda as constructor.
var panda = new Panda("haha");                          
console.log(Object.getPrototypeOf(panda) == Panda.prototype);
                                                        
var panda_proto = Object.getPrototypeOf(panda);         
console.log(Object.getPrototypeOf(panda_proto) == Object.prototype);   

输出

true,
true

在javascript里面,一旦我们定义了一个函数作为constructor,我们就要时刻记住不能漏用new operator, 不然就变成了使用this指向global objectfunction call,这个想想也醉了。

prototype做一层继承,还是挺简单的,但是要做两层继承,其中的弯弯绕想想也要醉了。 主要的问题是我们需要正确地调用每一层的contructor,并且把正确地设置prototype的链接关系。

下面这个例子简单演示了一个两层的继承。

function Animal(color, weight) {
    this.color = color;
    this.weight = weight;
}
 
Animal.prototype.getColor = function () { 
    return this.color;
}
 
Animal.prototype.getWeight = function () { 
    return this.weight;
}
 
function Panda(name, color, weight) {
    Animal.call(this, color, weight);
    this.name = name;
}
 
 // the magic way to link to the proper prototype
Panda.prototype = Object.create(Animal.prototype);
 
Panda.prototype.getDescription = function () { 
    return "name=" + this.name + ", color=" + this.getColor() + ", weight=" + this.getWeight();
}
 
var panda = new Panda("panpan", "black", 250);
console.log(panda.getDescription());

输出内容是

name=panpan, color=black, weight=250

javascript里面的可用数据结构已经无法吐槽了。

  1. python的数据结构是很好的设计,简洁,清楚,一般一个人如果去面试,选用python写白板程序,是选对了语言。
  2. java的collection container很丰富,各种实现都有,定义清楚,接口清晰.
  3. c++的stl基于c++的template展开,虽然接口有点啰嗦,不如上面两个简洁,性能高效是无可争议的。而且c/c++系列可以做直接内存操作,可以直接实现需要的数据结构。
  4. 到了javascript这里,主要可用的就是Array, 和Object (当作hashmap用), Array/Object在loop的时候限制一大堆,想想都要醉了。

@BinChang 在递归面前,还有loop限制么?基本上大部分数据结构都能从这两种派生出来。

回到顶部