第一章 基本技巧
尽量少用全局变量
全局变量污染是一个老生常谈的问题,在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循环经常用于遍历数组和类数组对象,比如arguments
和HTMLColltion
对象,通过用法如下:
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
进行过滤,会在枚举中出现,这可能会导致一些混乱。
因此,最好的方式就是不要给内置的原型增加属性,非要加需要遵循以下原则:
- 当未来的ECMAScript版本或者JavaScript的具体实现可能将该功能作为一个统一的内置方法时。举例来说,可以添加es6中的方法,用于在旧浏览器中对其支持,在这种情况下,仅仅是提前定义了有用的方法。
- 如果检查了自定义的属性或者方法不存在时,但是在其他地方已经实现了该方法,或者我们项目所支持的浏览器JavaScript引擎已经实现了,对于其他不支持的给予兼容。
- 准确的用文档记录下来,并和团队交流清楚
如果遇到以上情形,我们可以采用以下模式为原型增加自定义方法:
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
配置一下,就可以快捷使用了
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据