越前君

越前君 查看完整档案

广州编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

很少在 Segmentfault 写文章,主要都在简书上更新搬砖日记。

简书主页 👉 https://www.jianshu.com/u/f4d...

个人动态

越前君 赞了文章 · 4月7日

从一个误写的逗号谈开去——JS代码是如何被压缩的

故事起源于一个很小问题,我写了个代码,被质疑有问题:简化之后大概如下:

let a;
const x = { b: 123 };
a = 123,
delete x

被质疑的主要原因是第三行a=123的后面为什么是逗号,不是分号。坦白来说,我是简单的手误,将分号错写成了逗号。但是感觉貌似应该也没有什么问题,毕竟uglifyjs会将某些语句进行合并,将分号变成逗号。继而再一想,uglifyjs是如何来进行代码压缩的、它是如何知道该合并哪些语句,不合并哪些语句的、 它又有哪些合并规则?于是有了本文。

1. AST(抽象语法树)

要想了解JS的压缩原理,需要首先了解AST。

抽象语法树:AST(Abstract Syntax Tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。之所以说语法是「抽象」的,是因为这里的语法并不会表示出真实语法中出现的每个细节。

举个例子:

image.png

image.png

从上面两个例子中,可以看出AST是源代码根据其语法结构,省略一些细节(比如:括号没有生成节点),抽象成树形表达。抽象语法树在计算机科学中有很多应用,比如编译器、IDE、压缩代码、格式化代码等。[1]

2. 代码压缩原理

了解了AST之后,我们再分析一下JS的代码压缩原理。简单的说,就是

1. 将code转换成AST
2. 将AST进行优化,生成一个更小的AST
3. 将新生成的AST再转化成code

PS:具体的AST树大家可以在astexplorer上在线获得

babel,eslint,v8的逻辑均与此类似,下图是我们引用了babel的转化示意图:
1.jpg

以我们之前被质疑的代码为例,看看它在uglify中是怎么样一步一步被压缩的:

// uglify-js的版本需要为2.x, 3.0之后uglifyjs不再暴露Compressor api
// 2.x的uglify不能自动解析es6,所以这里先切换成es5
// npm install uglify-js@2.x
var UglifyJS = require('uglify-js');

// 原始代码
var code = `var a;
var x = { b: 123 };
a = 123,
delete x`;

// 通过 UglifyJS 把代码解析为 AST
var ast = UglifyJS.parse(code);
ast.figure_out_scope();

// 转化为一颗更小的 AST 树
compressor = UglifyJS.Compressor();
ast = ast.transform(compressor);

// 再把 AST 转化为代码
code = ast.print_to_string();

// var a,x={b:123};a=123,delete x;
console.log("code", code);

到这里,我们已经了解了uglifyjs的代码压缩原理,但是还没有解决一个问题——为什么某些语句间的分号会被转换为逗号,某些不会转换。这就涉及到了uglifyjs的压缩规则。

3. 代码压缩规则

由于uglifyjs的代码压缩规则很多,我们这里只分析与本文中相关的部分:

uglifyjs的全部压缩规则可以参见:《[解读uglifyJS(四)——Javascript代码压缩](https://rapheal.sinaapp.com/2014/05/22/uglifyjs-squeeze/#more-705)》
连续的"表达式语句"可以合并成一个逗号表达式

image.png

PS:在线demo

这其中需要注意的是只有“表达式语句”才能被合并,那么什么是表达式语句呢?

表达式 VS 语句 VS 表达式语句

表达式:表达式都会返回一个值,可以放在任何一个需要值的地方

例如:

    a; //返回a的值
    b + 3; // 返回b+3的结果
语句:语句是一个行为,通常利用一个或多个关键字来完成给定的任务。程序由一系列语句构成。其中流控制语句有:if/while/for等。

例如:

    if(x > 0) {
      ...
    }
    for(var i = 0;i < arr.length; i ++) {
      ...
    }
    const a = 123;
表达式语句:既是表达式,又是语句

例如:

    A();
    function() {}();
    delete x.b;
    b = b + 3;

综上所述,因为a = 123 和 delete x都是表达式语句,所以分号被转换为逗号。而var x = {b:123}则因为是声明语句,所以和a=123不会合并,分号不会被转换。但var x = {b:123}和第一行var a又触发了另外一条规则,

多个var声明可以压缩成一个var声明

所以第一行和第二行会被合并为var a,x={b:123}

4. 总结

在本文中,我们讨论了什么是抽象语法树,uglifyjs的压缩原理,以及相应的压缩规则,最终明晰了为什么代码会被压缩成我们得到的样子,希望对大家有所帮助。

参考文献

[1]《抽象语法树在 JavaScript 中的应用
[2]《javascript 代码是如何被压缩的
[3]《[译]JavaScript中:表达式和语句的区别
[4]《解读uglifyJS(四)——Javascript代码压缩

查看原文

赞 17 收藏 11 评论 0

越前君 赞了文章 · 4月7日

UglifyJS3中文文档

UglifyJS3中文文档

译者:李平海

转载请注明原文链接(https://github.com/LiPinghai/... )与作者信息。

译序

此前翻译的UglifyJS2中文文档发布没多久UglifyJS3就发布了,囧,现在把本文档也更新成UglifyJS3版本。

与UglifyJS2相比API变动较大,简化较多,文档也增加了不少示例。

由于webpack本身集成了UglifyJS插件(webpack.optimize.UglifyJsPlugin),其命令webpack -p即表示调用UglifyJS来压缩代码,还有不少webpack插件如html-webpack-plugin也会默认使用UglifyJS。因此我们其实经常要用到它,但UglifyJS本身配置较复杂/选项繁多,又没有中文文档,使用起来如坠云雾。鉴于此特翻译此文,谬误甚多,敬请斧正。

词典:

parse       解释
compress    压缩
mangle      混淆
beautify    美化
minify      最小化
CLI         命令行工具
sourcemap   编译后代码对源码的映射,用于网页调试
AST         抽象语法树
name        名字,包括变量名、函数名、属性名
toplevel    顶层作用域
unreachable 不可达代码
option      选项/配置
STDIN       标准输入,指在命令行中直接输入
STDOUT      标准输出
STDERR      标准错误输出
side effects函数副作用,即函数除了返回外还产生别的作用,比如改了全局变量
shebang     释伴(#!)

以下为正文:

UglifyJS 3

UglifyJS 是一个js 解释器、最小化器、压缩器、美化器工具集(parser, minifier, compressor or beautifier toolkit)。

注意:

  • uglify-js@3APICLI已简化,不再向后兼容 uglify-js@2.

  • UglifyJS 2.x 文档在这里.

  • uglify-js 只支持 ECMAScript 5 (ES5).

  • 假如希望压缩 ES2015+ (ES6+)代码,应该使用 uglify-es这个npm 包。

安装

首先确认一直你已经安装了最新的node.js(装完后或许需要重启一下电脑)

用NPM安装CLI:

npm install uglify-js -g

用NPM下载给程序使用:

npm install uglify-js

CLI使用

Command line usage

 uglifyjs [input files] [options]

UglifyJS可以输入多文件。建议你先写输入文件,再传选项。UglifyJS会根据压缩选项,把文件放在队列中依次解释。所有文件都会在同一个全局域中,假如一个文件中的变量、方法被另一文件引用,UglifyJS会合理地匹配。

假如没有指定文件,UglifyJS会读取输入字符串(STDIN)。

如果你想要把选项写在文件名的前面,那要在二者之前加上双横线,防止文件名被当成了选项:

 uglifyjs --compress --mangle -- input.js

CLI选项:

Command line options

  -h, --help                  列出使用指南。
                              `--help options` 获取可用选项的详情。
  -V, --version               打印版本号。
  -p, --parse <options>       指定解析器配置选项:
                              `acorn`  使用 Acorn 来解析。
                              `bare_returns`  允许在函数外return。
                                              在压缩CommonJS模块或`.user.js `引擎调用被同步执行函数包裹的用户脚本 时会用到。
                              `expression`  不是解析文件,二是解析一段表达式 (例如解析JSON).
                              `spidermonkey`  输入文件是 SpiderMonkey
                                              AST 格式 (JSON).
  -c, --compress [options]    启用压缩(true/false)/指定压缩配置:
                              `pure_funcs`  传一个函数名的列表,当这些函数返回值没被利用时,该函数会被安全移除。
  -m, --mangle [options]       启用混淆(true/false)/指定混淆配置:
                              `reserved`  不被混淆的名字列表。
  --mangle-props [options]    混淆属性/指定压缩配置:
                              `builtins`  混淆那些与标准JS全局变量重复的名字。
                              `debug`  添加debug前缀和后缀。
                              `domprops`  混淆那些鱼DOM属性名重复的名字。
                              `keep_quoted`  只混淆没括起来的属性名。
                              
                              `regex`  只混淆匹配(该正则)的名字。
                              `reserved`  不需要混淆的名字的列表(即保留)。
  -b, --beautify [options]    是否美化输出(true/false)/指定输出配置:
                              `beautify`  默认是启用.
                              `preamble`  预设的输出文件头部。你可以插入一段注释,比如版权信息。它不会被解析,但sourcemap会因此调整。
                              `quote_style`  括号类型:
                                              0 - auto自动
                                              1 - single单引号
                                              2 - double双引号
                                              3 - original跟随原码
                              `wrap_iife`  把立即执行函数括起来。注意:你或许应禁用压缩配置中的`negate_iife`选项。 

 -o, --output <file>         输出文件路径 (默认 STDOUT). 指定 `ast` 或
                                `spidermonkey`的话分别是输出UglifyJS或SpiderMonkey AST。
    --comments [filter]         保留版权注释。默认像Google Closure那样,保留包含"@license"或"@preserve"这样JSDoc风格的注释。你可以传以下的参数:
                                - "all" 保留全部注释
                                - 一个合适的正则,如 `/foo/` 或 `/^!/`,保留匹配到的注释。 
                                注意,在启用压缩时,因为死代码被移除或压缩声明为一行,并非*所有*的注释都会被保留。
    --config-file <file>        从此JSON文件读取 `minify()` 配置。
    -d, --define <expr>[=value] 定义全局变量。
    --ie8                       支持IE8。
                                等同于在`minify()`的`compress`、 `mangle` 和 `output`配置设置`ie8: true`。UglifyJS不会默认兼容IE8。
    --keep-fnames               不要混淆、干掉的函数的名字。当代码依赖Function.prototype.name时有用。
    --name-cache <file>         用来保存混淆map的文件。
    --self                      把UglifyJS本身也构建成一个依赖包
                                (等同于`--wrap UglifyJS`)
    --source-map [options]      启用 source map(true/false)/指定sourcemap配置:
                                `base` 根路径,用于计算输入文件的相对路径。
                                `content`  输入sourcemap。假如的你要编译的JS是另外的源码编译出来的。
                                假如该sourcemap包含在js内,请指定"inline"。 
                                `filename`  输出文件的名字或位置。
                                `includeSources`  如果你要在sourcemap中加上源文件的内容作sourcesContent属性,就传这个参数吧。
                                `root`  此路径中的源码编译后会产生sourcemap.
                                `url`   如果指定此值,会添加sourcemap相对路径在`//#sourceMappingURL`中。
    --timings                   在STDERR显示操作运行时间。
    --toplevel                  压缩/混淆在最高作用域中声明的变量名。
    --verbose                   打印诊断信息。
    --warn                      打印警告信息。
    --wrap <name>               把所有代码包裹在一个大函数中。让“exports”和“global”变量有效。
                                你需要传一个参数来指定此模块的名字,以便浏览器引用。         

指定--output (-o)来明确输出文件,否则将在终端输出(STDOUT)

CLI sourcemap选项

CLI source map options

UglifyJS可以生成一份sourcemap文件,这非常有利于你调试压缩后的JS代码。传--source-map --output output.js来获取sorcemap文件(sorcemap会生成为output.js.map)。

额外选项:

  • --source-map filename=<NAME> 指定sourcemap名字。

  • --source-map root=<URL> 传一个源文件的路径。否则UglifyJS将假定已经用了HTTPX-SourceMap,并将省略//#sourceMappingURL=指示。

  • --source-map url=<URL> 指定生成sourcemap的路径。

例如:

    uglifyjs js/file1.js js/file2.js \
             -o foo.min.js -c -m \
             --source-map root="http://foo.com/src",url=foo.min.js.map

上述配置会压缩和混淆file1.jsfile2.js,输出文件foo.min.js 和sourcemapfoo.min.js.map,sourcemap会建立http://foo.com/src/js/file1.js
http://foo.com/src/js/file2.js的映射。(实际上,sourcemap根目录是http://foo.com/src,所以相当于源文件路径是js/file1.jsjs/file2.js

关联sourcemap

Composed source map

假如你的JS代码是用其他编译器(例如coffeescript)生成的,那么映射到JS代码就没什么用了,你肯定希望映射到CoffeeScript源码。UglifyJS有一个选项可以输入sourcemap,假如你有一个从CoffeeScript → 编译后JS的map的话,UglifyJS可以生成一个从CoffeeScript->压缩后JS的map映射到源码位置。

你可以传入 --source-map content="/path/to/input/source.map"或来尝试此特性,如果sourcemap包含在js内,则写 --source-map content=inline

CLI混淆选项

CLI mangle options

你需要传入--mangle (-m)来使启用混淆功能。支持以下选项(用逗号隔开):

  • toplevel — 混淆在最高作用域中声明的变量名(默认disabled)

  • eval - 混淆在evalwith作用域出现的变量名(默认disabled)

当启用混淆功能时,如果你希望保留一些名字不被混淆,你可以用--mangle reserved 声明一些名字(用逗号隔开)。例如:

 uglifyjs ... -m reserved=[$,require,exports]'

这样能防止require, exports$被混淆改变。

CLI混淆属性名 (--mangle-props)

CLI mangling property names (--mangle-props)

警告:这能会搞崩你的代码。混淆属性名跟混淆变量名不一样,是相互独立的。传入--mangle-props会混淆对象所有可见的属性名,除了DOM属性名和JS内置的类名。例如:

// example.js
var x = {
    baz_: 0,
    foo_: 1,
    calc: function() {
        return this.foo_ + this.baz_;
    }
};
x.bar_ = 2;
x["baz_"] = 3;
console.log(x.calc());

混淆所有属性(除了JS内置的):

$ uglifyjs example.js -c -m --mangle-props
var x={o:0,_:1,l:function(){return this._+this.o}};x.t=2,x.o=3,console.log(x.l());

混淆除了 reserved (保留)外的所有属性:

$ uglifyjs example.js -c -m --mangle-props reserved=[foo_,bar_]
var x={o:0,foo_:1,_:function(){return this.foo_+this.o}};x.bar_=2,x.o=3,console.log(x._());

混淆匹配regex(正则)的属性:

$ uglifyjs example.js -c -m --mangle-props regex=/_$/
var x={o:0,_:1,calc:function(){return this._+this.o}};x.l=2,x.o=3,console.log(x.calc());

混用多个混淆属性选项:

$ uglifyjs example.js -c -m --mangle-props regex=/_$/,reserved=[bar_]
var x={o:0,_:1,calc:function(){return this._+this.o}};x.bar_=2,x.o=3,console.log(x.calc());

为了混淆正常使用,我们默认避免混淆标准JS内置的名字(--mangle-props builtins可以强制混淆)。

tools/domprops.json 里有一个默认的排除名单,包括绝大部分标准JS和多种浏览器中的DOM属性名。传入--mangle-props domprops 可以让此名单失效。

可以用正则表达式来定义该混淆的属性名。例如--mangle-props regex=/^_/,只混淆下划线开头的属性。

当你压缩多个文件时,为了保证让它们最终能同时工作,我们要让他们中同样的属性名混淆成相同的结果。传入`--name-cache
filename.json`,UglifyJS会维护一个共同的映射供他们复用。这个json一开始应该是空的,例如:

$ rm -f /tmp/cache.json  # start fresh
$ uglifyjs file1.js file2.js --mangle-props --name-cache /tmp/cache.json -o part1.js
$ uglifyjs file3.js file4.js --mangle-props --name-cache /tmp/cache.json -o part2.js

这样part1.jspart2.js会知晓对方混淆的属性名。

假如你把所有文件压缩成同一个文件,那就不需要启用名字缓存了。

混淆没括起来的名字(--mangle-props keep_quoted)

Mangling unquoted names (--mangle-props keep_quoted)

使用括号属性名 (o["foo"])以保留属性名(foo)。这会让整个脚本中其余此属性的引用(o.foo)也不被混淆。例如:

// stuff.js
var o = {
    "foo": 1,
    bar: 3
};
o.foo += o.bar;
console.log(o.foo);
$ uglifyjs stuff.js --mangle-props keep_quoted -c -m
var o={foo:1,o:3};o.foo+=o.o,console.log(o.foo);

调试属性名混淆

Debugging property name mangling

为了混淆属性时不至于完全分不清,你可以传入--mangle-props debug来调试。例如o.foo会被混淆成o._$foo$_。这让源码量大、属性被混淆时也可以debug,可以看清混淆会把哪些属性搞乱。

$ uglifyjs stuff.js --mangle-props debug -c -m
var o={_$foo$_:1,_$bar$_:3};o._$foo$_+=o._$bar$_,console.log(o._$foo$_);

你可以用--mangle-props-debug=XYZ来传入自定义后缀。让o.foo 混淆成 o._$foo$XYZ_, 你可以在每次编译是都改变一下,来辨清属性名怎么被混淆的。一个小技巧,你可以每次编译时传随机数来模仿混淆操作(例如你更新了脚本,有了新的属性名),这有助于识别混淆时的出错。

API参考

API Reference

假如是通过NPM安装的,你可以在你的应用中这样加载UglifyJS:

var UglifyJS = require("uglify-js");

这输出一个高级函数minify(code, options),它能根据配置,实现多种最小化(即压缩、混淆等)。 minify()默认启用压缩和混淆选项。例子:

var code = "function add(first, second) { return first + second; }";
var result = UglifyJS.minify(code);
console.log(result.error); // runtime error, or `undefined` if no error
console.log(result.code);  // minified output: function add(n,d){return n+d}

你可以通过一个对象(key为文件名,value为代码)来同时最小化多个文件:

var code = {
    "file1.js": "function add(first, second) { return first + second; }",
    "file2.js": "console.log(add(1 + 2, 3 + 4));"
};
var result = UglifyJS.minify(code);
console.log(result.code);
// function add(d,n){return d+n}console.log(add(3,7));

toplevel选项例子:

var code = {
    "file1.js": "function add(first, second) { return first + second; }",
    "file2.js": "console.log(add(1 + 2, 3 + 4));"
};
var options = { toplevel: true };
var result = UglifyJS.minify(code, options);
console.log(result.code);
// console.log(3+7);

nameCache 选项例子:

var options = {
    mangle: {
        toplevel: true,
    },
    nameCache: {}
};
var result1 = UglifyJS.minify({
    "file1.js": "function add(first, second) { return first + second; }"
}, options);
var result2 = UglifyJS.minify({
    "file2.js": "console.log(add(1 + 2, 3 + 4));"
}, options);
console.log(result1.code);
// function n(n,r){return n+r}
console.log(result2.code);
// console.log(n(3,7));

你可以像下面这样把名字缓存保存在文件中:

var cacheFileName = "/tmp/cache.json";
var options = {
    mangle: {
        properties: true,
    },
    nameCache: JSON.parse(fs.readFileSync(cacheFileName, "utf8"))
};
fs.writeFileSync("part1.js", UglifyJS.minify({
    "file1.js": fs.readFileSync("file1.js", "utf8"),
    "file2.js": fs.readFileSync("file2.js", "utf8")
}, options).code, "utf8");
fs.writeFileSync("part2.js", UglifyJS.minify({
    "file3.js": fs.readFileSync("file3.js", "utf8"),
    "file4.js": fs.readFileSync("file4.js", "utf8")
}, options).code, "utf8");
fs.writeFileSync(cacheFileName, JSON.stringify(options.nameCache), "utf8");

综合使用多种minify()选项的例子:

var code = {
    "file1.js": "function add(first, second) { return first + second; }",
    "file2.js": "console.log(add(1 + 2, 3 + 4));"
};
var options = {
    toplevel: true,
    compress: {
        global_defs: {
            "@console.log": "alert"
        },
        passes: 2
    },
    output: {
        beautify: false,
        preamble: "/* uglified */"
    }
};
var result = UglifyJS.minify(code, options);
console.log(result.code);
// /* uglified */
// alert(10);"

生成警告提示:

var code = "function f(){ var u; return 2 + 3; }";
var options = { warnings: true };
var result = UglifyJS.minify(code, options);
console.log(result.error);    // runtime error, `undefined` in this case
console.log(result.warnings); // [ 'Dropping unused variable u [0:1,18]' ]
console.log(result.code);     // function f(){return 5}

生成错误提示:

var result = UglifyJS.minify({"foo.js" : "if (0) else console.log(1);"});
console.log(JSON.stringify(result.error));
// {"message":"Unexpected token: keyword (else)","filename":"foo.js","line":1,"col":7,"pos":7}

Note: unlike uglify-js@2.x, the 3.x API does not throw errors. To
achieve a similar effect one could do the following:

var result = UglifyJS.minify(code, options);
if (result.error) throw result.error;

最小化选项

Minify options

  • warnings (default false) — 传 true的话,会在result.warnings中返回压缩过程的警告。传 "verbose"获得更详细的警告。

  • parse (default {}) — 如果你要指定额外的解析配置parse options,传配置对象。

  • compress (default {}) — 传false就完全跳过压缩。传一个对象来自定义 压缩配置compress options

  • mangle (default true) — 传 false就跳过混淆名字。传对象来指定混淆配置mangle options (详情如下).

  • output (default null) — 要自定义就传个对象来指定额外的 输出配置output options. 默认是压缩到最优化。

  • sourceMap (default false) - 传一个对象来自定义
    sourcemap配置source map options.

  • toplevel (default false) - 如果你要混淆(和干掉没引用的)最高作用域中的变量和函数名,就传true

  • nameCache (default null) - 如果你要缓存 minify()多处调用的经混淆的变量名、属性名,就传一个空对象{}或先前用过的nameCache对象。
    注意:这是个可读/可写属性。minify()会读取这个对象的nameCache状态,并在最小化过程中更新,以便保留和供用户在外部使用。

  • ie8 (default false) - 传 true 来支持 IE8.

最小化配置的结构

Minify options structure

{
    warnings: false,
    parse: {
        // parse options
    },
    compress: {
        // compress options
    },
    mangle: {
        // mangle options

        properties: {
            // mangle property options
        }
    },
    output: {
        // output options
    },
    sourceMap: {
        // source map options
    },
    nameCache: null, // or specify a name cache object
    toplevel: false,
    ie8: false,
}

sourcemap配置

Source map options

这样生成sourcemap:

var result = UglifyJS.minify({"file1.js": "var a = function() {};"}, {
    sourceMap: {
        filename: "out.js",
        url: "out.js.map"
    }
});
console.log(result.code); // minified output
console.log(result.map);  // source map

要注意,此时sourcemap并不会保存为一份文件,它只会返回在result.map中。
sourceMap.url 传入的值只用来在result.code中设置//# sourceMappingURL=out.js.mapfilename 的值只用来在sourcemap文件中设置 file属性(详情看 规范)。

你可以把sourceMap.url设为true ,这样sourcemap会加在代码末尾。

你也可以指定sourcemap中的源文件根目录(sourceRoot)属性:

var result = UglifyJS.minify({"file1.js": "var a = function() {};"}, {
    sourceMap: {
        root: "http://example.com/src",
        url: "out.js.map"
    }
});

如果你要压缩从其他文件编译得来的带一份sourcemap的JS文件,你可以用sourceMap.content参数:

var result = UglifyJS.minify({"compiled.js": "compiled code"}, {
    sourceMap: {
        content: "content from compiled.js.map",
        url: "minified.js.map"
    }
});
// same as before, it returns `code` and `map`

如果你要用 X-SourceMap 请求头,你可以忽略 sourceMap.url

解析配置

Parse options

  • bare_returns (default false) -- 支持在顶级作用域中 return 声明。

  • html5_comments (default true)

  • shebang (default true) -- 支持在第一行用 #!command

压缩配置

Compress options

  • sequences(default: true) -- 连续声明变量,用逗号隔开来。可以设置为正整数来指定连续声明的最大长度。如果设为true 表示默认200个,设为false0则禁用。 sequences至少要是2,1的话等同于true(即200)。默认的sequences设置有极小几率会导致压缩很慢,所以推荐设置成20或以下。

  • properties -- 用.来重写属性引用,例如foo["bar"] → foo.bar

  • dead_code -- 移除没被引用的代码

  • drop_debugger -- 移除 debugger;

  • unsafe (default: false) -- 使用 "unsafe"转换 (下面详述)

  • unsafe_comps (default: false) -- 保留<<=不被换成 >>=。假如某些运算对象是用getvalueOfobject得出的时候,转换可能会不安全,可能会引起运算对象的改变。此选项只有当 comparisonsunsafe_comps 都设为true时才会启用。

  • unsafe_Func (default: false) -- 当 Function(args, code)argscode都是字符串时,压缩并混淆。

  • unsafe_math (default: false) -- 优化数字表达式,例如2 * x * 3 变成 6 * x, 可能会导致不精确的浮点数结果。

  • unsafe_proto (default: false) -- 把Array.prototype.slice.call(a) 优化成 [].slice.call(a)

  • unsafe_regexp (default: false) -- 如果RegExp 的值是常量,替换成变量。

  • conditionals -- 优化if等判断以及条件选择

  • comparisons -- 把结果必然的运算优化成二元运算,例如!(a <= b) → a > b (只有设置了 unsafe_comps时才生效);尽量转成否运算。例如 a = !b && !c && !d && !e → a=!(b||c||d||e)

  • evaluate -- 尝试计算常量表达式

  • booleans -- 优化布尔运算,例如 !!a? b : c → a ? b : c

  • typeofs -- 默认 true. 转换 typeof foo == "undefined"foo === void 0. 注意:如果要适配IE10或以下,由于已知的问题,推荐设成false

  • loops -- 当dowhilefor循环的判断条件可以确定是,对其进行优化。

  • unused -- 干掉没有被引用的函数和变量。(除非设置"keep_assign",否则变量的简单直接赋值也不算被引用。)

  • toplevel -- 干掉顶层作用域中没有被引用的函数 ("funcs")和/或变量("vars") (默认是false , true 的话即函数变量都干掉)

  • top_retain -- 当设了unused时,保留顶层作用域中的某些函数变量。(可以写成数组,用逗号隔开,也可以用正则或函数. 参考toplevel)

  • hoist_funs -- 提升函数声明

  • hoist_vars (default: false) -- 提升 var 声明 (默认是false,因为那会加大文件的size)

  • if_return -- 优化 if/return 和 if/continue

  • inline -- 包裹简单函数。

  • join_vars -- 合并连续 var 声明

  • cascade -- 弱弱地优化一下连续声明, 将 x, x 转成 xx = something(), x 转成 x = something()

  • collapse_vars -- 当 varconst 单独使用时尽量合并

  • reduce_vars -- 优化某些变量实际上是按常量值来赋值、使用的情况。

  • warnings -- 当删除没有用处的代码时,显示警告

  • negate_iife -- 当立即执行函数(IIFE)的返回值没用时,取消之。避免代码生成器会插入括号。

  • pure_getters -- 默认是 false. 如果你传入true,UglifyJS会假设对象属性的引用(例如foo.barfoo["bar"])没有函数副作用。

  • pure_funcs -- 默认 null. 你可以传入一个名字的数组,UglifyJS会假设这些函数没有函数副作用。警告:假如名字在作用域中重新定义,不会再次检测。例如var q = Math.floor(a/b),假如变量q没有被引用,UglifyJS会干掉它,但 Math.floor(a/b)会被保留,没有人知道它是干嘛的。你可以设置pure_funcs: [ 'Math.floor' ] ,这样该函数会被认为没有函数副作用,这样整个声明会被废弃。在目前的执行情况下,会增加开销(压缩会变慢)。

  • drop_console -- 默认 false. 传true的话会干掉console.*函数。如果你要干掉特定的函数比如console.info ,又想删掉后保留其参数中的副作用,那用pure_funcs来处理吧。

  • expression -- 默认 false。传true来保留终端语句中没有"return"的完成值。例如在bookmarklets。

  • keep_fargs -- 默认true。阻止压缩器干掉那些没有用到的函数参数。你需要它来保护某些依赖Function.length的函数。

  • keep_fnames -- 默认 false。传 true来防止压缩器干掉函数名。对那些依赖Function.prototype.name的函数很有用。延展阅读:keep_fnames混淆选项.

  • passes -- 默认 1。运行压缩的次数。在某些情况下,用一个大于1的数字参数可以进一步压缩代码大小。注意:数字越大压缩耗时越长。

  • keep_infinity -- 默认 false。传true以防止压缩时把1/0转成Infinity,那可能会在chrome上有性能问题。

  • side_effects -- 默认 true. 传false禁用丢弃纯函数。如果一个函数被调用前有一段/*@__PURE__*/ or /*#__PURE__*/ 注释,该函数会被标注为纯函数。例如 /*@__PURE__*/foo();

混淆配置

Mangle options

  • reserved (default [])。 传一个不需要混淆的名字的数组。 Example: ["foo", "bar"].

    • toplevel (default false)。混淆那些定义在顶层作用域的名字(默认禁用)。ß

    • keep_fnames(default false)。传true的话就不混淆函数名。对那些依赖Function.prototype.name的代码有用。延展阅读:keep_fnames压缩配置.

    • eval (default false)。混淆那些在with或eval中出现的名字。

// test.js
var globalVar;
function funcName(firstLongName, anotherLongName) {
    var myVariable = firstLongName +  anotherLongName;
}
var code = fs.readFileSync("test.js", "utf8");

UglifyJS.minify(code).code;
// 'function funcName(a,n){}var globalVar;'

UglifyJS.minify(code, { mangle: { reserved: ['firstLongName'] } }).code;
// 'function funcName(firstLongName,a){}var globalVar;'

UglifyJS.minify(code, { mangle: { toplevel: true } }).code;
// 'function n(n,a){}var a;'

混淆属性的配置

Mangle properties options

  • reserved (default: []) -- 不混淆在reserved 数组里的属性名.

  • regex (default: null) -— 传一个正则,只混淆匹配该正则的属性名。

  • keep_quoted (default: false) -— 只混淆不在括号内的属性名.

  • debug (default: false) -— 用原名字来组成混淆后的名字.
    传空字符串"" 来启用,或者非空字符串作为debu后缀。(例如"abc", foo.bar=>foo.barabc)

  • builtins (default: false) -- 传 true的话,允许混淆内置的DOM属性名。不推荐使用。

输出配置

Output options

代码生成器默认会尽量输出最简短的代码。假如你要美化一下输出代码,可以传--beautify (-b)。你也可以传更多的参数来控制输出代码:

  • ascii_only (default false) -- 忽略字符串和正则(导致非ascii字符失效)中的Unicode字符。

  • beautify (default true) -- 是否美化输出代码。传-b的话就是设成true。假如你想生成最小化的代码同时又要用其他设置来美化代码,你可以设-b beautify=false

  • bracketize (default false) -- 永远在if, for,do, while, with后面加上大括号,即使循环体只有一句。

  • comments (default false) -- 传 true"all"保留全部注释,传 "some"保留部分,传正则 (例如 /^!/) 或者函数也行。

  • indent_level (default 4) 缩进格数

  • indent_start (default 0) -- 每行前面加几个空格

  • inline_script (default false) -- 避免字符串中出现</script中的斜杠

  • keep_quoted_props (default false) -- 如果启用,会保留对象属性名的引号。

  • max_line_len (default 32000) -- 最大行宽(压缩后的代码)

  • space-colon (default true) -- 在冒号后面加空格

  • preamble (default null) -- 如果要传的话,必须是字符串。它会被加在输出文档的前面。sourcemap会随之调整。例如可以用来插入版权信息。

  • preserve_line (default false) -- 传 true 就保留空行,但只在beautify 设为false时有效。ß

  • quote_keys (default false) -- 传true的话会在对象所有的键加上括号

  • quote_style (default 0) -- 影响字符串的括号格式(也会影响属性名和指令)。

  • 0 -- 倾向使用双引号,字符串里还有引号的话就是单引号。

  • 1 -- 永远单引号

  • 2 -- 永远双引号

  • 3 -- 永远是本来的引号

  • semicolons (default true) -- 用分号分开多个声明。如果你传false,则总会另起一行,增强输出文件的可读性。(gzip前体积更小,gzip后稍大一点点)

  • shebang (default true) -- 保留开头的 shebang #! (bash 脚本)

  • width (default 80) -- 仅在美化时生效,设定一个行宽让美化器尽量实现。这会影响行中文字的数量(不包括缩进)。当前本功能实现得不是非常好,但依然让美化后的代码可读性大大增强。

  • wrap_iife (default false) --传true的话,把立即执行函数括起来。 更多详情看这里
    #640

综合应用

Miscellaneous

保留版权告示或其他注释

你可以传入--comments让输出文件中保留某些注释。默认时会保留JSDoc-style的注释(包含"@preserve","@license" 或 "@cc_on"(为IE所编译))。你可以传入--comments all来保留全部注释,或者传一个合法的正则来保留那些匹配到的注释。例如--comments /^!/会保留/*! Copyright Notice */这样的注释。

注意,无论如何,总会有些注释在某些情况下会丢失。例如:

function f() {
    /** @preserve Foo Bar */
    function g() {
        // this function is never called
    }
    return something();
}

即使里面带有"@preserve",注释依然会被丢弃。因为内部的函数g(注释所依附的抽象语法树节点)没有被引用、会被压缩器干掉。

书写版权信息(或其他需要在输出文件中保留的信息)的最安全位置是全局节点。

unsafe`compress`配置

The unsafecompress option

在某些刻意营造的案例中,启用某些转换有可能会打断代码的逻辑,但绝大部分情况下是安全的。你可能会想尝试一下,因为这毕竟会减少文件体积。以下是某些例子:

  • new Array(1, 2, 3)Array(1, 2, 3)[ 1, 2, 3 ]

  • new Object(){}

  • String(exp)exp.toString()"" + exp

  • new Object/RegExp/Function/Error/Array (...) → 我们干掉用new

  • void 0undefined (假如作用域中有一个变量名叫"undefined";我们这么做是因为变量名会被混淆成单字符)

编译条件语句

Conditional compilation

Uglify会假设全局变量都是常量(不管是否在局部域中定义了),你可以用--define (-d)来实现定义全局变量。例如你传--define DEBUG=false,UglifyJS会在输出中干掉下面代码:

if (DEBUG) {
    console.log("debug stuff");
}

你可以像--define env.DEBUG=false这样写嵌套的常量。

在干掉那些永否的条件语句以及不可达代码时,UglifyJS会给出警告。现在没有选项可以禁用此特性,但你可以设置 warnings=false 来禁掉所有警告。

另一个定义全局常量的方法是,在一个独立的文档中定义,再引入到构建中。例如你有一个这样的build/defines.js

const DEBUG = false;
const PRODUCTION = true;
// 等等

这样构建你的代码:

  uglifyjs build/defines.js js/foo.js js/bar.js... -c

UglifyJS会注意到这些常量。因为它们无法改变,所以它们会被认为是没被引用而被照样干掉。如果你用const声明,构建后还会被保留。如果你的运行环境低于ES6、不支持const,请用var声明加上reduce_vars设置(默认启用)来实现。

编译条件语句API

你也可以通过程序API来设置编译配置。其中有差别的是一个压缩器属性global_defs

var result = UglifyJS.minify(fs.readFileSync("input.js", "utf8"), {
    compress: {
        dead_code: true,
        global_defs: {
            DEBUG: false
        }
    }
});

global_defs"@"前缀的表达式,UglifyJS才会替换成语句表达式:

UglifyJS.minify("alert('hello');", {
    compress: {
        global_defs: {
            "@alert": "console.log"
        }
    }
}).code;
// returns: 'console.log("hello");'

否则会替换成字符串:

UglifyJS.minify("alert('hello');", {
    compress: {
        global_defs: {
            "alert": "console.log"
        }
    }
}).code;
// returns: '"console.log"("hello");'

使用minify()获得原生UglifyJS ast

Using native Uglify AST with minify()

// 例子: 只解析代码,获得原生Uglify AST

var result = UglifyJS.minify(code, {
    parse: {},
    compress: false,
    mangle: false,
    output: {
        ast: true,
        code: false  // optional - faster if false
    }
});

// result.ast 即是原生 Uglify AST
// 例子: 输入原生 Uglify AST,接着把它压缩并混淆,生成代码和原生ast

var result = UglifyJS.minify(ast, {
    compress: {},
    mangle: {},
    output: {
        ast: true,
        code: true  // 可选,false更快
    }
});

// result.ast 是原生 Uglify AST
// result.code 是字符串格式的最小化后的代码

使用 Uglify AST

Working with Uglify AST

可以通过TreeWalkerTreeTransformer分别横截(?transversal)和转换原生AST。

ESTree/SpiderMonkey AST

UglifyJS有自己的抽象语法树格式;为了某些现实的原因
我们无法在内部轻易地改成使用SpiderMonkey AST。但UglifyJS现在有了一个可以输入SpiderMonkeyAST的转换器。
例如Acorn ,这是一个超级快的生成SpiderMonkey AST的解释器。它带有一个实用的迷你CLI,能解释一个文件、把AST转存为JSON并标准输出。可以这样用UglifyJS来压缩混淆:

    acorn file.js | uglifyjs --spidermonkey -m -c

-p --spidermonkey选项能让UglifyJS知道输入文件并非JavaScript,而是SpiderMonkey AST生成的JSON代码。这事我们不用自己的解释器,只把AST转成我们内部AST。

使用 Acorn 来解释代码

Use Acorn for parsing

更有趣的是,我们加了 -p --acorn选项来使用Acorn解释所有代码。如果你传入这个选项,UglifyJS会require("acorn")

Acorn确实非常快(650k代码原来要380ms,现在只需250ms),但转换Acorn产生的SpiderMonkey树会额外花费150ms。所以总共比UglifyJS自己的解释器还要多花一点时间。

Uglify Fast Minify Mode

很少人知道,对大多数js代码而言,其实移除空格和混淆符号已经占了减少代码体积之中到的95%--不必细致地转换。简单地禁用压缩compress能加快UglifyJS的构建速度三四倍。我们可以比较一下
butternut和只使用混淆mangle的模式的Uglify的压缩速度与gzip大小:
butternut:

d3.jsminify sizegzip sizeminify time (seconds)
original451,131108,733-
uglify-js@3.0.24 mangle=false, compress=false316,60085,2450.70
uglify-js@3.0.24 mangle=true, compress=false220,21672,7301.13
butternut@0.4.6217,56872,7381.41
uglify-js@3.0.24 mangle=true, compress=true212,51171,5603.36
babili@0.1.4210,71372,14012.64

在CLI中,这样启用快速最小化模式:

uglifyjs file.js -m

API这样用:

UglifyJS.minify(code, { compress: false, mangle: true });
查看原文

赞 31 收藏 34 评论 0

越前君 赞了文章 · 3月23日

lint-staged如何做到只lint staged?

介绍

lint-staged针对暂存的git文件运行linters并且不要让 💩 进入你的代码库!

开始

去年分享过一个主题——规范化工作流之约定式提交,主要内容是提交代码时对暂存区代码格式的校验和提交信息规范校验。当时就接触到了lint-staged,只知道这个工具能针对暂存区的文件处理,并未深入了解, 那时候就有一些疑问埋在心里,最近得空,特来解疑。

疑问点:

git分为暂存区和工作区,如果一个文件同时存在在两个区(某文件git add后又再次修改,如下图test2.js ),此时本地的文件内容实际上是等同未暂存区的,根据介绍lint-staged会lint暂存区的那个版本,那么这是怎么做到的呢?

hasPartiallyStagedFiles

先猜测:

使用SourceTree提交代码偶尔会比较卡,稍微窥得点儿(未暂存文件消失再重现),因此猜测可能是用了什么方法先清除未暂存文件然后再恢复。

猜测归猜测,还是要验证一下。

结论

经过分析,lint-staged在执行检查前会保存当前文件状态,然后清除掉修改,再执行lint任务,执行完毕再恢复。

重点就是:如何保存?如何恢复?

我总结出lint-staged的流程大致如下

flow

这样就很清晰了,由图可知,上述疑问点为红色流程部分,下面我们来分析一下流程中的具体实现。

分析

流程大致分为四部分:

  • Stashing changes
  • Running linters
  • Updating stash
  • Restoring local changes

我们来分别看一下每一步做了什么

保留案发现场并清除干扰(Stashing changes...)

git write-tree // 得到 indexTree
git add .
git write-tree // 得到 workingCopyTree
git read-tree $indexTree
git checkout-index -af // 清除文件修改(未暂存的test2.js被清除)

根据以上操作步骤得知,lint-staged通过tree对象来保存暂存区目录和工作区目录,并清除掉工作区修改文件,操作完成后,可以看到,被修改的test2.js已经被清除(如下图)。

gitStashSave

执行代码检查任务(Running linters...)

按照配置的命令走,比如配置了 "*.js": "eslint"

eslint test2.js test.js

linters

更新(Updating stash...)

上一步(Running linters)如果有检查到错误,直接跳过走下一步(Restoring local changes)

git write-tree // 得到 formattedIndexTree  

这里需要特别声明一下,

如果上一步(Running linters)未检测到错误,那么这里得到的formattedIndexTree 会和第一步的indexTree一样,如果检测到错误并将修复后文件添加到暂存区,如配置命令是eslint --fix , git add的话,那么代码被修复过,formattedIndexTreeindexTree不同

恢复案发现场(Restoring local changes...)

git read-tree $workingCopyTree // 首先恢复工作区内容,对应第一步的git add .
git checkout-index -af // 清除工作区修改
git read-tree $formattedIndexTree // 恢复暂存区内容 
git apply $patch // 如果修复了代码,也应用到工作区

总结

归根结底,都是git对象的操作。

查看原文

赞 9 收藏 4 评论 2

越前君 赞了文章 · 3月20日

在 Mac 下安装 nvm 管理 node(解决版)

前言

在使用 node 的过程中,用 npm 安装一些模块,特别是全局包的时候,由于 Mac 系统安全性的限制,经常出现安装没有权限,或者安装完成使用时出现 Command not found 的情况。

之前我都是通过使用修改权限的方式来解决,但是太麻烦又感觉不太安全,于是我就到网上找解决的方法,发现其实官方也是推荐我们使用 node 的管理工具来解决这个问题的。官方推荐了两个 nnvm,这里我选择的是 nvm

至于两者的区别可以看一下淘宝团队的一篇文章管理node版本,选择nvm还是n?

安装

这篇文章主要想说的就是在 Macnvm 的安装和遇到的问题。

注意:不要使用 Homebrew 安装 nvm,这个在 nvm 的官方文档中有说明。

nvm官方说明

具体的步骤如下:

首先打开终端,进入当前用户的 home 目录中。

cd ~

然后使用 ls -a 显示这个目录下的所有文件(夹)(包含隐藏文件及文件夹),查看有没有 .bash_profile 这个文件。

ls -a

如果没有,则新建一个。

touch ~/.bash_profile

如果有或者新建完成后,我们通过官方的说明在终端中运行下面命令中的一种进行安装:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.2/install.sh | bash
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.2/install.sh | bash

在安装完成后,也许你会在终端输入 nvm 验证有没有安装成功,这个时候你会发现终端打出 Command not found,其实这并不是没有安装成功,你只需要重启终端就行,再输入 nvm 就会出现 Node Version Manager 帮助文档,这表明你安装成功了。

注意

这里需要注意的几点就是:

第一点 不要使用 homebrew 安装 nvm

第二点 关于 .bash_profile 文件。如果用户 home 目录下没有则新建一个就可以了,不需要将下面的两段代码写进去,因为你在执行安装命令的时候,系统会自动将这两句话写入 .bash_profile 文件中。

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion

网络上我找了好多文章都是说在安装前先手动将下面这两句话写进去,经过测试是不正确的,并且会造成安装不成功,这一点需要注意一下。

export NVM_DIR="${XDG_CONFIG_HOME/:-$HOME/.}nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

第三点 保证 Mac 中安装了 git,一般只要你下载了 MacXcode 开发工具,它是自带 git 的。

在最新的 Catalina 系统下安装 nvm

注意最新的 macOS Catalina 系统(即版本 10.15 及之后)默认的 shellzsh,不在是 bash ,安装完之后会出现命令不可用的情况。

这里有两种解决办法:

第一种 我们需要将默认 shell 改为 bash,具体参见苹果官网的相关文章 如何更改默认 Shell,如果你之前就习惯使用 zsh 也可自行配置。

第二种 如果你要使用 zsh 终端,那么在上述方式安装完之后,在 .bash_profile 同一目录下创建一个 .zshrc 文件,使用 vim 打开文件添加下面两句话,重启终端即可。

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion

一些常见的nvm命令

nvm ls-remote 列出所有可安装的版本

nvm install <version> 安装指定的版本,如 nvm install v8.14.0

nvm uninstall <version> 卸载指定的版本

nvm ls 列出所有已经安装的版本

nvm use <version> 切换使用指定的版本

nvm current 显示当前使用的版本

nvm alias default <version> 设置默认 node 版本

nvm deactivate 解除当前版本绑定

nvm 默认是不能删除被设定为 default 版本的 node,特别是只安装了一个 node 的时候,这个时候我们需要先解除当前版本绑定,然后再使用 nvm uninstall <version> 删除

node被安装在哪里

在终端我们可以使用 which node 来查看我们的 node 被安装到了哪里,这里终端打印出来的地址其实是你当前使用的 node 版本快捷方式的地址。

/Users/你的用户名/.nvm/versions/node/v10.13.0/bin/node

如果你想查看所有 node 版本的安装文件夹,我们可以在 访达(finder) 中使用快捷键 Command+Shift+G 输入 /Users/你的用户名/.nvm/versions 地址就可以看到。

但是这里想说的是 Mac 默认是不显示隐藏文件夹的,.nvm 是个隐藏文件夹在 访达(finder) 中看不到,在 Mac 下显示隐藏文件的快捷键是 Command+Shift+.,关闭也是这个快捷键。

看!这是我的!

node位置

这是在 v8.14.0 版本 nodelibnode_modules 下的模块,可以看到我刚刚安装的 cnpmelectron

v8.14.0

但是在 v10.13.0 同样的目录下只有 npm

v10.13.0

所以可以知道在 nvmnode 版本管理方式,安装的模块不是公用的,也就是说你在切换版本后需要在切换的版本下重新安装,这一点和 n 是不同的,当然这也是它的优势。

查看原文

赞 39 收藏 26 评论 7

越前君 赞了文章 · 3月11日

谈谈 <script> 标签以及其加载顺序问题,包含 defer & async

谈谈 <script> 标签加载顺序的问题

这篇文章比较长,如果你耐心读完了,我会感谢你愿意在这篇文章上花费时间,也希望你有收获。

其实说起<script>,几乎搞前端的都知道他的作用:引入 JavaScrit 代码。没错,这就是<script>被创建的最初原因。<script>标签出现的很早,这个元素是由 Netscape 创造,并在 Netscape Navigator 2中首先实现。后来,这个元素被加入到正式的 HTML 规范中。

JavaScript 的诞生离不开 Netscape ,JavaScript 是由 Netscape 公司的布兰登·艾奇(Brendan Eich)在 1995 年开发的一种脚本语言,JavaScript 的第一个版本 JavaScript 1.0 就在 Netscape Navigator 2 实现。

<script> 拥有的属性

  • async:可选,表示应该立即下载脚本,但不应妨碍页面中的其他操作,比如下载其他资源或等待加载其他脚本。只对外部脚本文件有效。
  • charset:可选。表示通过 src 属性指定的代码的字符集。由于大多数浏览器会忽略它的值,因此这个属性很少有人用。
  • defer:可选。表示脚本可以延迟到文档完全被解析和显示之后再执行。只对外部脚本文件有效。IE7 及更早版本对嵌入脚本也支持这个属性。
  • language: 已废弃。原来用于表示编写代码使用的脚本语言(如 JavaScript 、 JavaScript1.2 或 VBScript )。大多数浏览器会忽略这个属性,因此也没有必要再用了。
  • src:可选。表示包含要执行代码的外部文件。
  • type:可选。可以看成是 language 的替代属性;表示编写代码使用的脚本语言的内容类型(也称为 MIME 类型)。虽然 text/javascript
    和 text/ecmascript 都已经不被推荐使用,但人们一直以来使用的都还是 text/javascript 。实际上,服务器在传送 JavaScript 文件时使用的
    MIME 类型通常是 application/x–javascript ,但在 type 中设置这个值却可能导致脚本被忽略。另外,在非IE浏览器中还可以使用以下值:
    application/javascript 和 application/ecmascript 。考虑到约定俗成和最大限度的浏览器兼容性,目前 type 属性的值依旧还是
    text/javascript 。不过,这个属性并不是必需的,如果没有指定这个属性,则其默认值仍为text/javascript 。
在以上属性中 async属性是 HTML5 中的新属性。

引入方式 JavaScript 的两种方式

内联形式

这种方式指的是在 html 文件中,添加一个<script></scritp>标签,然后将 JavaScript代码直接写在里面,如:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>script 标签</title>
  <script type="text/javascript">
    console.log('内联 JavaScript');
  </script>
</head>

<body>
  <!-- content -->
</body>

</html>

外置形式

外置形式是将 JavaScript 代码写在外部的一个文件里面,在 html 文件中通过 <script> 标签的 src 属性引入,如:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>script 标签</title>
</head>

<body>
  <!-- content -->
  <script type="text/javascript" data-original="./js/01.js"></script>
</body>

</html>

两种引入形式的比较

对于这两种方式,毫无疑问,外置形式明显好于内联形式,主要表现为以下方面:

  • 可维护性:外置 Javascript 文件可以被多个页面调用而不用在每个页面上反复地书写.如果有需要改变的部分,你只需要在一处修改即可.所以外置JavaScript 导致代码工作量减少,进而使得维护手续也更加方便。
  • 可缓存:浏览器能够根据具体的设置缓存链接的所有外部 JavaScript文件。也就是说,如果有两个页面都使用同一个文件,那么这个文件只需下载一次。因此,最终结果就是能够加快页面加载的速度。
  • 关注点分离:将 JavaScript 封装在外部的.js文件遵循了关注点分离的法则.总体来说,分离 HTML,CSS 和 JavaScript 从而让我们更容易操纵他们.而且如果是多名开发者同步工作的话,这样也更方便。

因此,在今后的开发中尽量使用外置方式的形式引入JavaScript

<script> 标签加载顺序

如果要谈<script> 标签加载顺序问题,首先要谈的就是标签的位置,因为标签的位置对于JavaScript加载顺序来说有着很重要的影响。

标签位置

<script> 标签的位置有两种,一种是方式<head>元素里面,另外一种就是放在<body> 元素中页面内容的后面,下面将一一介绍这两种形式:

<script> 标签放在<head>元素里

<!DOCTYPE html>
<html>

<head>
  <title>Example HTML Page</title>
  <script type="text/javascript" data-original="example1.js"></script>
  <script type="text/javascript" data-original="example2.js"></script>
</head>

<body>
  <!-- 这里放内容 -->
</body>

</html>

这是一种比较传统的做法,目的就是把所有外部文件(包括 CSS 文件和 JavaScript 文件)的引用都放在相同的地方.可是,在文档的 <head> 元素中包含所有 JavaScript 文件,意味着必须等到全部 JavaScript 代码都被下载、解析和执行完成以后,才能开始呈现页面的内容(浏览器在遇到 <body> 标签时才开始呈现内容)。对于那些需要很多 JavaScript 代码的页面来说,这无疑会导致浏览器在呈现页面时出现明显的延迟,而延迟期间的浏览器窗口中将是一片空白。很明显,这种做法有着很明显的缺点,特别是针对于现在的移动端来说,如果超过 1s 还没有内容呈现的话将是一种很差的用户体验。为了避免这个问题,就有了下面这种加载方式。

<script> 标签放在<body> 元素中页面内容的后面

<!DOCTYPE html>
<html>

<head>
  <title>Example HTML Page</title>
</head>

<body>
  <!-- 这里放内容 -->
  <script type="text/javascript" data-original="example1.js"></script>
  <script type="text/javascript" data-original="example2.js"></script>
</body>

</html>

对于这种方式,在解析包含的 JavaScript 代码之前,页面的内容将完全呈现在浏览器中。而用户也会因为浏览器窗口显示空白页面的时间缩短而感到打开页面的速度加快了

延迟加载

<script>的每个属性设计来肯定都是有用的,下面我们就来说一说 defer 属性。
HTML 4.01 为 <script> 标签定义了 defer 属性。这个属性的用途是表明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在 <script> 元素中设置defer 属性,相当于告诉浏览器立即下载,但延迟执行,比如:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>script 标签</title>
  <script defer="defer" type="text/javascript" data-original="./js/01.js"></script>
  <script defer="defer" type="text/javascript" data-original="./js/02.js"></script>
</head>

<body>
  <!-- content -->
  <script type="text/javascript" data-original="./js/03.js"></script>
</body>

</html>

在这个例子中,虽然我们把 <script> 元素放在了文档的 <head> 元素中,但其中包含的脚本将延迟到浏览器遇到 </html> 标签后再执行。HTML5 规范要求脚本按照它们出现的先后顺序执行,因此第一个延迟脚本会先于第二个延迟脚本执行,而这两个脚本会先于 DOMContentLoaded 事件执行。在现实当中,延迟脚本并不一定会按照顺序执行,也不一定会在 DOMContentLoaded 事件触发前执行,因此最好只包含一个延迟脚本。

"在现实当中,延迟脚本并不一定会按照顺序执行,也不一定会在 DOMContentLoaded 事件触发前执行,因此最好只包含一个延迟脚本。" 这段话是《JavaScript 高级程序设计(第三版)》中的一句话,纠结了很久。自己也尝试写了一些例子,但反馈的结果都是:如果引入的 <script>标签 都使用了 defer 属性,他们的执行顺序都是按照他们引入的顺序来的。那么作者为什么会写上这一句话呢,个人感觉原因是:即使在 HTML5 规范中有这么一条,不一定所有的浏览器厂商都会遵照这个规定,可能某些浏览器厂商并没有实现这个规范,但支持 defer 属性,那么就会出现作者所描述的那种情况,所以为了安全起见,在开发中使用一个 defer 是非常有必要的。
还有一点需要注意的是,defer 属性只适用于外部脚本文件。
defer 的兼容性如下:

图片描述

从图中可以看出,某些浏览器或者在一些低版本的浏览器中并不支持defer属性,因此,把延迟脚本放在页面底部仍然是最佳选择。

异步加载

说完了延迟加载,然后我们再说下异步加载,即使用 async属性。
HTML5 为 <script> 元素定义了 async 属性。这个属性与 defer 属性类似,都用于改变处理脚本的行为。同样与 defer 类似, async 只适用于外部脚本文件,并告诉浏览器立即下载文件,下载完成后立即执行。但与 defer不同的是,标记为 async 的脚本并不保证按照指定它们的先后顺序执行。例如:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>script 标签</title>
  <script async type="text/javascript" data-original="./js/01.js"></script>
  <script async type="text/javascript" data-original="./js/02.js"></script>
</head>

<body>
  <!-- content -->
</body>

</html>

在以上代码中,可能由于 01.js 下载时间比较长,由于两个 <script> 标签都是异步执行,互不干扰,因此 02.js 可能就会先于 01.js 执行。因此,确保两者之间互不依赖非常重要。指定 async 属性的目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容。为此,建议异步脚本不要在加载期间修改 DOM

async的兼容性如下:

图片描述

可以看出 IE9 及以下版本都不支持 async属性,因此,把延迟脚本放在页面底部仍然是最佳选择。

<script> 标签加载可视化

下面将用一张图来描述<script> 标签三种状态下(normal,defer,async)于html加载的关系:
图片描述

绿色代表html解析,淡蓝色代表html解析停止,蓝色代表script下载,粉红色代表script执行。从上图很容易的看出来只要执行script,html就会停止渲染,除此之外也可以清晰的看出他们之间的加载关系。

小结

  • 所有 <script> 标签引进的 JavaScript 会按照他们引入的顺序依次被解析,在没有使用 defer 或者 async 的情况下,只有在解析完前面 <script> 元素中的代码之后,才会开始解析后面 <script> 元素中的代码。
  • 由于浏览器会先解析完不使用 defer 属性的 <script> 元素中的代码,然后再解析后面的内容,所以一般应该把 <script> 元素放在页面最后,即主要内容后面, </body> 标签前面。
  • 使用 defer 属性可以让脚本在文档完全呈现之后再执行,延迟脚本总是按照指定它们的顺序执行。
  • 使用 async 属性可以表示当前脚本不必等待其他脚本,也不必阻塞文档呈现。不能保证异步脚本按照它们在页面中出现的顺序执行。

结束语

这篇文章是最近读了 《JavaScript 高级程序设计(第三版)》后写的。现在仔细阅读这本书你会发现其中真的有很多的乐趣,这些乐趣来自于你可以更深一步的去了解 JavaScript,源自于你原来可以将这个知识点弄得这么的透彻,源自于你也许真的对这么语言有了兴趣。其实我在github上创建了一个仓库,用户记录自己在读了这本书中一些知识点以后的一些理解,算是阅读笔记吧,也算是鼓励自己坚持认真的把这本书看完,抵抗一下天生的惰性,如果你也想进一步深刻的了解 JavaScript这门语言,可以点击这里,大家一起在github学习。最后,如果这篇文章有写的不对的地方还望各位大佬指出。

查看原文

赞 22 收藏 8 评论 2

越前君 发布了文章 · 2月21日

随机打乱数组及Fisher–Yates shuffle算法详解

介绍几种随机打乱数组的方法,及其利弊。

一、Array.prototype.sort 排序

注意一下,sort() 方法会改变原数组,看代码:

// ES6 写法
function randomShuffle(arr) {
  return arr.sort(() => Math.random() - 0.5)
}

// ES5 写法
function randomShuffle(arr) {
  var compareFn = function () {
    return Math.random() - 0.5
  }
  return arr.sort(compareFn)
}
但实际上这种方法并不能真正的随机打乱数组。在多次执行后,每个元素有很大几率还在它原来的位置附近出现。可看下这篇文章:常用的 sort 打乱数组方法真的有用?

二、Fisher–Yates shuffle 经典洗牌算法

这种算法思想,目前有两种稍有不同的实现方式,这里我把它们都算入 Fisher–Yates shuffle。分别是 Fisher–Yates shuffleKnuth-Durstenfeld Shuffle

著名的 Lodash 库的方法 _.shuffle() 也是使用了该算法。

1. Fisher–Yates shuffle(Fisher and Yates' original method)

由 Ronald Fisher 和 Frank Yates 提出的 Fisher–Yates shuffle 算法思想,通俗来说是这样的:

假设有一个长度为 N 的数组

  1. 从第 1 个到剩余的未删除项(包含)之间选择一个随机数 k。
  2. 从剩余的元素中将第 k 个元素删除并取出,放到新数组中。
  3. 重复第 1、2 步直到所有元素都被删除。
  4. 最终将新数组返回

实现

function shuffle(arr) {
  var random
  var newArr = []

  while (arr.length) {
    random = Math.floor(Math.random() * arr.length)
    newArr.push(arr[random])
    arr.splice(random, 1)
  }

  return newArr
}

举例

假设我们有 1 ~ 8 的数字

表格每列分别表示:范围、随机数(被移除数的位置)、剩余未删除的数、已随机排列的数。
RangeRollScratchResult
 1 2 3 4 5 6 7 8 

现在,我们从 1 ~ 8 中随机选择一个数,得到随机数 k 为 3,然后在 Scratch 上删除第 k 个数字(即数字 3),并将其放到 Result 中:

RangeRollScratchResult
1 - 8 31 2 3 4 5 6 7 83 

现在我们从 1 ~ 7 选择第二个随机数 k 为 4,然后在 Scratch 上删除第 k 个数字(即数字 5),并将其放到 Result 中:

RangeRollScratchResult
1 - 7 41 2 3 4 5 6 7 83 5 

现在我们从 1 ~ 6 选择下一个随机数,然后从 1 ~ 5 选择依此类推,总是重复上述过程:

RangeRollScratchResult
1–651 2 3 4 5 6 7 83 5 7
1–531 2 3 4 5 6 7 83 5 7 4
1–441 2 3 4 5 6 7 83 5 7 4 8
1–311 2 3 4 5 6 7 83 5 7 4 8 1
1–221 2 3 4 5 6 7 83 5 7 4 8 1 6
  1 2 3 4 5 6 7 83 5 7 4 8 1 6 2
2. Knuth-Durstenfeld Shuffle(The modern algorithm)

Richard Durstenfeld 于 1964 年推出了现代版本的 Fisher–Yates shuffle,并由 Donald E. Knuth 在 The Art of Computer Programming 以 “Algorithm P (Shuffling)” 进行了推广。Durstenfeld 所描述的算法与 Fisher 和 Yates 所给出的算法有很小的差异,但意义重大。

-- To shuffle an array a of n elements (indices 0..n-1):  
for i from n−1 downto 1 do  // 数组从 n-1 到 0 循环执行 n 次
  j ← random integer such that 0 ≤ j ≤ i  // 生成一个 0 到 n-1 之间的随机索引
  exchange a[j] and a[i] // 将交换之后剩余的序列中最后一个元素与随机选取的元素交换

Durstenfeld 的解决方案是将“删除”的数字移至数组末尾,即将每个被删除数字与最后一个未删除的数字进行交换

实现

// ES6 写法
function shuffle(arr) {
  let i = arr.length

  while (--i) {
    let j = Math.floor(Math.random() * i)
    ;[arr[j], arr[i]] = [arr[i], arr[j]]
  }

  return arr
}


// ES5 写法
function shuffle(arr) {
  var i = arr.length
  var j
  var t

  while (--i) {
    j = Math.floor(Math.random() * i)
    t = arr[i]
    arr[i] = arr[j]
    arr[j] = t
  }
  
  return arr
}

Knuth-Durstenfeld Shuffle 将算法的时间复杂度降低到 O(n),而 Fisher–Yates shuffle 的时间复杂度为 O(n2)。后者在计算机实现过程中,将花费不必要的时间来计算每次剩余的数字(可以理解成数组长度)。

举例

同样,假设我们有 1 ~ 8 的数字

表格每列分别表示:范围、当前随机数(即随机交互的位置)、剩余未交换的数、已随机排列的数。
RangeRollScratchResult
 1 2 3 4 5 6 7 8 

我们从 1 ~ 8 中随机选择一个数,得到随机数 k 为 6,然后交换 Scratch 中的第 6 和第 8 个数字:

RangeRollScratchResult
1 - 8 61 2 3 4 5 8 76 

接着,从 1 ~ 7 中随机选择一个数,得到随机数 k 为 2,然后交换 Scratch 中的第 2 和第 7 个数字:

RangeRollScratchResult
1 - 7 61 7 3 4 5 82

继续,下一个随机数是1 ~ 6,得到的随机数恰好是 6,这意味着我们将列表中的第 6 个数字保留下来(经过上面的交换,现在是 8),然后移到下一个步。同样,我们以相同的方式进行操作,直到完成排列:

RangeRollScratchResult
1 - 6 61 7 3 4 58 2 6 
1 - 5 15 7 3 41 8 2 6 
1 - 4 35 7 43 1 8 2 6 
1 - 3 35 74 3 1 8 2 6 
1 - 2 175 4 3 1 8 2 6 

因此,结果是 7 5 4 3 1 8 2 6

三、总结

若要实现随机打乱数组的需求,不要再使用 arr.sort(() => Math.random() - 0.5) 这种方法了。目前用得较多的是 Knuth-Durstenfeld Shuffle 算法。

四、参考

查看原文

赞 0 收藏 0 评论 0

越前君 发布了文章 · 2月15日

JavaScript 判断是否为数组

JavaScript 判断数组的几种方法及其利弊。

1. typeof

对于 Function、String、Number、Undefined 等几种类型的对象来说,他完全可以胜任。但是为 Array 时:

var arr = [1, 2, 3]
console.log(typeof arr) // "object"

// 同样的
console.log(typeof null) // "object"
console.log(typeof {}) // "object"

所以不能使用 typeof 来判断。

2. instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

var arr = [1, 2 ,3]
console.log(arr instanceof Array) // true

3. 原型链(constructor)

一般情况下,除了 undefinednull,其它都能使用 constructor 判断类型。

var arr = [1, 2, 3]
console.log(arr.__proto__.constructor === Array) // true
console.log(arr.constructor === Array) // true

// 注意:arr.__proto__ === Array.prototype 为 true。

但是某些情况下,判断是不准确的,比如:

// 构造函数
function Fn() {}
// 修改原型对象
Fn.prototype = new Array()
// 实例化对象
var fn = new Fn()

console.log(fn.constructor === Fn) // false
console.log(fn.constructor === Array) // true
// 此时的 fn 应该是一个普通对象,而非数组,所以此时使用 constructor 判断是不合适的。
使用 instanceof 和 constructor 的局限性:

使用和声明都必须是在当前页面,比如父页面引用了子页面,在子页面中声明了一个 Array,将其赋值给父页面的一个变量,那么此时做原型链的判断:Array === object.constructor 得到的是 false,原因如下:

  1. Array 属于引用型数据,在传递过程中,仅仅是引用地址的传递。
  2. 每个页面的 Array 原生对象所引用的地址是不一样的,在子页面声明的Array 所对应的构造函数是子页面的 Array 对象;父页面来进行判断,使用的 Array 并不等于子页面的 Array

看代码:

var iframe = document.createElement('iframe')
document.body.appendChild(iframe)
var xArray = window.frames[window.frames.length - 1].Array
var xarr = new xArray()
var arr = new Array()

// 不同页面,结果并非我们所预期的 true,而是 false 哦!
console.log(xarr instanceof Array) // false
console.log(xarr.constructor === Array) // false

// 同页面才是 true 哦!
console.log(arr instanceof Array) // true
console.log(arr.constructor === Array) // true

4. Array.isArray

isArray() 方法是 ES5 标准提供的一个判断数组方法。

function isArray(arr) {
  return Array.isArray(arr)
}

5. Object.prototype.toString

以上方法,或多或少都稍显不足,下面这个方法则是通用,足矣!

function isArray(arr) {
  return Object.prototype.toString.call(arr) === '[object Array]'
}

综上所述,也可总结成如下方法:

function isArray(arr) {
  const toString = Object.prototype.toString
  const isArray = Array.isArray || function (args) { return toString.call(args) === '[object Array]' }
  return isArray(arr)
}

参考

查看原文

赞 0 收藏 0 评论 0

越前君 提出了问题 · 2月7日

在 react 中,哪种写法性能更好?

在 react、vue.js 中都是采用 Virtual DOM 的方式以减少频繁地操作 DOM 元素带来的性能问题。

由于对这块理解不深,如下两行代码,哪种更好,性能会有什么区别吗?求教,谢谢!

下面是 jsx 示例:

const ele = props => {
  const { type } = props
  return (
    <div>
      <span>{type ? '正确的' : '错误的'}</span>
    </div>
  )
}
const ele = props => {
  const { type } = props
  return <div>{type ? <span>正确的</span> : <span>错误的</span>}</div>
}

关注 2 回答 2

越前君 收藏了文章 · 2月7日

深入理解 JS 中的类型转化

哪些操作能导致类型转换呢

if 条件判断

为 false 的值 -> false undefined null 0 '' NaN

运算符操作

常见的运算符 + - * /
+ 比较特殊除了相加之外 还有字符串拼接的含义

1)数字和非字符串相加

1 + null -> 0;

// undefined 比较特殊 表示未定义的,不是一个数字
1 + undefined -> NaN

// 会把空对象转换成数字,如果转换不成数字就变成字符串拼接
1 + {} -> 1[object object]

2)非数字相加

// 把两边都转化成数字
true + true -> 2

// 如果有一方是字符串,则进行字符串拼接
true + {} -> true[object object]

valueOf / toString

对象的原型链上有 valueOf 和 toString 两个方法

let obj = {
  symbol[toPrimitive](){
    return 500
  },
  valueOf(){
    return 100
  },
  toString(){
    return 200
  }
}

// 两边都转换成数字 obj 先调用 valueOf valueOf 如果返回不是数字 则继续调用 toString 方法
true + obj -> 101

symbol[toPrimitive] 是对象内置属性,当一个对象要转换成对应的原始类型时,会调用此方法。

总结下,当对象要进行类型转换时,会依次调用 symbol[toPrimitive] valueOf toString 

+ 、- 、!

+ - 和 ! 一样,可以放在变量前面,进行快速类型转换

1 + +'1234' -> 1235

1 + '1234'  -> '11234'

比较元算 > = <

1)数字和数字直接比较

2)字符串比较

// 字符串和字符串,比较的是 AscII 码
console.log('a'.charCodeAt(0))
console.log('b'.charCodeAt(0))

'a' < 'b' -> true
'a' < 'bdede' -> true // 一样的因为比的是第一位

// 数字和字符串相比,字符串先转化成数字,如果转化不成数字 这比较始终返回 false
1 < '123' -> true
1 < '1df' -> false

// 如果是对象和字符串相比,需要把对象先转化成基本类型(字符串之后再比较)
[] == '' -> true
// [].valueOf 为[],继续调用 [].toString 为 '',比较返回 true

3) == 比较

