[学习笔记] - JS骚写法大合集

JS的语法虽然学完了,ECMAScript 6也看得差不多了,但是浏览器上还是有大量的JS代码看不懂,或者说是看不懂结构。这里记录学习他们的过程

开端

既然有看不懂的东西,而且几乎不知道怎么去搜索,那么就想想制作这样一个需要什么,那么就一定有这样的教程,不出所料,找到了JS插件制作教程,而这里面大部分东西,都是造成了JS有这么多骚写法的罪魁祸首。当然,实际上还不止插件(闭包),为了兼容不同的浏览器,冗杂等,出现了各种各样的骚写法

  • 闭包
  • 兼容
  • 冗杂

插件

所谓插件,就是封装在一个闭包中的一些函数集,但是前端开发与后端开发等不同的地方在于,后端可能自己能够安排命名空间,模组的引用等等。而前端做不到,引入一个JS插件等于将他所有的变量全部引入,这样会导致命名冲突,所以为了解级这样的问题,需要使用全局对象来包装

1
2
3
4
5
6
7
8
9
var plugin = {
add: function(n1,n2){...},
sub: function(n1,n2){...},
mul: function(n1,n2){...},
div: function(n1,n2){...},
sur: function(n1,n2){...}
}
// 调用
plugin.add(1,2)

闭包

这里的闭包,和python等中的闭包其实不同(当然本质是一样的),他们的目的稍有些不一样,js中的闭包,其目的是为了封装插件,实现私有作用域,延长内部变量的生命周期,使得插件的函数可以重复调用,而不会有全局污染

1
2
3
4
5
6
7
8
;(function(global, undefined) {
var plugin = {
add: function(n1, n2){...}
...
}
// 最后将插件对象暴露给全局对象
'plugin' in global && (global.plugin = plugin);
})(window);
  • 在定义插件之前添加一个分号,可以解决js合并时可能会产生的错误问题;
  • undefined在老一辈的浏览器是不被支持的,直接使用会报错,js框架要考虑到兼容性,因此增加一个形参undefined,就算有人把外面的undefined定义了,里面的undefined依然不受影响;
  • window对象作为参数传入,是避免了函数执行的时候到外部去查找

这样会有个问题,因为插件不一定用于浏览器,从而没有window,所以我们还可以这么干,我们不传参数,直接取当前的全局this对象为作顶级对象用。

1
2
3
4
5
6
7
8
9
10
11
12
;(function(global, undefined) {
"use strict" //使用js严格模式检查,使语法更规范
var plugin = {
add: function(n1, n2){...}
...
}
// 最后将插件对象暴露给全局对象
var _global = (function(){
return this || (0, eval)('this');
}());
!('plugin' in _global) && (_global.plugin = plugin);
}());

函数相关骚写法

面对众多浏览器,为了适配他们,js在函数上的骚写法是下足了功夫

IIFE(立即调用函数表达式)

这个也是导致JS骚写法一大堆的罪魁祸首,什么符号都来了,在其他语言中,用括号保住函数啥的简直不会去想,但是在JS里面就有独特的意义,它经常和闭包一起使用,将全局对象传进局部作用域里,通过这种方式,可以不用使用new也就是不用自己去管理内存,即可调用模组。我们先看看要使用new的方式,这样的方式还是经常在网站上见到

1
2
3
4
5
6
7
8
9
10
11
12
//自定义类
function plugin(){}

//提供默认参数
plugin.prototype.str = "default param";

//提供方法(如果不传参,则使用默认参数)
plugin.prototype.firstFunc = function(str = this.str){
alert(str);
}
//创建"对象"
var p = new plugin();

而立即调用函数表达式可以这样写

1
2
3
4
5
6
7
8
9
10
11
12
// 写法一
var p = (function(){
var str = "default param";
function(str = this.str){
alert(str);
}
})();

//写法二
(function(){
...
}());

这是一个被称为自执行匿名函数的设计模式,主要包含两部分。

  • 第一部分是包围在 圆括号运算符()里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。
  • 第二部分再一次使用()创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。

