最近在看 赵泽欣 / 鄢学鹍 翻译的 蝴蝶书, 把一些读后感言记录在这里。
主要是把作者的建议跟 ES5/ES5.1/ES6 新添加的功能进行了对比
涉及到的一些定义
- IIFE: Immediately Invoked Function Expression
立即执行函数表达式, 一般用来隐藏私有变量, 避免临时变量污染全局空间
常见的形式为:
(function(){
// function body
})()
更多请参见 这篇文章
- Hoisting: 变量定义提升
请参考 JavaScript Scoping and Hoisting
- 严格模式
一个 JavaScript 子集用作提供更彻底的错误检查。
更多内容请参考 MDN
- ES6 - ECMAScript 2015
ECMA组织制订的 JavaScript 语言的第6个标准
JavaScript 详细的历史版本请参考 阮一峰大神写的 JavaScript语言的历史。
以及这篇文章 ECMAScript版本历史
之前不知道的坑
- 在块注释中使用正则表达式可能出问题
如果在快注释中出现 "*/", JavaScript 解释器无法正常工作了。 下图代码执行时会报语法错误
/*/\d*/*/
- 尽量不要使用位运算
JavaScript 里面没有整数类型,只有双精度的浮点数。
因此位运算符会把它们的运算数从浮点数转换为整数,接着执行运算,然后再转换会浮点数。
在大多数语言中,这些运算符接近于硬件处理,所以非常快。
但 JavaScript 的执行环境一般接触不到硬件,所以非常慢
- parseInt
如果一个字符串中有数字和其他字符,parseInt 会解析从字符串开头出现的数字,直到字符串结尾或者碰到不可解析的字符。
var num = parseInt('3abcd'); // 3
parseInt 接受参数,第二个参数表示要解析数字的基数。该值介于 2~36 之间。
如果字符串以 '0x' 或者 '0X' 开头,praseInt函数就会把第一个参数当作十六进制字符串来解析。
作者建议手动指定第二个参数来避免意外的转换。
parseInt('0x3a'); // 58
parseInt('0x3a', 10); // 0
- 正则表达式函数对
g
标识的处理方法
函数 | 处理方式 |
---|---|
RegExp.prototype.test | 忽略 g 标识 |
RegExp.prototype.exec | 每次调用 exec 返回一次匹配 |
String.prototype.search | 忽略 |
String.prototype.split | 忽略 |
String.prototype.replace |
- 如果函数参数是一个正则表达式并且带有 g 标识,它会替换所有的匹配。 - 如果表达式没有带 g 标识,它会仅替换第一个匹配 |
String.prototype.match |
- 没有 g 标识, 与调用regex.exec(string)相同 - 表达式使用 g 标识 生成一个包含所有匹配(忽略捕获分组)的数组 |
严格模式下已经改进的缺点
- 全局的变量定义
传统的JS中,如果在函数中定义变量时没有使用 var
关键词, 那么这个变量就会被作为全局对象( 浏览器中的 window 对象, 或者 NodeJS 里面的 global 对象) 的属性.
严格模式下则不允许不适用 var/let/const 来定义变量。
// 使用 IIFE, 在函数中定义 name 变量并赋值
(function(){
name = "david";
})();
// 使用 hasOwnProperty window 对象 判断是否含有 name 属性
window.hasOwnProperty('name'); // true
// 使用严格模式
function func(){
'use strict';
name = 'david'; // Uncaught ReferenceError: a is not defined
}
- with 关键字不能使用
DC 不推荐使用 with 关键字来访问对象属性, 严格模式下已经禁用 with 关键字。
let
ES2016 引入了 let/const 来定义局部变量/常量。
let/const 的引入解决了书中的一些问题
- 变量重复定义
用户可以使用 var
在同一个函数作用于里面定义多个同名的变量。但是使用 let
不可以
{
let a = 1;
let a = 10; // Uncaught SyntaxError: Identifier 'a' has already been declared
}
- 拥有局部作用域
// 使用 var 定义的变量在 for 循环外可以引用
for (var i = 0; i < 10; i++) {}
console.log(i); //10
// 使用 let 定义的变量在 for 循环外不可使用
for(let j = 0; j < 10; j++) {}
console.log(j);// Error: j is not define
- 变量提升
使用 let
初始化的变量不会进行 Hoisting 变量定义提升。
console.log(foo); // 输出undefined
console.log(bar); // 报错ReferenceError
var foo = 2;
let bar = 2;
- 作用域
使用 let
在全局作用于定义的对象,也不过作为全局变量的属性。
let blog_link='http://www.cnblogs.com/zf-l/';
window.hasOwnProperty('blog_link'); //false
- 不需要使用 IIFE 来解决闭包问题
在 JS 中,如果一个函数多次调用函数外部的变量。可能会出现不可预知的问题:
// 在一个循环中使用定时器打印外层的变量值
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i); // 输出5次5
},0);
}
// 通过给临时函数赋值来解决
for (var k = 0; k < 5; k++) {
(function (k) {
setTimeout(function () {
console.log(k); //输出0,1,2,3,4
},0);
})(k);
}
// 使用 let 关键字
for (let j = 0; j < 5; j++) {
setTimeout(function () {
console.log(j); //输出0,1,2,3,4
},0);
}
已经实现的建议添加的函数
DC 书中列出了好多有用的函数,其中一些已经在 ES5.x/ES6 中实现
- Object.create
接受对象A作为参数,返回一个新的对象 B ,B 的原型是 A。
ES5 中已经实现。
Object.create = function(o){
var F = function(){};
F.prototype = o;
return new F();
}
- Array.prototype.reduce
Array.method('reduce', function(f, value){
vai i;
for(i = 0; i < this.length; i+=1){
value = f(this[i], value);
return value;
}
})
除了 reduce 函数, ES5 中还定义了这些有用的高阶函数
Array.prototype.reduceLeft
Array.prototype.some
Array.prototype.every
Array.prototype.map
Array.prototype.filter
Array.prototype.forEach
- 判断是否是数组
ES5 中定义了 Array.isArray 方法。 下面是书中的实现。
var is_array = function(value){
return Object.prototype.toString.apply(value) === '[object Array]';
}
- 柯里化
ES5 中可以通过 Function.prototype.bind
来实现函数的柯里化。下面是书中的实现方式
Function.method('curry', function(){
var slice = Array.prototype.slice,
args = slice.apply(arguments),
that = this;
return function(){
return that.apply(null, args.concat(slice.apply(arguments)));
}
})
面向对象方面的改进
在蝴蝶书里面,作者指出了 JavaScript 构造函数的几个缺点
- 没有私有环境,所有的属性都是公开的
- 无法访问 super (父类) 的方法
- 调用构造函数的时候忘记加 new 方法会造成严重的危害
其中第一个缺点 ES6 中还没有解决,另外两个已经可以防止。
- 使用 new 前缀调用构造函数
在 JavaScript 中, 如果在调用构造函数时忘记了在前面加上 new
前缀,那么 this 将不会绑定到一个新对象上。而是绑定到全局对象。
所以作者建议:
类名使用首字母大写的形式,并且不以首字母大写的形式拼写任何其他的东西。这样至少可以通过目视检查去发现是否缺少了 new 前缀。
一个更好的备选方案是不适用 new。
ES6 新添加了 class
语法, 使用 class
前缀定义的 类必须使用 new
前缀来调用,否则解释器就会报错。
class A{
constructor(){
console.log(new.target.name);
}
}
A(); // Uncaught TypeError: Class constructor A cannot be invoked without 'new'
注: 在构造方法调用中,new.target
指向被 new
调用的构造函数。
- 调用父类的方法和属性
ES6 提供了 super
关键字访问父类的构造函数和方法
super 有两种使用模式:
- 把
super
当作函数来使用,会调用父类的构造函数 - 使用
super.prop
来访问父类的属性和方法
下面 MDN 中的例子说明了如何使用 super 调用父类的构造函数和方法
class Rectangle{
constructor(height, width) {
this.name = 'Rectangle';
this.height = height;
this.width = width;
}
sayName() {
console.log('Hi, I am a ', this.name + '.');
}
}
class Square extends Rectangle{
constructor(length) {
this.height; // ReferenceError, super 必须在 this 之前调用
// 调用父类的构造函数,提供长方形的长度和宽度
super(length, length);
// Note: 在子类中,super() 必须在使用 this 之前调用
// 否则解释器会抛出 reference error.
this.name = 'Square';
}
get area() {
return this.height * this.width;
}
set area(value) {
this.height = this.width = Math.sqrt(value);
}
sayName() {
// 调用父类的同名方法
super.sayName();
console.log('Hi from Square .');
}
}
ES6 的其他改进
枚举数组
for in 可以用来遍历一个数组的所有属性, 遗憾的是, for in 无法保证属性的顺序,而大多数要遍历数组的场合都希望按照阿拉伯数字顺序来产生元素
ES6 提供了 for of
语法来遍历数组
// 使用 for 循环遍历数组
var i;
for(i = 0; i < myArray.length; i += 1){
console.log(myArray[i]);
}
// 使用 for of 遍历数组
for(let i of myArray){
console.log(i);
}
不定参数
在 ES6 出现之前, JS 处理不定参数只能使用 arguments
关键字。在文中作者如是说
因为语言的一个设计错误,
arguments
并不是一个真正的数组。它只是一个 "类似数组 (array-like)" 的对象。arguments 拥有一个 length 属性,但它没有任何数组的方法
下面的代码中分别使用 ES5 和 ES6 的语法实现了函数默认参数
var sum = function(){
var i, sum = 0;
for (i=0; i < arguments.length; i+=1){
sum += arguments[i];
};
return sum;
}
document.writeln(sum(4, 8, 15, 16, 23, 42)); // 108
相比较之下 ES6 中不定参数的实现方式就比较优雅, 跟 Python 中的 *args
类似。
var sum = function(...nums){
// 判断是否是数组
console.log(Array.isArray(nums)); // true
return nums.reduce((x,y)=>x+y);
}
document.writeln(sum(4, 8, 15, 16, 23, 42)); // 108
参数默认值
ES6 之前函数不支持在参数列表中指定默认值。
作者往往使用 ||
给函数参数选择是否使用默认值。ES6 提供了在函数形参中指定默认参数的方法。
下面的例子中分别使用 ES5 和 ES6 的语法定义默认参数
// 使用 || 在函数体中定义默认值
var factorial = function factorial(i, a){
a = a || 1;
if(i<2){
return a;
}
return factorial(i-1, a*i);
}
console.log(factorial(4)); // 24
// 使用 ES6 的默认参数重新定义 factorial 函数
var factorial = function factorial(i, a=1){
if(i<2){
return a;
}
return factorial(i-1, a*i);
}
console.log(factorial(4)); // 24
有争议的部分
自动分号
DC 建议 JS 语句每行的结尾都要加分号。但是随着技术的发展,有不同的声音提出来:
其他语法建议
- 使用字符串的 slice 替换 substring
slice
可以接受负数作为参数,完全可以替代substring
ES6 入门资料
下面的列表我整理的一些学习 ES6 的资料和书籍,供各位看官参考
转载请注明出处: [zf-l](http://www.cnblogs.com/zf-l/p/notes_js_good_parts.html)