JS的语法虽然学完了,ECMAScript 6也看得差不多了,但是浏览器上还是有大量的JS代码看不懂,或者说是看不懂结构。这里记录学习他们的过程
开端
既然有看不懂的东西,而且几乎不知道怎么去搜索,那么就想想制作这样一个需要什么,那么就一定有这样的教程,不出所料,找到了JS插件制作教程,而这里面大部分东西,都是造成了JS有这么多骚写法的罪魁祸首。当然,实际上还不止插件(闭包),为了兼容不同的浏览器,冗杂等,出现了各种各样的骚写法
- 闭包
- 兼容
- 冗杂
插件
所谓插件,就是封装在一个闭包中的一些函数集,但是前端开发与后端开发等不同的地方在于,后端可能自己能够安排命名空间,模组的引用等等。而前端做不到,引入一个JS插件等于将他所有的变量全部引入,这样会导致命名冲突,所以为了解级这样的问题,需要使用全局对象来包装
1 | var plugin = { |
闭包
这里的闭包,和python等中的闭包其实不同(当然本质是一样的),他们的目的稍有些不一样,js中的闭包,其目的是为了封装插件,实现私有作用域,延长内部变量的生命周期,使得插件的函数可以重复调用,而不会有全局污染
1 | ;(function(global, undefined) { |
- 在定义插件之前添加一个分号,可以解决js合并时可能会产生的错误问题;
undefined
在老一辈的浏览器是不被支持的,直接使用会报错,js框架要考虑到兼容性,因此增加一个形参undefined
,就算有人把外面的undefined
定义了,里面的undefined
依然不受影响;- 把
window
对象作为参数传入,是避免了函数执行的时候到外部去查找
这样会有个问题,因为插件不一定用于浏览器,从而没有window
,所以我们还可以这么干,我们不传参数,直接取当前的全局this
对象为作顶级对象用。
1 | ;(function(global, undefined) { |
函数相关骚写法
面对众多浏览器,为了适配他们,js在函数上的骚写法是下足了功夫
IIFE(立即调用函数表达式)
这个也是导致JS骚写法一大堆的罪魁祸首,什么符号都来了,在其他语言中,用括号保住函数啥的简直不会去想,但是在JS里面就有独特的意义,它经常和闭包一起使用,将全局对象传进局部作用域里,通过这种方式,可以不用使用new
也就是不用自己去管理内存,即可调用模组。我们先看看要使用new
的方式,这样的方式还是经常在网站上见到
1 | //自定义类 |
而立即调用函数表达式可以这样写
1 | // 写法一 |
这是一个被称为自执行匿名函数的设计模式,主要包含两部分。
- 第一部分是包围在 圆括号运算符
()
里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。 - 第二部分再一次使用
()
创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。
当然变成表达式的方法不只一种,function
前面加运算符,还有如下骚写法
1 | void function(){...}(); |
其本质是将函数声明转换成函数表达式,消除了javascript引擎识别函数表达式和函数声明的歧义
插件的链式调用
对于前端开发,链式调用在有些时候是一个好东西,如下
1 | $(<id>).show().css('color','red').width(100).height(100) |
其实现方法也很简单,在函数内部返回this
即可
对象合并函数
1 | // 对象合并 |
通过CLASS查找节点函数
1 | // 通过class查找dom |
默认参数
js里面默认参数一直是一个不好搞的地方,毕竟在ES6/ES2015之前没有原生支持,所以就会有这样的写法
1 | function mul(a, b) { |
不过ES6/ES2015之后,就没必要这么麻烦了
1 | function multiply(a, b = 1) { |
但是!!!IE浏览器并不支持,所以…,第二种写法的默认参数并不是那么常见
获得全局作用域
我们来关注一下
1 | _global = (function(){ |
- js中不允许存在独立的函数,变量和常量,它们都是
Global Object
的属性和方法,包括内置的属性和方法 Global Object
实际并不存在,它是由window
充当这个角色,并且这个过程是在js首次加载时进行的- 在一个页面中,首次载入js代码时会创建一个全局执行环境,即全局对象(
window
),并用this
来引用全局对象
eval
调用
1 | (0, eval) |
- 使用逗号操作符,逗号操作符总会返回表达式中的最后一项,所以0是个工具人
()
则属于立刻执行这个表达式,返回eval
,eval传入this
字符串,然后被当做实际的ECMAScript语句来解析(0, eval)
返回的是eval
函数,其目的是兼容较低版本的IE浏览器(不可以直接运行eval
)
1 | eval(); // <-- 调用括号左边的表达式 — "eval" — 计算出一个引用 |
直接和间接调用之后的区别是什么?
间接eval调用
1 | (1, eval)('...') |
间接调用计算出来的是一个值,而不是引用,且间接调用是在全局范围内执行执行代码的
如果一个变量或者其他表达式不在"当前的作用域",那么JavaScript机制会继续沿着作用域链上查找直到全局作用域(global
或浏览器中的window
)如果找不到将不可被使用。
直接eval调用
1 | eval('...') |
eval
和(eval)
计算出的是一个引用
模块化编程
自Nodejs项目创建一来,js的模块化编程就开始了,但是Nodejs是运行在服务器端,而之前js是运行在前端,为了解决他们直接兼容等问题,模块化编程也需要有相应的对策,于是js模块化分为了2个类型
前端的插件一般都是开源的(毕竟你几乎没办法闭源),这样一个插件的开发其实是由很多人共同开发的,每个人开发一部分功能,每个功能都是一个文件,而这样,如何才能合并所有人开发的插件呢?前端一般使用加载器如同:browserify、require
、seajs
,于是只要判断是否存在加载器,有就使用加载器,没有,用使用顶级域对象
require
:node和ES6都支持的引入export/import
:只有ES6支持的导出引入module.exports/exports
:只有node支持的导出
CommonJS规范
这个就是编程语言常见(Common)的那种类型,在CommonJS中,有一个全局性方法require()
,可以通过以下方式加载模块
1 | var math = require('math'); |
CommonJS定义的模块分为
- 模块标识(module)
- 模块定义(exports)
- 模块引用(require)
在一个node执行一个文件时,会给这个文件内生成一个exports
和module
对象,而module
又有一个exports
属性。他们之间的关系如下图,都指向一块内存区域。exports
只是module.exports
的引用,就像C++的引用一样。
graph LR exports-->内存; module.exports-->内存;
- 真正被
require
出去的内容还是module.exports
,和python__all__
比较像 - 尽量都用
module.exports
导出,然后用require导入。
AMD规范
这里的AMD可不是AMD YES的那个AMD,这里的AMD是异步模块定义(Asynchronous Module Definition),是为浏览器环境设计的模块加载的规范,它采用了异步的方式加载模块,模块加载的时候不应该它后面的语句运行,在模块加载结束后,通过回调函数的方式,执行所有依赖这个模块语句。
AMD也采用require()
语句加载模块,但是不同于CommonJS,它要求两个参数:
1 | require([module], callback); |
- 第一个参数:一个数组,里面的成员就是要加载的模块
- 第二个参数:加载成功之后的回调函数
UMD规范
有人对上面2种方式并不满意,于是搞了个先判断当前环境是否支持 CommonJS 规范,若否则再判断是否支持AMD规范。参见jQuery代码。
1 | // jQuery 2.2.0 |
ES6规范
当然,上面毕竟还是进行了判断哪一个规范,作为程序员肯定知道这是不可能令人满意的,于是肯定会继续改,既然现基础上实现不了了,那我们就加!
- 导出
1 | export { |
- 导入
1 | import { a, b, c } from './math'; |
export default
将内容直接导出为一个模块,而不是作为对象的属性。
1 | // 导出 |
webpack
// TODO
原型链
- JavaScript只有一种结构:对象
- 每个实例对象(
object
)都有一个私有属性(称之为__proto__
)指向它的构造函数的原型对象(prototype - 该原型对象也有一个自己的原型对象( proto )
- 层层向上直到一个对象的原型对象为
null
,null
为原型链种最后一个环节
- 每个实例对象(
1 | let f = function () { |
函数签名
//TODO