编写高质量的 JavaScript 代码(一)

一、理解JavaScript的浮点数

由IEEE754标准制定,JavaScript中所有的数字都是双精度浮点数,即64位编码数字。 JavaScript大多数的算术运算符可以进行整数、浮点数或者两者的组合进行计算。但是位运算符比较特殊,JavaScript不会直接把操作数作为浮点数进行运算。需要这些步骤完成运算:

1、把操作数8和1转换成32位整数;

2、每一位按位或运算;

3、把结果转换成64位浮点数。比如:

 8 | 1;  // 9
 //000000000000000000000000000001000 | 000000000000000000000000000001001 = 000000000000000000000000000001001

浮点数的计算是不精确的,浮点运算只能四舍五入到最接近的可表示的实数。当执行一系列的运算时,随着舍入误差的积累,运算结果会越来越不精确。比如:

0.1 + 0.2;  //0.30000000000000004
0.1 + 0.2 + 0.3;   //0.6000000000000001

加法中的结合律在JavaScript中对于浮点数有时候并不成立:

(0.1 + 0.2) + 0.3;   //0.6000000000000001
0.1 + (0.2 + 0.3);   //0.6

小心浮点数,解决其计算不精确的一个简单策略就是将浮点数转换成整数进行运算,整数的运算是精确的,不用担心舍入误差。

二、当心隐式的强制转换

JavaScript中,运算符+既重载了数字相加,又重载了字符串连接操作,这取决于其参数的类型,简单总结如下:

(1)如果两个操作数都是数值,执行常规加法运算

(2)如果有一个操作数是字符串,则将另一个操作数转换成字符串,再进行字符串的拼接

(3)如果有一个操作数是对象、数值或布尔值,如果 toString 方法存在并且返回原始类型,返回 toString 的结果。如果toString 方法不存在或者返回的不是原始类型,调用 valueOf 方法,如果 valueOf 方法存在,并且返回原始类型数据,返回 valueOf 的结果。其他情况,抛出错误。如果是undefined、null、NaN会调用String()函数取得字符串值’undefined’、’null’、’NaN’,再按照情形(2)进行运算

算数运算符-*/、和%在计算之前都会尝试将其参数转换为数字,简单总结如下:

(1)如果两个操作数都是数值,执行常规运算

(2)如果有一个数是NaN,则结果是NaN

(3)如果有一个操作数字符串、布尔值、null或undefined,则先调用Number()方法将其转换为数值,再进行运算

(4)如果有一个操作数是对象,如果 valueOf 存在,且返回原始类型数据,返回 valueOf 的结果。如果 toString 存在,且返回原始类型数据,返回 toString 的结果。其他情况,抛出错误。再按照上面规则进行运算。

因此,valueOf()toString()方法应该被同时重写,并返回相同的数字字符串或数值表示,才不至于强制隐式转换时得到意想不到的结果。 逻辑运算符||&&可以接受任何值作为参数,会将参数隐式的强制转换成布尔值。JavaScript中有6个假值:false、0、“”、NaN、null和undefined,其他所有的值都为真值。因此在函数中判断参数是否是undefined不能简单的使用if,而应该使用typeof:

function isUndefined(a){
    if (typeof a === 'undefined'){    //或者a === undefined
        console.log('a is not defined')
    }
}

三、避免对混合类型使用==运算符

"1.0e0" == {valueOf: function(){return true}};   //true

相等操作符==在比较两个参数时会参照规则进行隐式转换,判断两个值是否相等,使用全等操作符===是最安全的。j简单总结一下==的隐式转换规则:

四、尽量少用全局对象,始终声明局部变量

定义全局变量会污染共享的公共命名空间,可能导致意外的命名冲突,不利于模块化,导致程序中独立组件间的不必要耦合。全局变量在浏览器中会被绑定到全局的window对象,添加或修改全局变量会自动更新全局对象,更新全局对象也会自动更新全局全局命名空间。

window.foo;  //undefined
var foo = 'global foo';
window.foo;  //"global foo"
window.foo = 'changed'
foo;  //changed