如果一方是数字,会先把另一方转换成数字 然后比较

若果其中一方是 boolean 类型 会把 boolean 类型转换为数字

null == undefined // true

null、undefined 和任何类型相比 == 都返回 false

NaN 和任何类型相比返回 false 包括它本身

举例

console.log([] == ![]);
// [] == false  单目运算优先级最高 为 false 的情况 false undefined NaN null 0 ''
// [] == 0      [].valueOf()  -> [] 不是原始类型继续调用 toSting 
// [] == 0      [].toString() -> ''
// '' == 0      Number('')
//  0 == 0      true

(完)

查看原文

越前君 发布了文章 · 1月18日

详谈JSON与JavaScript

JSON 在编程生涯简直就是无处不在啊。

  • 那么 JSON 是什么呢?
  • 跟我们的 JavaScript 有什么关系呢?
  • 在 JavaScript 中,我们如何处理 JSON 数据呢?

一、JSON

JSON(JavaScript Object Natation)是一种轻量级的数据交换格式。由于易于阅读、编写,以及便于机器解析与生成的特性,相比 XML,它更小、更快、更易解析,使得它成为理想的数据交换语言。完全独立于语言的一种文本格式。

JSON 的两种结构:

  • “名称/值” 对的集合:不同语言中,它被理解成对象(object)、记录(record)、结构(struct)、字典(dictionary)、哈希表(hash table)、有键列表(keyed list)或者关联数组(associative array)。
  • 值的有序列表:大部分语言中,它被理解成数组(array)。

