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

编写高质量的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]*/

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

欢迎加入QQ群:374933367,与腾云阁原创作者们一起交流,更有机会参与技术大咖的在线分享!

相关阅读

Kotlin Native 详细体验,你想要的都在这儿
Java 程序员快速上手 Kotlin 11招
JavaScriptCore全面解析 (上篇)


此文已由作者授权腾讯云技术社区发布,转载请注明文章出处
原文链接:https://www.qcloud.com/community/article/560964
获取更多腾讯海量技术实践干货,欢迎大家前往腾讯云技术社区

传播腾讯海量技术实践经验,www.qcloud.com

一、理解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]*/

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

欢迎加入QQ群:374933367,与腾云阁原创作者们一起交流,更有机会参与技术大咖的在线分享!

相关阅读

Kotlin Native 详细体验,你想要的都在这儿
Java 程序员快速上手 Kotlin 11招
JavaScriptCore全面解析 (上篇)


此文已由作者授权腾讯云技术社区发布,转载请注明文章出处
原文链接:https://www.qcloud.com/community/article/560964
获取更多腾讯海量技术实践干货,欢迎大家前往腾讯云技术社区