JavaScript会把没有使用var声明的变量简单地当做全局变量,如果忘记声明局部变量,改变量会被隐式地转变成全局变量。任何时候都应该使用var声明局部变量。

function swap(array, i, j){
    var temp = a[i];    //使用var声明局部变量,否则temp会变成全局变量
    a[i] = a[j];
    a[j] = temp;
}

五、理解变量提升

JavaScript不支持块级作用域,变量定义的作用域并不是离其最近的封闭语句或代码块,而是包含它们的函数。来看一个例子。

function test(params) {
    for(var i = 0; i < 10; i++){
        var params = i;
    }
    return params;
}
test(20);  //9

在for循环中声明了一个局部变量params,由于JavaScript不支持块级作用域,params重新声明了函数参数params,导致最后的结果并不是我们传进去的值。

理解JavaScript变量声明需要把声明变量看作由声明和赋值两部分组成。JavaScript隐式地提升声明部分到封闭函数的顶部,而将赋值留在原地。也就是变量的作用域是整个函数,在=语句出现的位置进行赋值。下面第一种方式会被JavaScript隐式地提升声明部分,等价于第二种方式那样。建议手动提升局部变量的声明,避免混淆。

function f() {                              function f() {
     /*do something*/                           var x;
    //...                                       //...
    {                                           {
        //...                                       //...
        var x = /*...*/                             x = /*...*/
        //...                                       //...
    }                                           }
}                                            }

JavaScript没有块级作用域的一个例外是异常处理,try-catch语句将捕获的异常绑定到一个变量,该变量的作用域只是catch语句块。下面的例子中catch语句块中的x值的改变并没有影响最初声明的x的值,说明该变量的作用域只是catch语句块。

function test(){
    var x = 'var', result = [];
    result.push(x);
    try{
        throw 'exception';
    } catch(x){
        x = 'catch';
    }
    result.push(x);
    return result;
}
test();  //["var", "var"]

六、熟练掌握高阶函数

  高阶函数是那些将函数作为参数或返回值的函数,是一种更为抽象的函数。函数作为参数(其实就是回调函数)在JavaScript中被大量使用:

[3,2,1,1,4,9].sort(function(){
    if(x < y){
        return -1;
    }
    if(x > y){
        return 1;
    }
    return 0;
});   //[1,1,2,3,4,9]

var name = ['tongyang', 'Bob', 'Alice'];
name.map(function(name){
    return name.toUpperCase();
});   //['TONGYANG', 'BOB', 'ALICE']

学会使用高阶函数通常可以简化代码并消除繁琐的样板代码,如果出现重复或者相似的代码,我们可以考虑使用高阶函数。

var aIndex = "a".charCodeAt(0);   //97
var alphabet = "";
for(var i = 0; i < 26; ++i){
    alphabet += String.fromCharCode(aIndex + i)
}
alphabet;  //"abcdefghijklmnopqrstuvwxyz"

var digits = "";
for(var i = 0; i < 10; ++i){
    digits += i;
}
digits;  //0123456789

var random = "";
for(var i = 0; i < 8; ++i){
    random += String.fromCharCode(Math.floor(Math.random() * 26) + aIndex);
}
random;  //atzuvtcz

这三段代码有相同的基本逻辑,按照特定的规则拼接字符串。我们使用高阶函数来重写这段代码

function buildString(number, callback){
    var result = "";
    for(var i = 0; i < number; ++i){
        result += callback(i);
    }
    return result;
}

var aIndex = "a".charCodeAt(0);   //97
var alphabet = buildString(26, function(i){
    return String.fromCharCode(aIndex + i);
});
var digits = buildString(10, function(i){
    return i;
});
var random = buildString(8, function(){
    return String.fromCharCode(Math.floor(Math.random() * 26) + aIndex);
});

相比之下,高阶函数更简捷,逻辑更清晰,掌握高阶函数会提高代码质量,这需要多读优秀的源码,多在项目中实践才能熟练的掌握。

七、在类数组对象上复用通用的数组方法

Array.prototype中的标准方法被设计成其他对象可复用的方法,即使这些对象没有继承Array。