例如用以下 JSON 数据来描述一个人的信息:

{
  "name": "Frankie",
  "age": 20,
  "skills": ["Java", "JavaScript", "TypeScript"]
}

注意,JavaScript 不是 JSON,JSON 也不是 JavaScript。但 JSON 与 JavaScript 是存在渊源的,JSON 的数据格式是从 JavaScript 对象中演变出来的。(从名称上可以体现)

二、JSON 与 JavaScript 的区别

JSON 是一种数据格式,也可以说是一种规范。JSON 是用于跨平台数据交流的,独立于语言和平台。而 JavaScript 对象是一个实例,存在于内存中。JavaScript 对象是没办法传输的,只有在被序列化为 JSON 字符串后才能传输。

JavaScript 类型JSON 的不同点
对象和数组属性名称必须是双引号括起来的字符串;最后一个属性后不能有逗号
数值禁止出现前导零( JSON.stringify() 方法自动忽略前导零,而在 JSON.parse() 方法中将会抛出 SyntaxError);如果有小数点,则后面至少跟着一位数字。
字符串只有有限的一些字符可能会被转义;禁止某些控制字符; Unicode 行分隔符 (U+2028)和段分隔符 (U+2029)被允许 ; 字符串必须用双引号括起来。请参考下面的示例,可以看到 JSON.parse() 能够正常解析,但将其当作 JavaScript 解析时会抛出 SyntaxError 错误:
let code = '"\u2028\u2029"'
JSON.parse(code)  // 正常
eval(code)  // 错误