当然变成表达式的方法不只一种,function前面加运算符,还有如下骚写法

1
2
3
4
5
void function(){...}();
// 或者
!function(){...}();
// 或者
+function(){...}();

其本质是将函数声明转换成函数表达式,消除了javascript引擎识别函数表达式和函数声明的歧义

插件的链式调用

对于前端开发,链式调用在有些时候是一个好东西,如下

1
$(<id>).show().css('color','red').width(100).height(100)

其实现方法也很简单,在函数内部返回this即可

对象合并函数

1
2
3
4
5
6
7
8
9
// 对象合并
function extend(o,n,override) {
for(var key in n){
if(n.hasOwnProperty(key) && (!o.hasOwnProperty(key) || override)){
o[key]=n[key];
}
}
return o;
}

通过CLASS查找节点函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 通过class查找dom
if(!('getElementsByClass' in HTMLElement)){
HTMLElement.prototype.getElementsByClass = function(n, tar){
var el = [],
_el = (!!tar ? tar : this).getElementsByTagName('*');
for (var i=0; i<_el.length; i++ ) {
if (!!_el[i].className && (typeof _el[i].className == 'string') && _el[i].className.indexOf(n) > -1 ) {
el[el.length] = _el[i];
}
}
return el;
};
((typeof HTMLDocument !== 'undefined') ? HTMLDocument : Document).prototype.getElementsByClass = HTMLElement.prototype.getElementsByClass;
}

默认参数

js里面默认参数一直是一个不好搞的地方,毕竟在ES6/ES2015之前没有原生支持,所以就会有这样的写法

1
2
3
4
5
6
7
8
9
10
function mul(a, b) {
b = (typeof b !== 'undefined') ? b : 1;
return a * b;
}

// 这种写法会在b = 0或null的时候BOOM
function mul(a, b) {
b = b || 1;
return a * b;
}

不过ES6/ES2015之后,就没必要这么麻烦了

1
2
3
function multiply(a, b = 1) {
return a * b;
}

但是!!!IE浏览器并不支持,所以…,第二种写法的默认参数并不是那么常见

获得全局作用域

我们来关注一下

1
2
3
_global = (function(){
return this || (0, eval)('this');
}());
  • 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
2
3
4
5
eval(); // <-- 调用括号左边的表达式 — "eval" — 计算出一个引用
(eval)(); // <-- 调用括号左边的表达式 — "(eval)" — 计算出一个引用
(((eval)))(); // <-- 调用括号左边的表达式 — "(((eval)))" — 计算出一个引用
(1,eval)(); // <-- 调用括号左边的表达式 — "(1, eval)" — 计算出一个值
(eval = eval)(); // <-- 调用括号左边的表达式 — "(eval = eval)" — 计算出一个值

直接和间接调用之后的区别是什么?

间接eval调用

1
2
3
4
5
6
7
8
9
10
11
12
13
(1, eval)('...')
(eval, eval)('...')
(1 ? eval : 0)('...')
(__ = eval)('...')
var e = eval; e('...')
(function(e) { e('...') })(eval)
(function(e) { return e })(eval)('...')
(function() { arguments[0]('...') })(eval)
this.eval('...')
this['eval']('...')
[eval][0]('...')
eval.call(this, '...')
eval('eval')('...')

间接调用计算出来的是一个值,而不是引用,且间接调用是在全局范围内执行执行代码的

如果一个变量或者其他表达式不在"当前的作用域",那么JavaScript机制会继续沿着作用域链上查找直到全局作用域(global或浏览器中的window)如果找不到将不可被使用。

直接eval调用

1
2
3
4
5
6
7
8
eval('...')
(eval)('...')
(((eval)))('...')
(function() { return eval('...') })()
eval('eval("...")')
(function(eval) { return eval('...'); })(eval)
with({ eval: eval }) eval('...')
with(window) eval('...')

eval(eval)计算出的是一个引用

模块化编程

自Nodejs项目创建一来,js的模块化编程就开始了,但是Nodejs是运行在服务器端,而之前js是运行在前端,为了解决他们直接兼容等问题,模块化编程也需要有相应的对策,于是js模块化分为了2个类型

