尽量少用全局变量

全局变量污染是一个老生常谈的问题,在es5时代常见的做法就是使用函数作用域隔离,es6时const、let出现,我们还可以直接在块级作用域中声明。

相对于使用,我们通过一些代码来回顾一下全局变量污染所带来的问题。

全局作用域中是存在this的,this就等于window,只是浏览器为了方便,用window来表示全局对象本身。
function sum(x, y) {
    result = x + y;
    return result;
}
console.log(result);  //Uncaught ReferenceError: result is not defined

上述例子中,我们未声明就使用了result变量,代码在一般情况下可以运行,但是如果我们调用了该函数,全局作用域就会存在一个result变量,如果其他地方也是用了相同命名的变量,那么就会产生问题。

function sum(x, y) {
    result = x + y;
    return result;
}
sum(1,2);
console.log(result);  //3

解决办法就是提前声明好变量。

function sum(x, y) {
    let result = x + y;  //var也行
    return result;
}
sum(1,2);
console.log(result);  //Uncaught ReferenceError: result is not defined

另一种情况:

function sum(x, y) {
    var result = res = x + y;
    return result;
}
sum(1, 2);
console.log(res);  //3

我们虽然声明了result,但是有可能会使用连等的方式批量赋值给两个变量,此时根据js运行规则,从右至左的操作符优先级,赋值那行的代码实际是这样:

var result = (res = x + y);

为了避免出现这种问题,我们应该先声明再使用。

变量释放时的副作用

从上面的代码中,我们并没有使用var去明确声明一个变量,此时运行sum函数,全局就会存在对应的变量,但是这种解释存在一些不准确,我们可以将其称为隐式全局变量,它与真正的全局变量会有一些不同。

  • 使用var、let、const创建的全局变量(非函数内创建)不能被删除
  • 隐式全局变量可以删除

隐式全局变量严格意义上讲不是真正的变量,而是全局对象的属性,属性是可以通过delete操作符删除成功的,但变量不可以。

var a = 1;
b = 2;
(function() {
    c = 3;
})();

//删除操作
delete a;
delete b;
delete c;

//测试结果
console.log(typeof a); //number
console.log(typeof b); //undefined
console.log(typeof c); //undefined

在严格模式strict中是不允许未声明就进行赋值操作的,会直接抛出错误。strict模式可以说是对未来模式的一种预演,因为es5本身就是一个过渡的版本,这也是为以后的const、let铺路。

访问全局对象

window对象这个玩意和undefined一样都有一个憨批问题,那就是可以在局部变量被重新声明,然后导致在局部作用域中完全变了味道。

如:

function test() {
    const window = 1;

    console.log(window);
}

test();  //1

那么怎么才能准确的获取到window呢?

var global = (function() {
    return this;
})();

利用函数的闭包特性,在函数中的运行的匿名函数,this指向的是全局对象,这是es3时代设计的一个规则,而this指向window也只是恰好符合这一规则导致的,并不是为它量身定做的,为此,在es5的严格模式中,这个问题被修正。所以,在严格模式中我们无法通过这种方式准确的获取到全局对象。

这里引入另一个点:为啥this会等于window

自运行的一个匿名函数并没有指明它的由谁所调用的,此时无法获取到它的调用对象,此时this是等于null的,es3明确表述,当此类情形发生时,则把global作为this。

在es5严格模式则修正了这个规则,this被赋值undefined。


事实上我们很难去兼容不同环境来获取全局对象,而且不同的全局对象属性也是不同的,我们只能先假设我们的代码只运行在某些环境下,比如js模块,打包时基本都是使用了UMD的打包方式,从而挂载到全局对象上。

以webpack打包axios为例,代码如下:

(function webpackUniversalModuleDefinition(root, factory) {
    if (typeof exports === 'object' && typeof module === 'object')
        module.exports = factory();
    else if (typeof define === 'function' && define.amd)
        define([], factory);
    else if (typeof exports === 'object')
        exports["axios"] = factory();
    else
        root["axios"] = factory();
})(this, function() {
  ...
})