在 JavaScript 中,我们不能把以下对象叫做 JSON,如:

// 这只是 JS 对象
var people = {}

// 这跟 JSON 就更不沾边了,只是 JS 的对象
var people = { name: 'Frankie', age: 20 }

// 这跟 JSON 就更不沾边了,只是 JS 的对象
var people = { 'name': 'Frankie', 'age': 20 }

// 我们可以把这个称做:JSON 格式的 JS 对象
var people = { "name": "Frankie", "age": 20 }

// 我们可以把这个称做:JSON 格式的字符串
var people = '{"name":"Frankie","age":20}'

// 稍复杂的 JSON 格式的数组
var peopleArr = [
  { "name": "Frankie", "age": 20 },
  { "name": "Mandy", "age": 18 }
]

// 稍复杂的 JSON 格式字符串
var peopleStr = '[{"name":"Frankie","age":20},{"name":"Mandy","age":18}]'
尽管 JSON 与严格的 JavaScript 对象字面量表示方式很相似,如果将 JavaScript 对象属性加上双引号就理解成 JSON 是不对的,它只是符合 JSON 的语法规则而已。JSON 与 JavaScript 对象本质上是完全不同的两个东西,就像“斑马”和“斑马线”一样。

在 JavaScript 中,JSON 对象包含两个方法,用于解析的 JSON.parse() 和转换的 JSON.stringify() 方法。除了这两个方法,JSON 这个对象本身并没有其他作用,也不能被调用或者作为构造函数调用。