前端的插件一般都是开源的(毕竟你几乎没办法闭源),这样一个插件的开发其实是由很多人共同开发的,每个人开发一部分功能,每个功能都是一个文件,而这样,如何才能合并所有人开发的插件呢?前端一般使用加载器如同:browserifyrequireseajs,于是只要判断是否存在加载器,有就使用加载器,没有,用使用顶级域对象

  • require:node和ES6都支持的引入
  • export/import:只有ES6支持的导出引入
  • module.exports/exports:只有node支持的导出

CommonJS规范

这个就是编程语言常见(Common)的那种类型,在CommonJS中,有一个全局性方法require(),可以通过以下方式加载模块

1
2
var math = require('math');
math.add(2,3);

CommonJS定义的模块分为

  • 模块标识(module)
  • 模块定义(exports)
  • 模块引用(require)

在一个node执行一个文件时,会给这个文件内生成一个exportsmodule对象,而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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// jQuery 2.2.0
(function(global, factory) {
// 判断是否支持 CommonJS 规范
if (typeof module === "object" && typeof module.exports === "object") {
/* ... 省略进一步判断是否存在 document ... */
} else {
// 不支持 CommonJS 规范时执行
factory(global);
}
// 判断 window 对象是否存在,若否则代入全局的 this,即 Node 环境下的 global 对象
}(typeof window !== "undefined" ? window : this, function(window, noGlobal) {
/* ... 省略 jQuery 主体 ... */

// 检测是否支持 AMD 规范,如果支持则会 return 为 AMD 模块
if (typeof define === "function" && define.amd) {
define("jquery", [], function() {
return jQuery;
});
}

// 不支持 AMD 规范时才会执行以下代码
// 由于 factory(global) 没有传入 noGlobal,所以 !noGlobal 为 true
// 不支持 CommonJS,也不支持 AMD,则将 jQuery 暴露为全局变量
if (!noGlobal) {
window.jQuery = window.$ = jQuery;
}

return jQuery;
}));

ES6规范

当然,上面毕竟还是进行了判断哪一个规范,作为程序员肯定知道这是不可能令人满意的,于是肯定会继续改,既然现基础上实现不了了,那我们就加!

  • 导出
1
2
3
4
5
6
7
8
9
10
export {
a, b, c
};

// 重命名导出
export {
a as x,
b as y,
c as z
};
  • 导入
1
2
3
4
5
import { a, b, c } from './math';
// 重命名导入
import { a as z } from './math';
// 整体导入
import * as math from './math';
  • export default

将内容直接导出为一个模块,而不是作为对象的属性。

1
2
3
4
5
6
// 导出
let fn = function() {};
export default fn;

// 导入
import $ from './math';

webpack

// TODO

原型链

  • JavaScript只有一种结构:对象
    • 每个实例对象(object)都有一个私有属性(称之为 __proto__)指向它的构造函数的原型对象(prototype
    • 该原型对象也有一个自己的原型对象( proto )
    • 层层向上直到一个对象的原型对象为nullnull为原型链种最后一个环节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let f = function () {
this.a = 1;
this.b = 2;
}

console.log(o.a); // 1
// a是o的自身属性吗?是的,该属性的值为 1

console.log(o.b); // 2
// b是o的自身属性吗?是的,该属性的值为 2
// 原型上也有一个'b'属性,但是它不会被访问到。
// 这种情况被称为"属性遮蔽 (property shadowing)"

console.log(o.c); // 4
// c是o的自身属性吗?不是,那看看它的原型上有没有
// c是o.[[Prototype]]的属性吗?是的,该属性的值为 4

console.log(o.d); // undefined
// d 是 o 的自身属性吗?不是,那看看它的原型上有没有
// d 是 o.[[Prototype]] 的属性吗?不是,那看看它的原型上有没有
// o.[[Prototype]].[[Prototype]] 为 null,停止搜索
// 找不到 d 属性,返回 undefined

函数签名

//TODO

参考文献