高质量JavaScript(ES6-)开发准则建议_1_基础语法章1(f*ck js)

第1条:JS第一条,代码不“规范”,开发两行泪

    这里的规范是指任何一个项目开始之前都要明确代码运行的环境即浏览器版本及其所支持JavaScript版本。如笔者日常都是面向移动端(iPhone、Android等)进行开发因此ES3特性及部分ES5特性都可较为方便的使用,而部分不幸同行还要为IE奔波劳累......

    因此第一条也是最关键一条,开发者一定要明白清楚自己所要面向的代码运行环境,及其所能支持JS版本(即其所支持的ECMAScript标准)。

栗子1 const关键字,在ES6标准之前此关键字并没有纳入到ES标准中,因此它只在部分JavaScript引擎中支持,所以同一行代码在不同环境上运行结果会有差异:

const PI = 3.141592652589793;
PI = "modified!";
PI;

    同样的代码在支持const环境里,运行结果PI为3.141592653589792,部分老版本浏览器中只是将const视为var的代名词,运行结果就会是"modified"。面对此类问题的解决方案就只能要求开发人员针对所要运行的环境采用最低ES版本特性进行开发,不考虑IE的情况下,大部分程序猿基于ES3标准规范进行开发基本能够满足大部分场景。当然对于采用nodejs进行后端开发的开发人员来说,基于ES6进行开发是比较幸福的群体。

题外话:虽然目前很多公司启用了nodejs+babel模式开发前端运行JS代码,并通过babel进行运行环境适配调优,但就个人以往项目经历来看这个也是不能保证万无一失的,开发不能把握好目标运行环境特性还是要做好随时打包回家的考虑。

第2条:巧用“严格模式”

    前端JS开发一直存在一个问题,就是大多数业务场景下开发人员无法像控制JVM(或其他vm)版本那样控制客户端版本以控制自己代码的运行环境。这个很好理解,就好比Google搜索很好用但无法要求用户必须下载安装chrome才能搜索(少数2B前端开发会在这方面会幸福很多),但是或多或少又确实存在对于JS运行环境进行控制地需求(我个人很多时候抓狂地想让浏览器支持插件那样支持前端开发自定义的指定JS运行环境及版本,客观上来讲移动Hybrid APP一定程度上能够满足这样的需求)。经过几大厂(主要是google、微软、Firefox等)在W3C上博弈出的ES5标准中首次确定了严格模式(strict mode),通过它可以禁用一些JS语言中问题较多或易出错的特性(聊胜于无)。

    严格模式说明 引入它相对过去传统模式(又称作马虎模式/稀松模式/懒散模式)会在以下带来如下几方面变化:

    1. 更强的语法控制:一些静默错误会导致抛出错误(如果没有catch就会终止js进程);
    2. (可能)更高的运行效率:此模式下修复了JS引擎难以执行优化的缺陷,因此有时候相同的代码会运行的更快相对传统模式。
    3. 为未来预留:严格模式禁用了ECMAScript未来版本中可能会定义的一些语法,为未来提升预留准备。

开启严格模式方法:

A. 脚本文件级开启使用严格模式

// 整个脚本都开启严格模式的语法
"use strict";
var v = "Hi!  I'm a strict mode script!";

此模式下整个JS脚本文件都会启用严格模式逻辑,但是注意这里是一个巨坑!特别对于采用JS压缩拼接辅助工具的前端项目来说,如果一个启用了严格模式的JS文件与另外一些未启用严格模式的JS文件合并生成压缩JS文件就可能会导致未知异常。因此个人不建议采用此模式开启严格模式,建议采用下面的模式。

B. 函数级开启使用严格模式

function strict(){
  // 函数级别严格模式语法
  'use strict';
  function nested() { return "And so am I!"; }
  return "Hi!  I'm a strict mode function!  " + nested();
}
function notStrict() { return "I'm not strict."; }

注意:就在范例中那样展示的,一个函数开启严格模式其内部定义的任意深度函数也会开启严格模式。

严格模式启用后差异:

1. 更强的语法语义控制:

1.1 不可创建意外地全局变量