三、JSON.stringify()

将一个 JavaScript 对象或值转换为 JSON 字符串。

JSON.stringify(value, replacer, space)
  • 参数 value ,是将要序列化成 JSON 字符串的值。
  • 参数 replacer (可选),如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数未提供(或者值为 null),则对象所有的属性都会被序列化。
const people = {
  name: 'Frankie',
  age: 20
}

const peopleStr1 = JSON.stringify(people, ['name'])
const peopleStr2 = JSON.stringify(people, (key, value) => {
  if (typeof value === 'string') {
    return undefined
  }
  return value
})

console.log(peopleStr1) // '{"name":"Frankie"}'
console.log(peopleStr2) // '{"age":20}'
  • 参数 space (可选),指定缩进用的空白字符串,用于美化输出(pretty-print)。如果参数为数字,它表示有多少个空格,值大于 10 时,输出空格为 10,小于 1 则表示没有空格。如果参数为字符串,该字符串将被将被作为空格。如果参数没有提供(或者值为 null),将没有空格。注意,若使用非空字符串作为参数值,就不能被 JSON.parse() 解析了,会抛出 SyntaxError 错误。
一般来说,参数 replacerspace 平常比较少用到。

看示例:

const symbol = Symbol()

const func = () => { }

const people = {
  name: 'Frankie',
  age: 20,
  birthday: new Date(),
  sex: undefined,
  home: null,
  say: func,
  [symbol]: 'This is Symbol',
  skills: ['', undefined, , 'JavaScript', undefined, symbol, func],
  course: {
    name: 'English',
    score: 90
  },
  prop1: NaN,
  prop2: Infinity,
  prop3: new Boolean(true) // or new String('abc') or new Number(10)
}