其中typeof exports === 'object' && typeof module === 'object'typeof exports === 'object'是用于cjs模块兼容,原始cjs规范中并没有module.exports,是node添加了该功能。

typeof define === 'function' && define.amd为amd的方式。

root才是挂载到window对象的方式,所以,有时候真的需要准确获取吗?这应该是取决于我们的使用环境。

如果非要准确获取,我们可以参考这篇文章:《用 globalThis 访问全局对象》

里面讲述了不同环境下的全局对象的获取,以及正在提案中的globalThis

for循环

for循环经常用于遍历数组和类数组对象,比如argumentsHTMLColltion对象,通过用法如下:

for (var i = 0; i < arr.length; i++) {
    //具体操作
}

这种模式的问题在于每次遍历时都要去访问数据的长度,这样会使得代码变慢,特别是当arr不是数据,而是HTMLColltion(html容器)时。

获取html容器的方法:

  • document.getElementsByName()
  • document.getElementsByClassName()
  • document.getElementsByTagName()

还有很多其他HTML容器,它们在DOM标准以前就引入了,并一直使用至今,包括有:

  • document.images 获取页面所有img元素
  • document.links 获取页面所有a元素
  • document.forms 获取页面所有form元素
  • document.forms[0].elements 页面第一个from下的所有字段元素

html容器痛点在于它是动态的,当页面新增一个元素时,比如我先获取了一次所有图片元素,然后又js插入了一个新的img元素,之前获取的结果也会获取到这个新插入的元素。

这就导致每次获取length时,是需要重新查询并计算的,而dom的操作时非常耗时的,这就是为什么好的for循环会先将遍历的数组或者数据容器的长度先存起来。

for (var i = 0,len=arr.length; i < len; i++) {
    //具体操作
}

需要注意的是,如果修改了html容器,比如新增了一个dom元素,就需要修改对应存起来的长度。

另一些提升的细节,比如使用逐步递减的方式进行循环,因为同0比较比同数组长度比较,或者同非0数组比较更有效率。

var arr = [],
    i = arr.length;
    
while (i--) {
  //具体操作
}

还有就是使用更少的变量:

var i,arr = [];

for(i=arr.length;i--;) {
  //具体操作
}

我们首先是使用了对数值的判断(上面提到了),第二是省略了一个len变量。

补充:

在for循环中3个判断条件都可以省略的,但是;不能少,这里我们只是把第三个条件省略了,第二个条件本身就兼有第二和第三的功能。

for-in循环

for in循环常常用来遍历非数组对象,使用for in循环也被称之为枚举,从技术上来说,也可以使用它来循环遍历数组,但是不推荐这么使用,因为当这个数组对象的原型添加了新的函数或者属性时,枚举会枚举到该属性,从而导致数据遍历发生逻辑错误,因此推荐数组使用正常的for循环,而for-in用于处理对象。

其实对象也会出现上面数组的这种情况,当我们给对象的原型添加自定义方法时,for-in也可以枚举到该属性。

const a = {
    name: "a",
    age: 1,
};

Object.prototype.test = function() {
    console.log(this);
};

for (let i in a) {
    console.log(i);
}
// name
// age
// test

其原因是因为我们并没有对新增的属性设置不可枚举,而默认是可枚举的。

在es5中引入一个属性描述对象,在之前该功能只存在于系统内部,并未对外抛出,该描述对象存在一个默认值enumerable:true表示该属性可以被遍历,于是乎我们可以通过fon-in遍历到该属性。

补充:

Object.keys并不会获取到test,因为它只会获取自身可枚举属性,而for-in是可以枚举到继承的属性。

我们查看一下原型上对象的描述对象

console.log(Object.getOwnPropertyDescriptor(Object.getPrototypeOf(a), "test"));

// {
//   configurable: true,
//   enumerable: true,  
//   value: function test() {},
//   writable: true,
// }

其中enumerable为true,当我们设置为false时,for-in就获取不到了,但是一般我们可以使用hasOwnProperty的方式。

for (let i in a) {
    if (a.hasOwnProperty(i)) {
        //具体操作
    }
}