//这样做会异常
(function(){
    "use strict";
    nothing = "123";
}();

1.2 传统模式下静默的错误失败赋值在严格模式会抛出Error

"use strict";

// 给不可写属性赋值
var obj1 = {};
Object.defineProperty(obj1, "x", { value: 42, writable: false });
obj1.x = 9; // 抛出TypeError错误

// 给只读属性赋值
var obj2 = { get x() { return 17; } };
obj2.x = 5; // 抛出TypeError错误

// 给不可扩展对象的新属性赋值
var fixed = {};
Object.preventExtensions(fixed);
fixed.newProp = "ohai"; // 抛出TypeError错误

1.3 删除不可删除属性会抛Error

"use strict";
delete Object.prototype; // 抛出TypeError错误

1.4 同一个对象内的属性名称必须唯一(这是个bug,ES6以后已修改删除此限制)

"use strict";
var o = { p: 1, p: 2 }; // !!! 语法错误

注意:理论上最新版本的浏览器都已经删除此限制(15年8月以后),但是一旦启用严格模式还是要考虑“黄金第1条”。另外说明一下,之所以认定它是一个bug并删除此限制理由有多方面,有兴趣可以参考“bug 1041128”了解更多细节,不过在个人实践过程中该规则存给我造成最大的困难在于进行JSON对象初始化(特别是那种有从数据库直接拿出两个表数据,利用属性同名覆盖机制进行数据前端关联的情况,别问我怎么知道的)时真心会出现重复属性名的情况。

1.5 函数参数名不能重复

function sum(a, a, c){ // !!! 语法错误
  "use strict";
  return a + a + c; // 代码运行到这里会出错
}

1.6 禁止八进制数字语法

ECMAScript并不包含八进制语法, 但基本所有的浏览器都支持这种以零(0)开头的八进制语法: 0644 === 420 还有 "\045" === "%".在ECMAScript 6标志中开始支持为一个数字加"0o"的前缀来表示八进制数.

"use strict";
var a = 0o10; // ES6: 八进制  这是ok的


var sum = 015 + // !!! 语法错误
          197 +
          142;

1.7 (ES6开始)禁止对基本数据数据类型值的属性进行设置

ES6(ECMAScript 2015)开始针对基本数据类型(Primitive)的值,即string,number,boolean,null,undefined,symbol (ECMAScript 2015新增)6种基本数据类型的值的数据进行赋值操作会抛出TypeError

(function() {
"use strict";

false.true = "";              //TypeError
(14).sailing = "home";        //TypeError
"with".you = "far away";      //TypeError

})();

2. 简化变量使用以提高使JS编译器能够更好地进行代码优化

2.1 禁用with

"use strict";
var x = 17;
with (obj) // !!! 语法错误
{
  // 如果没有开启严格模式,with中的这个x会指向with上面的那个x,还是obj.x?
  // 如果不运行代码,我们无法知道,因此,这种代码让引擎无法进行优化,速度也就会变慢。
  x;
}

注意:后文会对with这个变态进行更加深入的分析,这里大家所要知道就是别用它!with常见替代方案是将目标对象赋给一个短命名变量,然后访问这个变量上的相应属性.

2.2 严格模式下的 eval 不再为上层范围(surrounding scope,注:包围eval代码块的范围)引入新变量.

在正常模式下,  代码 eval("var x;") 会给上层函数(surrounding function)或者全局引入一个新的变量 x . 这意味着, 一般情况下,  在一个包含 eval 调用的函数内所有没有引用到参数或者局部变量的名称都必须在运行时才能被映射到特定的定义 (因为 eval 可能引入的新变量会覆盖它的外层变量). 在严格模式下 eval 仅仅为被运行的代码创建变量, 所以 eval 不会使得名称映射到外部变量或者其他局部变量:

var x = 17;
var evalX = eval("'use strict'; var x = 42; x");
console.assert(x === 17);
console.assert(evalX === 42);

小提示:虽然个人认为直接禁用eval在严格模式种会是更好的选择,但考虑到这样会导致大量js库dead,所以只能YY了。不过个人建议有选择的情况下永远永远别用eval。

2.3 禁止删除声明变量

"use strict";

var x;
delete x; // !!! 语法错误

eval("var y; delete y;"); // !!! 语法错误

3. 让eval和arguments变的简单(直译如此,个人认为是让它们变的更正常,接近开发直觉)

3.1 任何变量都不能被命名或试图命名为eval和arguments

以下尝试都会失败:

"use strict";
eval = 17;
arguments++;
++eval;
var obj = { set p(arguments) { } };
var eval;
try { } catch (arguments) { }
function x(eval) { }
function arguments() { }
var y = function eval() { };
var f = new Function("arguments", "'use strict'; return 17;");

3.2 参数的值不会随 arguments 对象的值的改变而变化

假设一个函数第一个参数名叫arg。严格模式下,函数的 arguments 对象会保存函数被调用时的原始参数。arguments[i] 的值不会随与之相应的参数arg的值的改变而变化,同名参数的值也不会随与之相应的 arguments[i] 的值的改变而变化。

function f(a){
  "use strict";
  a = 42;
  return [a, arguments[0]];
}
var pair = f(17);
console.assert(pair[0] === 42);
console.assert(pair[1] === 17);

3.3 不再支持 arguments.callee【笔者表示严重不适】

按照官方说法,正常模式下,arguments.callee 指向当前正在执行的函数。这个作用很小:直接给执行函数命名就可以了!此外,arguments.callee 十分不利于优化,例如内联函数,因为 arguments.callee 会依赖对非内联函数的引用。在严格模式下,arguments.callee 是一个不可删除属性,而且赋值和读取时都会抛出异常:

'use strict';
var f = function(b) { if(b) return arguments.callee(false); };
f(true); // throws a TypeError

小提醒:笔者表示对不再支持arguments.callee表示遗憾,阅读过《JavaScript高级程序涉及(第3版)》中“5.5.4 函数内部属性”应该知道函数名称本身也只是函数对象的一个索引而已,除非禁止函数对象重命名否则永远存在函数名被篡改和失效的可能,因此对于递归调用需求中个人是偏向推荐arguments.callee调用的,通过此方式可以避免函数的执行与函数名称强关联。

4. 更加安全的JS

4.1 严格模式下传递给函数的值不会被强制转换为对象(也就是常说“自动装箱”)

对一个普通的函数来说,this总会是一个对象:不管调用时this它本来就是一个对象;还是用布尔值,字符串或者数字调用函数时函数里面被封装成对象的this;还是使用undefined或者null调用函数式this代表的全局对象(使用callapply或者bind方法来指定一个确定的this)。这种自动转化为对象的过程不仅是一种性能上的损耗,同时在浏览器中暴露出全局对象也会成为安全隐患,因为全局对象提供了访问那些所谓安全的JavaScript环境必须限制的功能的途径。所以对于一个开启严格模式的函数,指定的this不再被封装为对象,而且如果没有指定this的话它值是undefined.

"use strict";
function fun() { return this; }
console.assert(fun() === undefined);
console.assert(fun.call(2) === 2);
console.assert(fun.apply(null) === null);
console.assert(fun.call(undefined) === undefined);
console.assert(fun.bind(true)() === true);

4.2 通过函数名直接访问caller及arguments会受到限制,以保证方法堆栈的安全

function restricted()
{
  "use strict";
  restricted.caller;    // 抛出类型错误
  restricted.arguments; // 抛出类型错误
}
function privilegedInvoker()
{
  return restricted();
}
privilegedInvoker();

4.3 出于安全考虑禁用arguments.callee

在一些旧时的ECMAScript实现中arguments.caller是一个对象,它里面存储的属性指向那个函数的变量。这是一个安全隐患,因为它通过函数抽象打破了本来被隐藏起来的保留值;它同时也是引起大量优化工作的原因。出于这些原因,现在没有浏览器去实现它。但是因为它这种历史遗留的功能,arguments.caller在严格模式下同样是一个不可被删除的属性,在赋值或者取值时会报错。

"use strict";
function fun(a, b)
{
  "use strict";
  var v = 12;
  return arguments.caller; // 抛出类型错误
}
fun(1, 2); // 不会暴露v(或者a,或者b)

5. 为未来ES版本迭代预留空间

5.1 关键字保护

在严格模式中,关键字(implementsinterfaceletpackageprivateprotectedpublicstaticyield)不能用于变量名或者形参名。

5.2 禁止了不在脚本或者函数层面上的函数声明

"use strict";
if (true){
  function f() { } // !!! 语法错误
  f();
}

for (var i = 0; i < 5; i++){
  function f2() { } // !!! 语法错误
  f2();
}

function baz() { // 合法
  function eit() { } // 同样合法
}

小提醒:对于采用了惰性模式(layier)及运行时初始化方法逻辑的项目团队来说,这可能不是一个好消息。理论上最好要求函数体内不能存在索引可变的匿名函数实现方案。

使用严格模式的正确姿势:

1. 谨慎考量项目需求及团队人员知识情况作为判断是否要启用严格模式的重要依据(如果是一堆面向IE的历史遗留坑就呆着别动了);

2. 如果确认启用严格模式,除非你及你的团队确定知道你们在干什么且确定会进行深远的坚持和改造,否则请使用函数级启用方案;

3. 主流浏览器现在实现了严格模式。但是不要盲目的依赖它,因为市场上仍然有大量的浏览器版本只部分支持严格模式或者根本就不支持(比如IE10之前的版本);

4. 严格模式改变了语义,依赖这些改变可能会导致没有实现严格模式的浏览器中出现问题或者错误,同样反之也存在同样的可能。因此启用严格模式后的测试过程中需要在支持或者不支持严格模式的浏览器中测试你的代码。

5. 如果采用严格模式,建议通过引入babel、lint等工具能够带来效率及可靠性的提升(内流满面的说道)。

最后的想法:当前(2019年)来看严格模式没有那么好也没有那么糟,还处于鸡肋向鸡胸脯升级过程中。不过在后续ES标准中不出意外(不要出现微软那种一手好牌往死里作的黑天鹅话)肯定会对它进行不断增强。

参考:

1. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode

2. 《JavaScript高级程序涉及(第3版)》

3. 《Effective JavaScript 68 Specific Ways to Harness the Power of JavaScript》

版权说明:本文写作只用于总结梳理个人日常开发学习经验教训,欢迎非商业性质转载引用但请标明出处,如发现非经作者本人容许的商业行为,作者会维护自身相关合法权益。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值