const replacer = (key, value) => {
  // 这里我其实没做什么处理,跟忽略 replacer 参数是一致的。

  // 若符合某种条件不被序列化,return undefined 即可。
  // 比如 if (typeof value === 'string') return undefined
  
  // 也可以通过该函数来看看序列化的执行顺序。

  // console.log('key: ', key)
  // console.log('value: ', value)
  return value
}

// 序列化操作
const peopleStr = JSON.stringify(people, replacer)

// '{"name":"Frankie","age":20,"birthday":"2021-01-17T10:24:39.333Z","home":null,"skills":["",null,null,"JavaScript",null,null,null],"course":{"name":"English","score":90},"prop1":null,"prop2":null,"prop3":true}'
console.log(peopleStr) 

console.log(JSON.stringify(function(){})) // undefined
console.log(JSON.stringify(undefined)) // undefined
结合以上示例,有以下特点:
  • 非数组对象的属性不能保证以特定的顺序属性出现在序列化后的字符串中。(示例可能没体现出来)
  • 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。(如 prop3
  • undefined、任意的函数以及 symbol 值,在序列化过程中有两种不同的情况。若出现在非数组对象的属性值中,会被忽略;若出现在数组中,会被转换成 null
  • 函数、undefined 被单独转换时,会返回 undefined
  • 所有以 symbol 为属性值的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。
  • Date 日期调用了其内置的 toJSON() 方法将其转换成字符串(同 Date.toISOString()),因此会被当做字符串处理。
  • NaNInfinity 格式的数值及 null 都会被当做 null
  • 其他类型的对象,包括 MapSetWeakMapWeakSet,仅会序列化可枚举的属性。
  • 转换值如果含有 toJSON() 方法,该方法定义什么值将被序列化。
  • 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
针对最后两点举例说明:
  • 若对象本身实现了 toJSON() 方法,那么调用 JSON.stringify() 方法时,JSON.stringify() 会将这个对象的 toJSON() 方法的返回值作为参数去进行序列化。
const people = {
  name: 'Frankie',
  age: 20,
  toJSON: () => {
    return { name: 'Mandy' }
  }
}

console.log(JSON.stringify(people))
// 结果是 {"name":"Mandy"},而不是 {"name":"Frankie","age":20}
// 需要注意的是,若对象的 toJSON 属性值不是函数的话,仍然是该对象作为参数进行序列化。


// 上面还提到 Date 对象本身内置了 toJSON() 方法,所以以下返回结果是:
// "2021-01-17T09:40:08.302Z"
console.log(JSON.stringify(new Date()))


// 假如我去修改 Date 原型上的 toJSON 方法,结果会怎样呢?
Date.prototype.toJSON = function () { return '被改写了' }
console.log(JSON.stringify(new Date())) // "被改写了"
  • 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
const foo = {}
const bar = {
  b: foo
}
foo.a = bar
console.log(foo)

// 如果这时候对 foo 进行序列化操作,就会抛出错误。
JSON.stringify(foo) // Uncaught TypeError: Converting circular structure to JSON

foo 对象和 bar 对象会无限相互引用,可以看下 foo 打印结果如下,如果此时对 foo 进行序列化操作,就会抛出错误:Uncaught TypeError: Converting circular structure to JSON

针对这个问题,看看别人的解决方法,看这里 JSON-js。具体用法是,先引入其中的 cycle.js 脚本,然后 JSON.stringify(JSON.decycle(foo)) 就 OK 了。

JSON.stringify() 总结:
  1. 若被序列化的对象,存在 toJSON() 方法,真正被序列化的其实是 toJSON() 方法的返回值。
  2. 若提供了 replacer 参数,应用这个函数过滤器,传入的函数过滤器的值是第 1 步返回的值。
  3. 对第 2 步返回的每个值,进行相应的序列化。
  4. 如果提供了 space 参数,执行相应的格式化操作。

四、JSON.parse()

JSON.parse() 方法用来解析 JSON 字符串,构造由字符串描述的 JavaScript 值或对象。提供可选的 reviver 函数用以在返回之前对所得到的对象执行变换(操作)。

JSON.parse(text, reviver)
  • 参数 text,要被解析成 JavaScript 值的字符串。
  • 参数 reviver(可选)转换器,如果传入该参数(函数),可以用来修改解析生成的原始值,调用时机在 parse 函数返回之前。

    如果指定了 reviver 函数,则解析出的 JavaScript 值(解析值)会经过一次转换后才将被最终返回(返回值)。更具体点讲就是:解析值本身以及它所包含的所有属性,会按照一定的顺序(从最最里层的属性开始,一级级往外,最终到达顶层,也就是解析值本身)分别的去调用 reviver 函数,在调用过程中,当前属性所属的对象会作为 this 值,当前属性名和属性值会分别作为第一个和第二个参数传入 reviver 中。如果 reviver 返回 undefined,则当前属性会从所属对象中删除,如果返回了其他值,则返回的值会成为当前属性新的属性值。

    当遍历到最顶层的值(解析值)时,传入 reviver 函数的参数会是空字符串 ""(因为此时已经没有真正的属性)和当前的解析值(有可能已经被修改过了),当前的 this 值会是 {"": 修改过的解析值},在编写 reviver 函数时,要注意到这个特例。(这个函数的遍历顺序依照:从最内层开始,按照层级顺序,依次向外遍历

// JSON 字符串
const peopleStr = '{"name":"Frankie","age":20,"birthday":"2021-01-17T10:24:39.333Z","home":null,"skills":["",null,null,"JavaScript",null,null,null],"course":{"name":"English","score":90},"prop1":null,"prop2":null,"prop3":true}'

// 若需输出 this 对象,不能使用箭头函数
const reviver = (key, value) => {
  // 可以通过该函数来看看序列化的执行顺序。
  // console.log('key: ', key)
  // console.log('value: ', value)

  // 若删除某个属性,return undefined 即可。
  // 比如 if (typeof value === 'string') return undefined

  // 如果到了最顶层,则直接返回属性值
  if (key === '') return value

  // 数值类型,将属性值变为原来的 2 倍
  if (typeof value === 'number') return value * 2

  // 其他的原样解析
  return value
}

const parseObj = JSON.parse(peopleStr, reviver)

console.log(parseObj)

解析结果如下:

{
  "age": 40,
  "birthday": "2021-02-14T06:02:18.491Z",
  "course": { "name": "English", "score": 180 },
  "home": null,
  "name": "Frankie",
  "prop1": null,
  "prop2": null,
  "prop3": true,
  "skills": ["", null, null, "JavaScript", null, null, null]
}

五、其他

针对 Line separator 和 Paragraph separator 的处理,可以看这里:JSON.stringify用作 JavaScript

六、拓展

根据 ECMA-262 标准定义,一个字符串可以包含任何东西,只要它不是一个引号,一个反斜线或者一个行终止符。

以下被认为是行终止符:

  • \u000A - Line Feed
  • \u000D - Carriage Return
  • \u2028 - Line separator
  • \u2029 - Paragraph separator

七、参考

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 7 次点赞
  • 获得 6 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 6 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-12-03
个人主页被 2.1k 人浏览