《javascript设计模式》读书笔记一:编写可维护的代码

一. 尽量少用全局变量

Javascript 使用的是函数作用域,在函数内声明的变量,只有在函数内有效,不能在函数外使用。全局变量则是在函数外部声明,在函数内无需声明就可以使用。

每一个javascript环境都有一个全局对象,在函数外部使用this进行访问。创建的全局变量都归全局对象所有。

在浏览器中,使用window表示全局变量本身

1
2
3
4
myName = "haha"
console.log(myName) //haha
console.log(window.myName) //haha
console.log(window["myName"]) //haha
全局变量的产生

javascript的两个特性总让我们在不知不觉中就创建了全局变量

  1. 可直接使用变量。甚至无需声明

  2. javascript有个隐含全局变量。即任何变量,如果未经声明,就为全局对象所有

    例1:

    1
    2
    3
    4
    function sum() {
    result = x + y;
    return result;
    }

    结果:例子中result未经声明, 归全局对象所有,在一般情况下可以正常使用, 但是在相同的全局命名空间使用了另外的result 变量, 就会有问题。

    带有var声明的链式赋值有可能导致隐含全局变量

    面的例子中你估计想要的结果?

    例2:

    1
    2
    3
    function foo() {
    var a = b = 0
    }

    结果:a是局部变量,b是全局变量

    原因:从右到左的操作符优先级。首先是优先级较高的表达式b=0,此时b未经声明(归全局对象所有)。表达式的返回值是0, 被赋给了var声明的局部变量a。

全局变量的问题
  1. 全局变量存在于同一个全局命名空间内,很有可能发生命名冲突

  2. 变量释放时的副作用

    隐含全局变量与明确定义的全局变量有细微的不同。

    • 使用var创建的全局变量(这类变量在函数外部创建),不可以使用delete操作符撤销变量

    • 不使用var创建的隐含全局变量(即使它是在函数内部创建),也可以使用delete操作符撤销变量

      例3:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      var a = 1;
      b = 2;
      (function foo() {
      c = 3;
      }());
      // 企图删除
      delete a; //false
      delete b; //true
      delete c; //true
      // 测试删除情况
      console.log(typeof a, typeof b, typeof c)
      // "number", "undefined", "undefined"

这说明隐含全局变量不是真正的变量。而是只是全局对象的属性,属性是可以通过delete操作符删除的,但是变量不可以

如何最小化全局变量的数量
  1. 命名空间模式
  2. 即时函数
  3. 使用var声明变量

最重要的方式还是使用var声明。上面的例子改造如下:

例1:

1
2
3
4
function sum() {
var result = x + y; // result 就不会变成全局变量。
return result;
}

例2:

1
2
3
4
function foo() {
var a, b;
a = b = 0; //都是局部变量
}

二. 变量声明提升(凌散变量)的问题

javascript允许在任何地方声明变量,无论在哪里声明,效果都等同于在函数顶部进行声明。这就是变量声明提升。注意:提升的只是声明部分,赋值部分不提升。

1
2
3
4
5
6
7
a = "shu"
function getName() {
console.log(a)
var a = "liqi"
console.log(a)
}
getName();

结果:‘undefined’,’ liqi’

原因:在函数getName作用域内,a被看做函数作用域内的变量。函数中所有的变量声明都会被提升的函数的最顶成,但是赋值部分位置不变。所以导致log出:’undefined’, liqi。

1
2
3
4
5
6
7
8
name = "shu"
function getName() {
var name;
console.log(name)
name = "liqi"
console.log(name)
}
getName();
结论

为了避免的这样的混乱, 我们最好在开始就声明要用的变量。

三. for循环

for循环常用在数组或者类数组对象(伪数组)上面。类对象数组例如:arguments,HTML DOM对象:

1
2
3
document.getElementsByName()
document.getElementsByClassName()
document.getElementsByTagName()
通常的for循环使用:
1
2
3
for(var i = 0; i < arr.length; i++) {
console.log(i); // 处理arr[i]
}

这种写法在于每次循环都需要访问数组的长度。这样会使代码变慢。特别当arr不是数组,是HTML DOM

对象的时候。更耗时。

