哈哈,各位园友新年快乐!愚安好久没在园子里写东西了,这次决定针对javascript做一个系列,叫做《小王子浅读Effective javascript》,主要是按照David Herman的《Effective javascript》的内容做一些解读和扩展。原书中有68点针对javascript编程的小tips,很是经典,园友们也可以网上找找PDF读一下,写的很不错,也可以买买原著读读,提醒下,这本书的中文译本也已经出了。为什么叫“浅读”,一方面是愚安并不是专业javascript程序员,有些地方的理解可能不够深刻,也不会按照原著条目的顺序和数量来写,另一方面,人要低调嘛,哈哈哈!为什么叫小王子呢?愚安我的一系列ID经常被同学吐槽太屌丝,所以在这个系列中暂用“小王子”这个高大上的ID。
废话说的有点多,进入正题。
JavaScript是ECMAScript脚本语言的一个分支。ECMAScript 是Ecma国际组织标准化的,这门语言的另外2个分支是ActionScript(macroMedia,Adobe)和JScript(微软)。JavaScript是有Netscape的Brendan Eich开发的,最初叫Mocha而后是LiveScript,最后改为JavaScript。1993.3月,Sun公司发布了支持JavaScript的Navigator2.0(译者:我感觉这是错误的,可查看原文)。鉴于JavaScript作为客户端脚本语言取得广泛流行,微软制定了自己的脚本语言JScript,发布于1996.8月的ie3.0中。Netscape公司在日内瓦提交了JavaScript给Ecma国际标准化组织,申请成为标准。
版本一览及浏览器支持情况
需要指出的是,目前所有的主流 Web 浏览器都支持 ECMA-262 第三版,即JavaScript 1.5版本,JavaScript 1.6-1.9只是ECMAScript (JavaScript on Gecko)升级至JavaScript 2.0的临时代号。
var JS_ver = [];
(Number.prototype.toFixed)?JS_ver.push("1.5"):false;
([].indexOf && [].forEach)?JS_ver.push("1.6"):false;
((function(){try {[a,b] = [0,1];return true;}catch(ex) {return false;}})())?JS_ver.push("1.7"):false;
([].reduce && [].reduceRight && JSON)?JS_ver.push("1.8"):false;
("".trimLeft)?JS_ver.push("1.8.1"):false;
JS_ver.supports = function()
{
if (arguments[0])
return (!!~this.join().indexOf(arguments[0] +",") +",");
else
return (this[this.length-1]);
}
alert("Latest Javascript version supported: "+ JS_ver.supports());
alert("Support for version 1.7 : "+ JS_ver.supports("1.7"));
于某种原因,Javascript 1.7版本的某些特性是没有得到广泛的支持。不过大部分浏览器都支持了1.8版和1.8.1版的特性。(注:所有的IE浏览器(IE8或者更老的版本)只支持1.5版的Javascript)这个脚本,既能通过检测特征来检测JavaScript版本,它还能检查特定的Javascript版本所支持的特性。
由于最终用户可能使用不同Web浏览器的不同版本,因此,我们必须精心地编写Web程序,使得其在所有的浏览器上始终工作如一。
接下来,我们来聊一下各个版本的javascript的一些不同特性;
首先,原书中讲到const定义常量,这个特性在ECMAScript标准中,并未定义,但很多版本的js中都把const作为关键字,用于定义常量。但各个实现并不相同,有的无法更改,有的只是跟var一样,可以更改。
const PI = 3.141592653589793;
PI = "modified!";
PI; // 3.141592653589793
或者
const PI = 3.141592653589793;
PI = "modified!";
PI; // "modified!"
在ie下,对const定义的常量进行再赋值,会弹出语法错误。
书中,提到的第二个例子是“use strict”。我们知道,为了更好的规范,和更好的兼容性,我们会在js代码中加入“use strict”。同样,你也可以在函数体的开始处加入这句指令以启用该函数的严格模式。
function (x){
"use strict";
//....
}
使用字符串字面量作为指令语法看起来有点怪异,但它的好处是向后兼容。由于解释执行字符串字面量并没有任何副作用,所以ES3引擎执行这条指令是无伤大雅的。ES3引擎解释执行该字符串,然后立即丢弃其值。这使得编写的严格模式的代码可以运行在旧的JavaScript引擎上,但有一个重要的限制:旧的引擎不会进行任何的严格模式检查。如果你没有在ES5环境中做过测试,那么,编写的代码运行于ES5环境中就很容易出错。
function f() {
var arguments = []; // error: redefinition of arguments
// ...
}
在严格模式下,不允许重定义arguments变量,但没有实现严格模式检查的环境会接受这段代码。然而,这段代码部署在实现ES5的产品环境中将导致程序出错。所以,你应该总是在完全兼容ES5的环境中测试严格代码。
“use strict”指令只有在脚本或函数的顶部才能生效,这也是使用严格模式的一个陷阱。这样,脚本连接变得颇为敏感。对于一些大型的应用软件,在开发中使用多个独立的文件,然而部署到产品环境时却需要连接成一个单一的文件。例如,想将一个文件运行于严格模式下,我们只需要在文件开头加入“use strict”,然而将多个javascript文件合并时,“use strict”只在开头时出现,其后面的代码才生效,所以在合并文件时,应当将其放在脚本的最开头。如:
// file2.js
// 非严格模式
function g() {
var arguments = [];
// ...
} // ...
// file1.js
"use strict";
function f() { // 严格模式不在生效
// ...
} // ...
在自己的项目中,你可以坚持只使用“严格模式”或只使用“非严格模式”的策略,但如果你要编写健壮的代码应对各种各样的代码连接,你有两个可选的方案。
第一个解决方案是不要将进行严格模式检查的文件和不进行严格模式检查的文件连接起来。这可能是最简单的解决方案,但它无疑会限制你对应用程序或库的文件结构的控制力。在最好的情况下,你至少要部署两个独立的文件。一个包含所有期望进行严格检查的文件,另一个则包含所有无须进行严格检查的文件。
第二个解决方案是通过将其自身包裹在立即调用的函数表达式(Immediately Invoked Function Expression, IIFE)中的方式连接多个文件。总之,将每个文件的内容包裹在一个立即调用的函数中,即使在不同的模式下,它们都将被独立地解释执行。
// 未定义严格模式
(function () {
// file1.js
"use strict";
function f() {
// ...
}// ...
})();
(function () { // file2.js
// 非严格模式
function f() {
var arguments = []; // ...
}// ...
})();
编写文件使其在两种模式下行为一致。想要编写一个库,使其可以工作在尽可能多的环境中,你不能假设库文件会被脚本连接工具置于一个函数中,也不能假设客户端的代码库是否处于严格模式或者非严格模式。要想构建代码以获得最大的兼容性,最简单的方法是在严格模式下编写代码,并显式地将代码内容包裹在本地启用了严格模式的函数中。这种方式类似于前面描述的方案——将每个文件的内容包裹在一个立即调用的函数表达式中,但在这种情况下,你是自己编写立即调用的函数表达式并且显式地选择严格模式,而不是采用脚本连接工具或模块系统帮你实现。
(function () {
"use strict";
function f() {
// ...
}
// ...
})();
要注意的是,无论这段代码是在严格模式还是在非严格模式的环境中连接的,它都被视为是严格的。相比之下,即使一个函数没有选择严格模式,如果它连接在严格代码之后,它仍被视为是严格的。所以,为了达到更为普遍的兼容性,建议在严格模式下编写代码。
- David在这个Item的最后,做了个精悍的小提示:
- 决定你的应用程序支持JavaScript的哪些版本 。
- 确保你使用的任何JavaScript的特性对于应用程序将要运行的所有环境都是支持的。
- 总是在执行严格模式检查的环境中测试严格代码。
- 当心连接那些在不同严格模式下有不同预期的脚本。
小王子扩展阅读:
ECMAScript 6 是JavaScript的下一个标准,正处在快速开发之中,大部分已经完成了。预计Mozilla将在这个标准的基础上,推出JavaScript的 2.0。ECMAScript 6 的目标,是使得JavaScript可以用来编写复杂的应用程序、函数库和代码的自动生成器(code generator)。最新的浏览器已经部分支持ECMAScript 6 的语法,可以通过《ECMAScript 6 浏览器兼容表》查看浏览器支持情况。
在Chrome下,可以访问chrome://flags 下,开启javascript实验特性,可以做一些测试
let关键字
let关键字类似于var,用来声明变量,但是该变量只在声明所在的块级作用域有效。
下面的代码如果使用var,最后输出的是10。
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6]();
如果使用let,声明的变量仅在块级作用域内有效,最后输出的是6。
var a = [];
for (var i = 0; i < 10; i++) {
let c = i;
a[i] = function () {
console.log(c);
};
}
a[6]();
let实际上为JavaScript新增了块级作用域。
function doSomething() {
let N = 5;
if (someCondition) {
let N = 10;
doSomethingElse(N);
}
console.log(N); // 5
}
上面的代码有两个代码块,都声明了变量N。可以看到,外层代码块不受内层代码块的影响。如果使用var定义变量,最后输出的值就是10。
const关键字
const与let的作用相似,也用来在块级作用域声明变量。但是,它声明的是常量,一旦声明,它的值就不能改变。
const PI = 3.1415;
PI // 3.1415
PI = 3;
PI // 3.1415
const PI = 3.1; PI // 3.1415
Set数据结构
ECMAScript 6 提供了新的数据结构Set。它类似于数组,但是所有值都是唯一的。
var e = new Set(); // 新建集合
e.add("1") // 加入集合
e.add("2")
e.add("3")
e.add("4")
e.add("4") // 注意“4”被加入了两次
e.has("1") // true
e.has("4") // true
e.has("5") // false
e.delete("4"); // delete item
e.has("4") // false
Map数据结构
ECMAScript 6 还提供了map数据结构。它就是一个键值对的数据结构,类似于对象,但是“键”的范围不限于字符串。
var es6 = new Map(); // 新建Map
es6.set("edition", 6) // 键是字符串
es6.set(262, "standard") // 键是数值
es6.set(undefined, "nah") // 键是undefined
var hello = function() {
console.log("hello");
}
es6.set(hello, "Hello ES6!") // 键是函数
es6.has("edition") // true
es6.has("years") // false
es6.has(262) // true
es6.has(undefined) // true
es6.has(hello) // true
es6.delete(undefined) // delete map
es6.has(undefined) // false
es6.get(hello) // Hello ES6!
es6.get("edition") // 6
函数的多余参数
ECMAScript 6引入扩展运算符(...),允许获取函数的多余参数。
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
console.log( item );
});
}
var planets = [];
console.log("太阳系的内层行星是:" ); // 1个固定参数 + 4个可变参数
push(planets, "Mercury", "Venus", "Earth", "Mars");
**********golang玩家,是不是觉得很熟悉,O(∩_∩)O哈哈~
这种表示法不仅可以用于函数定义,还可以用于函数调用。
function createURL (comment, path, protocol, subdomain, domain, tld) {
var url = comment + ": " + protocol + "://" + subdomain + "." + domain + "." + tld + "/" + path;
console.log(url);
}
var weblink = ["hypertext/WWW/TheProject.html", "http", "info", "cern", "ch"],
comment = "世界上第一个网站";
createURL(comment, ...weblink ); // spread operator
从上面的例子可以看出,扩展运算符可以将数组转变成正常的参数序列。
var max = Math.max(...[14, 3, 77]);
generator 函数
ECMAScript 6 引入了generator 函数,允许函数内部暂停执行某些操作。
function* foo() {
yield 'foo';
yield 'bar';
yield 'baz';
}
上面就是一个generator函数,定义时function关键字后面加上星号。然后,函数内部就可以使用yield关键字,表示暂停执行某个操作,等到外部调用next方法时再执行。
var x = foo();
x.next().value // 'foo'
x.next().value // 'bar'
x.next().value // 'baz'
关于generator,node.js-11.24(unstable)已经开始支持。由于小王子我也曾经用node.js写过一些东西,对其关注度还算高,最新的Koa框架就是基于这一特性编写的。稍后,
我将为generator专门写一遍随笔,向一些对其还不是很了解的园友,介绍下这方面的内容。
简洁的方法定义
ECMAScript 6 允许直接写入函数,作为对象的方法。这样的书写更加简洁。
// ES 6
var Person = {
name: 'Joe',
hello() {
console.log('Hello, my name is', this.name);
}
};
回调函数的简洁写法
ECMAScript 6 允许函数的简写形式作为回调函数,不再需要function和return关键,最后一个表达式就是函数的返回值。
// ES 5
[1,2,3].map(function (x) {
return x * x;
});
// ES 6
[1,2,3].map(x => x * x);
函数参数的默认值
ECMAScript 6 允许为函数的参数设置默认值。
function history(lang = "C", year = 1972) {
return lang + " was created around the year " + year;
}
**********PHPer是不是也觉得很熟悉啊O(∩_∩)O哈哈~
数组处理的简洁写法
ECMAScript 6 提供简洁写法,对数组进行处理。
// ES 5
[1, 2, 3].map(function (i) {
return i * i;
});
// ES 6
[for (i of [1, 2, 3]) i * i];
// ES 5
[1,4,2,3,-8].filter(function(i) {
return i < 3;
});
// ES 6
[for (i of [1,4,2,3,-8]) if (i < 3) i];
新引入的for...of运算符,可以直接跟在表达式的前面或后面。
// 一重循环
var temperature = [0, 37, 100];
[t + 273 for (t of temperature)]; // [273, 310, 373]
// 三重循环
var a1 = ["x1", "y1"],
a2 = ["x2", "y2"],
a3 = ["x3", "y3"];
[(console.log(s + w + r))
for (s of a1)
for (w of a2)
for (r of a3)
];
多变量赋值
ECMAScript 6 允许简洁地对多变量赋值。
正常情况下,将数组元素赋值给多个变量,只能一次次分开赋值。
var first = someArray[0]; var second = someArray[1]; var third = someArray[2];
在ECMAScript 6 中可以写成
var [first, second, third] = someArray;
这种赋值写法在语法上非常灵活。
var [ start, end ] = ["earth", "moon"]
[start, end] = [end, start] // 变量互换
var [foo, [[bar], baz]] = [1, [[2], 3]]
var [,,third] = ["foo", "bar", "baz"]
var [head, ...tail] = [1, 2, 3, 4]
*********上面这个变量互换,是不是又让golang玩家兴奋了,所以说现代语言的进步,一大方面,就是简化一些操作的写法,提高编程效率
它还可以接受默认值。
var [missing = true] = [];
console.log(missing) // true
var { x = 3 } = {};
console.log(x) // 3
它不仅可以用于数组,还可以用于对象。
var { foo, bar } = { foo: "lorem", bar: "ipsum" };
console.log(foo) // "lorem"
console.log(bar) // "ipsum"
var o = { p1: [ "Hello", { p2: "World" } ] };
var { a: [p1, { p2 }] } = o;
console.log(p1) // "Hello"
console.log(p2) // "World"
有了这种用法,函数定义和调用时,使用参数就很方便。
function f({p1, p2, p3}) { // ... }
赋给函数参数默认值,也容易多了。
jQuery.ajax = function (url,{
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true, // ... more config }
) {
// ... do stuff
};
for...of结构
JavaScript的for...in结构,只能获得键,不能直接获取值。
var planets = ["Mercury", "Venus", "Earth", "Mars"];
for (p in planets) {
console.log(p); // 0,1,2,3
}
var es6 = {
edition: 6, committee: "TC39", standard: "ECMA-262"
};
for (e in es6) {
console.log(e); // edition, committee, standard
}
ECMAScript 6 提供for...of结构,允许获得值。
var planets = ["Mercury", "Venus", "Earth", "Mars"];
for (p of planets) {
console.log(p); // Mercury, Venus, Earth, Mars
}
var engines = Set(["Gecko", "Trident", "Webkit", "Webkit"]);
for (var e of engines) {
console.log(e); // Set only has unique values, hence Webkit shows only once
}
var es6 = new Map();
es6.set("edition", 6);
es6.set("committee", "TC39");
es6.set("standard", "ECMA-262");
for (var [name, value] of es6) {
console.log(name + ": " + value);
}
数据结构
class结构
ECMAScript 6 提供了“类”。在此之前,一般用构造函数模拟“类”。
// ES5
var Language = function(config) {
this.name = config.name;
this.founder = config.founder;
this.year = config.year;
};
Language.prototype.summary = function() {
return this.name + " was created by " + this.founder + " in " + this.year;
};
ECMAScript 6 允许使用class结构,达到同样的效果。
// ES6
class Language {
constructor(name, founder, year) {
this.name = name;
this.founder = founder;
this.year = year;
}
summary() {
return this.name + " was created by " + this.founder + " in " + this.year;
}
}
// 生成实例
var js = new Language;
上面代码的constructor方法,就是类的构造函数。
class结构还允许使用extends关键字,表示继承。
class MetaLanguage extends Language {
constructor(x, y, z, version) {
super(x, y, z);
this.version = version;
}
}
上面代码的super方法,表示调用父类的构造函数。
************在面向对象这一点上,javascript一直备受诟病,当然也有人觉得原型链的继承方式很赞。但,大体上,还是批评者较多,所以Google推出了Dart语言,基本上与ES6的特性类似。
module定义
ECMAScript 6 允许定义模块。也就是说,允许一个JavaScript脚本文件调用另一个脚本文件。
假设有一个circle.js,它是一个单独模块。
// circle.js
const PI = 3.1415;
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
然后,main.js引用这个模块。
// main.js
import { area, circumference } from 'circle';
console.log("Area of the circle: " + area(4) + " meter squared");
console.log("Circumference of the circle: " + circumference(14) + " meters");
*****************import,from的写法与python又很类似,还是那句话javascript语言的进步都是在从别的语言中学习优点。
好了,第一篇就写到这里了,目测,有点长,有什么疏漏的地方,还望各位园友指出批评。
谢谢大家!!!