JS全局变量污染和模块化

前言

用原始方式编写前端JS代码的同学,应该对全局变量的污染深有体会。通常都是这样形成过程:

  • 一个人一个JS文件搞定所有逻辑
  • 后来发现函数之间需要共享状态,然后定义一些全局变量
  • 随着逻辑变得复杂,一个文件太长不便于维护,然后按模块拆成不同文件
  • 随着项目大了,不同人开始负责不同模块。噩梦就开始,不同文件的函数和变量定义是共享的,谁也不知道谁的代码会不会被别人覆盖
  • 暴力点的方法就是通过行政命令,命名加上模块前缀,共享变量统一放在一个文件。如果是函数的局部变量,不用var声明的,查出来一个罚200

造成上面的问题可能由于早期页面都是后端渲染的方式,前端的JS确实只做为工具来用。不同模块的js甚至都不会同时引到一个文件,更不用说打包在一起。随着Ajax,SPA的兴起,前端项目逐渐变大庞大,必须有一种系统的方法解决变量冲突 。JS语言设计之处就借鉴:C(过程式),JAVA(面向对象),Lisp(函数式),解决变量冲突问题早就有方案,只不过没有跟c,java一样通过引入关键字解决。

问题

下面我们是一个简单的变量冲突的例子。文章后面我们将介绍如何使用严格模式,和通过纯JS实现模块隔离,来避免和解决变量冲突的风险和问题。

<html lang="en">
<header>
</header>
<button id="id">id</button>
<script src="main.js"></script>
<script src="sub1.js"></script>
<script src="sub2.js"></script>
</html>
//main 小明
console.log('main init:'+id)
setTimeout(function(){
    console.log("main timeout Id:"+Id)
    console.log("main timeout id:"+id)
//sub1 小李
console.log("sub1 Id:"+Id)
//sub2 小强
console.log("sub2 id:"+id)
//console
main init id:[object HTMLButtonElement]
sub1 Id:9
sub2 Id:8
main timeout Id:9
main timeout Id:8

假设我们认为小明是这次变量污染的受害者,他在main.js里声明了id=5,他期望

  • 第一个log应该是5,实际上是[object HTMLButtonElement],后来发现html有id重复的元素
  • 第二个log应该是5,实际上是9,后来他发现自己写成Id,引用到小李的变量去。
  • 把id修正后,他觉得应该是5,实际上id又被小强修改成8。

后来小明发现,只有把声明放在最前面,并且把<script src=main>放在最后面,才能获取到他预期的结果。

<html lang="en">
<header>
</header>
<button id="id">id</button>
<script src="sub1.js"></script>
<script src="sub2.js"></script>
<script src="main.js"></script>//更换到这
</html>
//main 小明
console.log('main init:'+id)
setTimeout(function(){
    console.log("main timeout id:"+id)
})

不过后面随着项目越做越大,js加载很慢,项目上有人提议在用异步加载<script async>,结果bug又漫天飞。项目痛定思痛决定,寻找系统的解决方案,听说严格模式,webpack,gulp,grunt,require,cmd ,commonjs可以解决这类问题。回想刚上手搞模块化的时候,被眼前一堆工具吓到,都不知如何选择。

这些工具大多配置很复杂,还有各种组合。本文就不细说这些工具,主要想从语言层面如何解决变量污染的问题,如何轻量级实现模块化编程。如果用纯JS就能实现模块化,再使用或者定制上述工具也就不难。

严格模式

全局变量污染的问题很大一部分是由于js过往创建全局变量的语法过于灵活,比如例子:

function add(a,b){
    sum=a+b
    return sum
var res=add(1,3)
console.log(res)//4
console.log(sum)//4

add函数由于开放者疏忽,没有用var声明临时变量,导致sum做为全局变量。

再看下面的例子

function Comp(){
this.a=4
    this.b=5
    this.sum=function(){
return this.a+this.b
var comp= Comp()
console.log(a)//4

由于开发者疏忽没有使用new创建对象(或者压根没理解JS支持面向对象),没有使用new 执行Comp方法时,Comp内部方法的this执行全局,this.a,this.b,this.sum都会产生和覆盖全局变量。
由于JS函数嵌套以及异步回调比较常见,忘记var和this的具体上下文很容易发生,不借助工具光靠规范和行政力量是无法保证的。

严格模式可以很好的解决这个问题,如react等很多大型js框架都以及采用严格模式。采用严格模式只需再头上加上"use strict"。上诉代码就会出现运行时错误。严格模式禁止未声明直接给变量赋值,禁止通过全局this定义给全局变量赋值。

ReferenceError: sum is not defined
TypeError: Cannot set property 'a' of undefined

但是上述严格模式的规则都是运行时决定的,也就是说加载编译期间JS并不会做检查。

//main.js
'use strict'
var i={o:1}
setTimeout(function(){
console.log(i)//{o:4}
})
//sub.js
'use strict'
i={o:4}
//var i={o:4} 跟不声明一样
console.log(i)

假设浏览器先执行main,再执行sub。sub的i并没有声明,这时i已经在main声明过,因此sub的i赋值语句是并不会抛出异常。如果在sub再次声明i, main的i 跟sub的i 也是共享的。

因此严格模式虽然能避免一些变量污染的问题,但还是无法根治。如果要彻底解决这个问题,只能引入模块化的思路,简单点说就是类似java的package和import。

模块化

JS语言设计思路就是用尽量最少的关键字(严重依赖function)来表达丰富的应用设计场景,比如面向对象,模块化,上下文切换都围绕function(closue)做文章。

现在有个需求如下:

有三个js文件,可以异步乱序加载,:main.js 持有一个全局变量total,main循环调用5次 math提供的inc方法,inc内部直接递增main的total变量。main调用完inc方法后,用util的方法打印total值。


<!DOCTYPE html>
<html lang="en">
<header>
</header>
<button id="id">id</button>
<script src="sample/math.js" async></script>
<script src="sample/util.js" async></script>
<script src="sample/main.js" async></script>
</html>
//main
main=this.main?(()=>{throw new Error()})():{};
(function(){
    window.onload=function(event) {
        main.total=0;
        for(var i=0;i<5;i++){
            math.inc()
        util.log(main.total)
})()
//math.js
math=this.math?(()=>{throw new Error()})():{};
(function(){
math.inc=function(){
main.total++
})()
//util.js
util=this.util?(()=>{throw new Error()})():{};