第一种改进的方案:
1
2
3
for(var i = 0, len = arr.length; i < len; i++) {
console.log(i) // 处理arr[i]
}

这种方式下, 对长度的值只提取一次。但是可以应用的整个循环。

第二种改进的方案:

逐步减到0,因为同0比较比同数组的长度比较(同非0数组)比较更有效率

1
2
3
for(var i = arr.length; i--;) {
console.log(i) // 处理arr[i]
}
两种方式的比较:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var arr = [1,2,3,4]
console.time('同长度比较')
for(var i = 0, len = arr.length; i < len; i++) {
console.log(i)
// 处理arr[i]
}
console.timeEnd('同长度比较')


console.time('同0比较')
for(var i = arr.length; i--;) {
console.log(i)
// 处理arr[i]
}
console.timeEnd('同0比较')

但是也存在一些缺点,就是处理的顺序倒过来了。

四. for-in循环

for-in是用来循环非数组对象的。当遍历对象属性时遇到原型链的属性时,使用hasOwnProperty()方法来过滤是非常重要的。

1
2
3
4
5
6
7
8
var obj = {
name: "shu",
age: 12
}
// 将一个方法clone添加到对象上
if (typeof Object.prototype.clone === "undefined") {
Object.prototype.clone = function() {};
}

为了避免在枚举的时候出现clone()。 需要调用hasOwnproperty()方法来过滤原型链属性。

1
2
3
4
5
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key,':', obj[key] )
}
}

结果: name : shu, age : 12

五.避免使用隐式类型转换

#####1.字符串连接符与算术运算符(+ - * / %)隐式转换规则

1
2
3
4
console.log( 1 + 'true')
console.log( 1 + true)
console.log( 1 + undefined)
console.log( 1 + null)

结果会是什么样的?

转换规则:

​ 1.字符串连接符(“+”两边有一边是字符串):会把其他数据类型调用String()方法然后拼接。

​ 2.运算操作符(除了不是字符串连接符的”+”就都是是运算操作符):会把其他数据类型调用Number()方法转成数字然后做运算。

所以例子的结果是:

1
2
3
4
5
6
7
8
9
10
11
// "+“ 是字符串连接符:String(1) + 'true' = '1true'
console.log( 1 + 'true')

// "+“ 是算术运算符: 1 + Number(true) = 1 + 1 = 2
console.log( 1 + true)

// "+“ 是算术运算符: 1 + Number(undefined) = 1 + NaN = NaN
console.log( 1 + undefined)

// "+“ 是算数运算符: 1 + Number(null) = 1 + 0 = 1
console.log( 1+ null)
2.关系运算符( > < >= <= == != === !==):会把其他数据类型转换成number之后再比较关系
1
2
3
4
5
6
7
8
console.log('2' > 10)
console.log('2' > '10')
console.log('abc' > 'b')
console.log('abc' > 'aad')
console.log(null == undefined)
console.log(undefined == undefined)
console.log(null == null)
console.log(NaN == NaN)

结果会是什么样的呢?

转换规则:

1.关系运算符两边有一边是字符串,会将其使用Number()转成数字,然后比较关系。

2.关系运算符两边都是字符串,两边按照字符串对应的unicode编码(可以使用charCodeAt()查看)转成数字,然后比较。

3.当有多个字符串进行比较,依次从左到右比较。

4.如果数据类型是null,undefined 不是严格比较两者都是相等的。

5.NaN 类型与任何数据类型比较都是NaN

所以例子的结果是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// false  Number(2) > 10 = 2 > 10 = false
console.log('2' > 10);

// true '2'.charCodeAt() > '10'.charCodeAt() = 50 > 49 = true
console.log('2' > '10');
console.log('2'.charCodeAt()); // 50
console.log('10'.charCodeAt()); // 49

// false 先比较a和b: 'a'.charCodeAt() > 'b'.charCodeAt() = 97 > 98 = false,直接得出结果
console.log('abc' > 'b');
console.log('a'.charCodeAt()); // 97
console.log('b'.charCodeAt()); // 98

//true 先比较 a和a, 两者相等。则比较b和a:'b'.charCodeAt() > 'a'.charCodeAt() = 98 > 97 = true
console.log('abc' > 'aad')