在JavaScript中很常见的类数组对象是DOM中的NodeList。类似document.getElementsByTagName这样的操作会查询Web页面中的节点,并返回NodeList作为搜索的结果。我们可以在NodeLIst对象上面使用通用的数组方法,比如forEach、map、filter。

scriptNodeList = document.getElementsByTagName('script');
[].forEach.call(scriptNodeList, function(node){
    console.log(node.src);
});

类数组对象有两个基本特征:

(1)具有一个整形length属性

(2)length属性大于该对象的最大索引。索引是一个整数,它的字符串表示的是该对象中的一个key

可以用一个对象字面量来创建类数组对象:

var arrayLike = {0: "a", 1: "b", 2: "c", length: 3};
var result = [].map.call(arrayLike, function(s){
    return s.toUpperCase();
});
result;  //["A", "B", "C"]

字符串也可以使用通用的数组方法

var result = [].map.call("abc", function(s){
    return s.toUpperCase();
}); //["A", "B", "C"]

只有一个Array方法不是通用的,即数组连接方法concat。这个方法会检查参数的[[Class]]属性。如果参数是一个真实的数组,则会将该数组的内容连接起来作为结果;否则,参数将以一个单一的元素来连接.

function namesColumn() {
    return ["Names"].concat(arguments);
}
namesColumn('tongyang', 'Bob', 'Frank');  //["Names", Arguments[3]]

可以使用slice方法来达到我们的目的

function namesColumn() {
    return ['Names'].concat([].slice.call(arguments));
}
namesColumn('tongyang', 'Bob', 'Frank');  /*["Names", "tongyang", "Bob", "Frank]*/

在类数组对象上复用通用的数组方法可以极大的减少冗余代码,提高代码质量

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

杨潼的专栏

1 篇文章1 人订阅

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏C语言及其他语言

运算符和表达式

1.基本运算符 C使用运算符(operator)来代表算术运算。例如,+运算符可以使它两侧的值加在一起。如果您觉得术语“运算符”听起来比较奇怪,那么请您记住...

2283
来自专栏信数据得永生

JavaScript 编程精解 中文第三版 四、数据结构:对象和数组

37910
来自专栏liulun

Nim教程【七】

这是国内第一个关于Nim的系列教程 先说废话 很开心,在今天凌晨快一点多的时候拿到了 nim-lang.com;nim-lang.cn;nim-lang.net...

1915
来自专栏iOS技术杂谈

Python Garbage Collection 与 Objective-C ARCPython GC 与 Objective-C ARC

转载请注明出处 https://cloud.tencent.com/developer/user/1605429 Python GC 与 Objective-C...

2817
来自专栏趣学算法

数据结构 第1讲 基础知识

        著名的瑞士科学家N.Wirth教授提出:数据结构+算法=程序。数据结构是程序的骨架,算法则是程序的灵魂。

623
来自专栏闪电gogogo的专栏

【数据结构(C语言版)系列一】 线性表

数据是对客观事物的符号表示,在计算机科学中是指所有能输入到计算机中并被计算机程序处理的符号的总称。

1403
来自专栏GreenLeaves

字符、字符串和文本的处理之Char类型

1262
来自专栏数据魔术师

数据结构-线性表|顺序表|链表(下)

1 1 1 哈喽。各位小伙伴好久不见,热心的小编赶在开学季又来给大家送上满满的干货了。祝大家开心快乐! 继上两次咱们聊了顺序表、单链表、静态链表等知识。那么热爱...

2567
来自专栏IT可乐

Java数据结构和算法(十三)——哈希表

  Hash表也称散列表,也有直接译作哈希表,Hash表是一种根据关键字值(key - value)而直接进行访问的数据结构。它基于数组,通过把关键字映射到数组...

2628
来自专栏逸鹏说道

我为NET狂面试题-基础篇-答案

面向过程: 答案:图片只贴核心代码,完整代码请打开解决项目查看 (答案不唯一,官方答案只供参考,若有错误欢迎提出~) 99乘法表 https://githu...

32713

扫码关注云+社区