hasOwnProperty方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性。

当然通过a对象去使用并不是最稳妥的办法,因为hasOwnProperty是原型继承,是可以被复写的,我们可以直接调用顶级对象的方式去使用。

for (let i in a) {
    if (Object.prototype.hasOwnProperty.call(a, i)) {
        //具体操作
    }
}

精炼一下,我们可以使用一个变量来缓存较长的属性名:

const hasOwnProperty = Object.prototype.hasOwnProperty;
for (let i in a) {
    if (hasOwnProperty.call(a, i)) {
        //具体操作
    }
}

不要增加内置的原型

直接对顶级构造函数:String、Object、Array...的原型增加功能,是一个非常强大的方式,但是有时候也因为过于强大,影响后续的可维性,因为这种做法会使得代码变得更加不可预测,其他的开发者在使用你的代码时可能期望内置的JavaScript方法是原样的,而不希望有一些额外的方法。

此外,直接给原型添加的属性,不使用hasOwnProperty进行过滤,会在枚举中出现,这可能会导致一些混乱。

因此,最好的方式就是不要给内置的原型增加属性,非要加需要遵循以下原则:

  1. 当未来的ECMAScript版本或者JavaScript的具体实现可能将该功能作为一个统一的内置方法时。举例来说,可以添加es6中的方法,用于在旧浏览器中对其支持,在这种情况下,仅仅是提前定义了有用的方法。
  2. 如果检查了自定义的属性或者方法不存在时,但是在其他地方已经实现了该方法,或者我们项目所支持的浏览器JavaScript引擎已经实现了,对于其他不支持的给予兼容。
  3. 准确的用文档记录下来,并和团队交流清楚

如果遇到以上情形,我们可以采用以下模式为原型增加自定义方法:

if (typeof Object.prototype.myMethod !== "function") {
    Object.prototype.myMethod = function() {
        //具体实现
    };
}

避免使用隐式转换

JavaScript在使用比较语法时,会使用隐式转换,这也是为什么false == 0或者"" == 0这类比较会返回true的原因。

为了避免隐式转换导致的混淆不清,请使用===!===进行比较操作。

因为后来进行代码维护的人并不清楚你使用==是无意还是有意的,而使用全等,语义更加清晰,代码行为一致,还减少了隐式转换的额外性能开销。

避免使用eval()

eval的使用会带来一些安全隐患,它会将字符串作为代码解析并执行,这可能导致运行了一些恶意代码,这是非常危险的。

相对于eval还有一些方式也需要注意:setInterval()、setTimeout()、Function;这些在传递参数时也要注意,字符串参数也会导致类似eval的隐患。

setTimeout("console.log(111)", 1000);
// 111

Function与eval比较类似,但是如果非要使用,请使用Function来代替eval,因为这样做有一个好处就是Function运行的代码是存在于该函数的局部空间中运行,这样做可以保证字符串中定义的变量不会自动成为全局变量,当然我们也可以用函数将eval包起来,也是一样的效果。

console.log(typeof a); // undefined
console.log(typeof b); // undefined
console.log(typeof c); // undefined

var jstring = "var a = 1;console.log(a)";
eval(jstring);

jstring = "var b = 2;console.log(b)";
new Function(jstring);

jstring = "var c = 3;console.log(c)";
(function() {
    eval(jstring);
})();

console.log(typeof a); // number
console.log(typeof b); // undefined
console.log(typeof c); // undefined

另一个有点在于Function类似于一个沙盒,无论在哪里执行Function它都仅仅能看到全局作用域,因此对局部作用域的变量影响比较小,而eval则是可以全部访问到。

var a = 3;

(function() {
    var a = 1;
    eval("a=2;");
    console.log(a);  //2  就近原则
})();

console.log(a);  //3

(function() {
    var a = 1;
    Function("a=4")();
    console.log(a);  //1
})();

console.log(a);  //4

那么如何控制Function访问的全局作用域,我们可以使用with来控制,但是危险人员依旧可以通过原型链访问到顶级对象,从而做一些危险操作,所以这也只是一个粗浅的沙箱。