// true 特殊情况
console.log(null == undefined)
// false 严格比较
console.log(null === undefined)

// true 特殊情况
console.log(undefined == undefined)

// true 特殊情况
console.log(null == null)

// false 特殊情况
console.log(NaN == NaN)
console.log(NaN === NaN)
结论

JavaScript在语句比较时会执行隐式类型转换(除了严格比较)。 有时候自己理不清。最好避免使用。使用就最好使用严格比较如:===, !==。

六. 避免使用eval()

eval函数的作用: 在当前作用域中执行一段JavaScript代码字符串。

1
2
3
4
5
6
7
8
var one = 1;
function test() {
var one = 2;
eval('one = 3');
return one;
}
console.log(test()); // 3
console.log(one); // 1

不推荐使用使用eval()的原因

1. eval()可以访问和修改它外部作用域的变量
1
2
3
4
5
6
function myFunOne() {
var local = 1;
eval('local = 3; console.log("eval:", local)')
console.log(local) // 3
}
myFunOne();
2.eval() 只在被直接调用并且调用函数就是 eval ()本身时,才在当前作用域中执行, 否则就是在全局作用域执行
1
2
3
4
5
6
7
8
9
var one = 1;
function test() {
var one = 2;
var a = eval; //这里将a变量指向了eval函数的引用
a('one = 3');
return one;
}
console.log(test()); // 2
console.log(one); // 3

这段代码等价于在全局作用域中调用 eval。

3.安全问题

eval 也存在安全问题,因为它会执行任意传给它的代码字符串, 在代码字符串未知或者是来自一个不信任的源时就会有安全问题。

结论

​ 绝对不要使用 eval,任何使用它的代码都会在它的工作方式,性能和安全性方面受到质疑。 如果一些情况必 须使用到 eval 才能正常工作,首先它的设计会受到质疑,这不应该是首选的解决方案。

eval() 的替代方式

new Fuction()构造函数和eval()比较类似。如果一定需要使用eval(),那么可以考虑使用new Fuction()来代替eval()。这样的好处是:new Function()中的代码将在局部函数空间中运行, 因此代码中的任何采用var定义的变量不会自定成为全局变量

1
2
3
4
5
6
7
8
9
console.log(typeof one); // "undefine"
console.log(typeof two); // "undefine"

eval('var one = 1; console.log(one);') // 1

new Function('var two = 2; console.log(two);')() // 2

console.log(typeof one); // number
console.log(typeof two); // "undefined"

注意:

setInterval(),setTimeout,function()等构造函数来传递参数。在特殊情况下,会导致类似eval()的隐患。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function myFun() {
console.log('1111');
}
// 认为 "myFun()" 是可执行的代码 不建议这么写
setInterval("myFun()", 1000)

// 认为 "myFun(1,2,3)" 是可执行的代码 不建议这么写
setInterval("myFun(1,2,3)", 1000);


// 推荐这种方式: 函数的引用
setInterval(myFun, 1000)
setInterval(function() {
myFun(1,2,3);
}, 1000);

// myFun是对函数的直接调用,也就是说当setInterval还没有开始函数func就执行了, 果这个函数没有返回值或者返回值不是可执行的函数或者其他的代码的话,就以上代码而言只会打印一次。
setInterval(myFun(), 1000 )

七. 不要增加内置的原型

增加内置构造函数(Object(),Array(),Fuction()等)的原型,但是这可能会严重影响可维护性。因为这种做法使得代码更加不可预测。其他开发者在使用你的代码的时候可能期望的内置的Javascript方法,而不是期望有一些你自己添加的方法。

并且,给原型添加属性在没有使用hasOwnproperpty()时可能会在循环中出现。这会导致一些混乱。

八. switch模式:

可以使用一下switch模式来提高代码的的可读性和健壮性

1
2
3
4
5
6
7
8
9
10
11
12
13
var type = 0;
var result = '';
switch (type) {
case 0:
result = 0;
break;
case 1:
result = 1;
break;
default:
result = 'default';
break;
}

注意:每一个case 需要有一个明确的break语句

文章作者: 舒小琦
文章链接: https://shuliqi.github.io/2019/05/14/编写可维护的代码(基础技巧)/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 舒小琦的Blog