JavaScript 语言精粹读书笔记

最近在看 赵泽欣 / 鄢学鹍 翻译的 蝴蝶书, 把一些读后感言记录在这里。
主要是把作者的建议跟 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 构造函数的几个缺点

  1. 没有私有环境,所有的属性都是公开的
  2. 无法访问 super (父类) 的方法
  3. 调用构造函数的时候忘记加 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 有两种使用模式:

  1. super 当作函数来使用,会调用父类的构造函数
  2. 使用 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)
原文地址:https://www.cnblogs.com/zf-l/p/notes_js_good_parts.html