有兴趣可以阅读本博客文章:《JSON 格式化对象,对象中存在函数的解决办法,JSON格式化函数》

里面有对这个粗浅沙箱的详细代码实现。

使用parseInt()的数值约定

parseInt可以从一个字符串中获取数值,该函数的第二个参数是一个进制参数,通常可以忽略该参数,但是最好不要这样做,因为当解析的字符串是从0开始就会出现错误,在es3版本中,0开始的字符串会被当做一个8进制的数,如果数值是一个"09",会因为不是一个合法的8进制,返回一个0,后面的9会被视为不是一个合法数值。但是在es5中,这个问题被修正。

但是我们需要明确一个目标,就是我们到底是需要类型转换还是从一段字符中解析出数值,其中单纯类型转换是非常快的,而parseInt解析会慢一些。

console.time("a");
Number("08");  //+"08"
console.timeEnd("a");

console.time("b");
parseInt("08das gjhsad1");
console.timeEnd("b");

当你复制到现代浏览器测试时你会发现parseInt全方位吊打转换,主要原因是浏览器和node都对其做了优化,所以,这种速度上的优势也仅限于旧时代了。

命名约定

构造函数首字母大写

这个就是大家都通晓的一个标准了,不多做解释。

分割单词

常见的用法就是驼峰写法,驼峰分为大驼峰和小驼峰,区别就是首字母是否大写。我们的函数推荐是小驼峰,大驼峰用于构造函数。

对于那些非函数的变量,该如何命名呢?我觉得大部分人会使用小驼峰命名,我们也可以使用另一种不错的方法,就是所有单词都用小写,并使用下划线分隔各个单词。

例如:first_name、sold_campany_name

而我们的ESMAScript使用驼峰命名法来为方法和属性进行命名。

常量命名

常量命名用于表明该变量在程序生命周期中不可改变,一般都采用全部大写的方式,如:

var MAX_VALUE = 800;

还有一种情况会使用全部大写,就是在给全局变量命名时,这可以使得这些变量很容易被识别出来。

私有成员

在js中是没有私有成员这个功能的,开发者常用的做法就是给属性名开头加上一个下划线

var parson = {
    getName() {
        return this._getFirst() + this._getLast();
    },
    _getFirst() {
        return "a";
    },
    _getLast() {
        return "b";
    }
}

在vue2中开头下划线是很常见的一个做法,在vue3中采用双下划线的方式命名,我们甚至可以约定下滑线作为后缀,用于标识这个属性为私有的。

编写注释

代码的注释是非常重要的,通常人们在深入思考一个问题时,会非常清楚这段代码的工作原理,但是当过去一周后,再次回来看该代码时,可能会花费很长时间来回想这段代码到底是干什么的。

不需要注释一些很明显的代码,例如每一个变量或者每一行都注释,通常有必要对所有的函数,函数参数,返回值和其他有趣不同寻常的算法和技术都记录下来。

设想注释就是未来代码阅读者的一个提示,主需要阅读注释就能明白代码中有哪些函数和属性名,举例来说,当有一段5至6行的代码执行了一个具体的工作,如果有一行描述改功能的注释,那么阅读者就可以略过代码细节。

关于注释并没有严格不变的规则,有时候注释可能会比代码本身还要长,比如正则表达式。

注释的规范可以参考JAVA的注释规范

例:

 /**
     * @param masterId    品牌商Id
     * @param shopId    店铺Id
     * @param skuId        商品skuId
     * @description: 校验商品标识码与店铺的所属关系
     * @return: net.jdcloud.APIECRM.model.ValidateSkuUsingGETResponse
     * @author: niaonao
     * @date: 2020/01/13
     */
    public static ValidateSkuUsingGETResponse validateSkuUsing(String masterId, String shopId, String skuId){
        return null;
    }

vscode可以安装插件:koroFileHeader

配置一下,就可以快捷使用了

分类: JavaScript模式 标签: 注释全局变量JavaScript模式变量释放全局对象for循环for-in循环隐式转换evalparseInt命名约定

评论

暂无评论数据

暂无评论数据

目录