Skip to content

JS-the-good-parts

JS 语言精粹 蝴蝶书

统计信息:字数 21996 阅读44分钟

作者电子书网址:https://www.crockford.com/books.html

第一张 精华

JS 语言的特殊性造成了精华和糟粕并存

第二章 语法

// page 18
Function.prototype.method = function (name, func) {
  this.prototype[name] = func;
  return this;
}

注释

在JS中使用注释最好使用//。不使用/**/的形式。因为正则表达式和注释可能发生冲突造成语法错误。

在JS中number就是64位的浮点数,没有int的概念。所以在JS中1.0 === 1

指数:100 = = = 1e2 = = = 1 * 10 * 10

字符串是不可变的:如果使用+进行连接,那么是创建一个新的字符串(并不是在原来的基础上加一个字符)。

'cat'.length === 3

for (let key in obj) {
  if (obj.hasOwnProperty(key)) {
    console.log(key);
  }
}

判断属性名来自于对象的成员还是原型链

try-catch-throw 还是不熟练

typeof(null) => 'Object'

在JS中%表示取余数。当两个运算数都是正数和求模运算一致,但是存在负数,就出现不一致的情况。

原型

公用的函数的方法可以放在对象的原型中(在es6中react中,直接放在组件的类中,作为组件的方法实现复用效果)。私有的方法单独作为对象的方法直接添加即可。

if (typeof Object.beget !== 'function') {
  Object.create = function(o) {
    var F = function() {};
    F.prototype = o;
    return new F();
  };
}

var another = Object.create(stooge);
another.nickname = "Moe";

原型连接在更新时是不起作用的。当我们对于对象作出改变,不会触及该对象的原型;

hasOwnproperty 会检查对象的属性,不会检查对象原型链上的属性

第三章 对象

属性委托(原型链)

如果尝试获取某个对象的属性值,但是这个对象没有对应的属性名,那么就会去原型链中逐层寻找这个属性。如果到达终点的Object.prototype没有,返回一个undefined值。

原型关系是动态的关系:如果我们给一个对象添加新的属性到原型中,那么该属性会对所有基于该原型创建的对象可见;

反射reflection

检查一个对象具有某个属性是很容易的事情;使用typeof可以获取对象的属性的数据类型。

存在一个问题:typeof对于原型中的任何属性都会产生值(例如construction 产生 function) 解决方案:1.让程序检查并丢掉函数的属性;2.使用hasOwnProperty方法,将对象中独有的属性返回,原型链中方法不会检查。

枚举 enumeration 循环遍历对象中的属性

遍历对象中的属性分为两种情况

1.未知对象的属性名:使用for-in遍历对象的属性,使用typeof过滤函数,使用hasOwnProperty过滤原型链的部分。

for (let name in object) {
  if(typeof object[name] !== 'function') {
    console.log(name + ':' + object[name]);
  }
}

2.已知函数的属性名:使用一个数组存放函数的属性名;获取数组的长度i,使用for遍历对象的属性值。

let properties = ['name', 'age', 'sex'];
for (let i = 0; i < properties.length; i++) {
  console.log(properties[i] + ":" + object[properties[i]]);
}

可以获取正确顺序的属性(不需要考虑原型链的属性)

删除属性 delete

删除对象的属性:如果对象具有某个属性,会删除这个属性。删除操作不会触及原型链中的任何对象及属性;删除这个对象的属性,如果原型链中还有这个属性,那么还可以获取这个属性(原型链上的属性);

减小全局变量的污染:使用let 创建局部变量在函数中。使用闭包形式向外暴露有限的接口。将全局性的资源加载到一个容器中,这样一个程序和其他程序的冲突就会降低。

第四章 函数

函数是功能实现的基本单元;一个函数的功能应当简单;编程就是把一组需求分解成一组函数和数据结构的技能。避免在一个函数内部实现多个功能。

使用字面量创建的函数链接到一个 Object.prototype ,使用函数表达式创建的函数,链接到一个Function.prototype,最终通过原型链链接到 Object.prototype.

函数在创建后,会具有一个prototype属性,它的值具有一个constructor属性就是该函数的对象。

函数可以被其他部分调用。

定义函数

函数的四部分:Function、函数名(匿名函数)、函数参数(parameters,形参,可选)、函数体。函数可以作为另一个函数的参数或者返回值。子函数可以访问内部的变量和参数,也可以访问外部父函数的变量和参数。函数的闭包。

调用函数

函数在调用过程中,除了传入的参数(括号中的参数),还默认传入this(this的值取决于函数的四种调用模式)和arguments(实参)。当实参大于形参,arguments会存储当前的函数全部实参。

arguments在函数参数不确定的时候可以使用(或者较多参数)arguments是一个伪数组,不具有数组的方法,这就是局限性。

let sum = function() {
  let sum = 0;
  for (let i = 0; i < arguments.length; i ++) {
    sum += arguemnts[i];
  }
  return sum;
};
函数的四种调用模式

1、方法调用模式

函数作为对象的方法。this指向这个对象。通过this,可以使得内部函数访问对象中的公共方法和属性。

let myObject = {
  value: 0,
  increase = (inc) => {
    this.value += typeof inc === 'number' ? inc : 1;
    } 
};
// 如果传入的参数是数值,函数的value属性叠加,不是数值就 + 1;
myObject.increase('test');
myObject.increase(2);
console.log(myObject.value === 3);

2、函数调用模式

一个函数不是作为对象的方法被调用,而是作为函数的形式被调用,此时 this 指向全局变量。

let myObject = {
  value = 3;
}
myObject.double = function () {
  let that = this;
  let helper = function () {
    that.value = add(that.value, that.value);
    console.log(this); //全局变量
  };
  helper(); //以函数的形式调用函数 helper
}
myObject.double();
console.log(myObject.value);

3、构造器调用模式

使用new构造器创建函数,函数中的this绑定到新创建的对象上。(使用构造函数创建新对象是早期的方法,现在使用class关键字创建js中的class)。这种方法在最新的代码中不使用。

let Quo = function (string) {
  this.status = sy=tring;
}
Quo.prototype.getStatus = function() {
  return this.status;
}
let myQuo = new Quo("test");
myQue.getStatue();

4、Apply调用模式:this指向apply中的第一个参数;第二个参数是参数数组;(对比call方法的使用)。

let array = [3, 4];
let sum = add.apply(null, array);
console.log(sum === 7);

let statusObject = {
  status: "OK"
};
let status = Quo.prototype.getStatus.apply(statusObject);
console.log(status === 'OK');

函数中遇到return就不会继续执行后面的代码了。

异常处理

异常:干扰函数正常执行过程中的事故(函数需要传入数值型参数,实际上传入字符串参数),当发现这样的事故,程序需要抛出一个异常。

throw语句会中断函数的执行;会抛出一个exception对象:具有异常的name属性和描述异常的message属性。

异常可以被try-catch捕获。产生异常的代码被try语句捕获,函数会跳转到catch语句执行。一个try语句只会有一个捕获所有异常的catch(一个try中只有一个catch)

如果解决异常的手段取决于异常的类型,异常检查器需要检查异常对宪法的name属性来确定异常的类型。

function add = (a, b) => {
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw {
      name: 'TypeError',
      message: 'add function needs number'
    };
  }
  return a + b;
}
function tryIt = function() {
  try {
    add("ten", 20);
  }
  catch (e) {
    console.log(e);
  }
}
递归

递归在dom树结构中使用较多;

let walk_the_dom = fucntion walk (node,func) {
  // 把节点和处理的函数传入,对当前节点进行处理;
  func(node);
  node = node.firstChild;
  while (node) {
    // 遍历每一个子节点,并递归处理
    walk(node, func);
    node = node.nextSibling;
  }
};

let getElementsByAttribute = function (att, value) {
  let results = [];

  walk_the_dom(document.body, fucntion(node) {
    let actual = node.nodeType === 1 && node.getAttribute(att);
    if (typeof actual === 'string' && (actual === value || typeof value !== 'string')) {
      results.push(node);
    }
  });
  return results;
}
作用域

es6中已经有块级作用域;

闭包

需要巩固

第五章 继承

需要巩固

第六章 数组

在JS中数组和对象实质都是对象,数组的属性是连续的整数。typeof(Array) = 'object'

// 判断数组的方法
var is_array = function(value) {
  return value && typeof value === 'object' && value.constructor === Array;
};
// 不足:识别不同的window或者frame失败

//改进方法
var is_array = function(value) {
  return Object.prototype.toString.apply(value) === '[object Array]';
}

数组中的方法存放在 Array.prototype 中,对象 Object.prototype 可以被扩充,数组的原型方法也可以被扩充。

Array.method('reduce', fucntion(f, value){
  for (let i = 0; i < this.length; i ++){
    value = f(this[i], value);
  }
  return value;
});
// 给数组扩展方法:传入一个函数和初始值,对数组的每一项运行函数。下面是实际案例。
let data = [1, 2, 3, 4];
var add = function(a, b) {
  retunr a + b;
}
var mult = function(a, b) {
  retunr a * b;
}
var sum = data.reduce(add, 0);
// 将数组执行add方法,初始值是0
var prodect = data.reduce(mult, 1);
// 将数组执行mult方法,初始值是1

一个数组可以通过下标设置属性,同时可以直接使用点语法设置属性。可以说,array.length 就是类似的对象的点语法。

JS 数组没有多维数组,支持元素为数组的数组,这里我们构造一个矩阵。

Array.matrix = function(m, n, init) {
  var mat = [];
  for (var i = 0; i < m; i++) {
    a = [];
    for (var j = 0; j < n; j++) {
      a[j] = initial;
    }
    mat[i] = a;
  }
  return mat;
}

var myMatrix = Array.matrix(4, 4, 0);
document.writeLn(myMatrix);
// 生成一个0填充的 4*4 的矩阵。

// 单位矩阵
Array.identity = function(n) {
  let mat = Array.matrix(n, n, 0);
  for (let i = 0; i < n; i++) {
    mat[i][i] = 1;
  }
  return mat;
};
myMatrix = Array.identity(4);
document.writeln(myMatrix[3][3]);

第七章 正则表达式

正则表达式书写很复杂,后期维护相对复杂。写的时候最好是短小精悍。

var parse = /^(?:[A-Za-z]+)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/;

// 划分网址

第九章 方法

Array

Array.concat 将多个数组,变量组合成一个新的数组。这个数组会浅复制一个数组,并将其他元素或者数组插入到新数组后面。

Array.join(seperator) 将数组的不同元素连接成字符串,原理是将每一项转化成一个字符串,将这些字符串连接。

Array.pop() 移出数组中的最后一个元素并返回(类似于堆栈stack),如果是空数组,那么返回undefined。

Array.push(item1, item2)在数组最后添加一个元素,原始数组改变,返回值是新数组的长度。

Array.reverse() 数组前后翻转,原始数组变成新数组,返回值就是新数组。

Array.shift() 移出数组的第一个元素(空数组返回undefined)shift 比 pop 速度慢很多。

array.unshift 在第一位增加元素,返回值是新数组的长度

array.slice(a, b) 浅复制数组的一部分,前面试闭区间后面是开区间;如果只有一个参数,表示从这个参数复制到数组的末尾。=>复制旧数组中的一部分

Array.sort() 字符串排序:可以给字符串进行排序,不能直接给数组排序(首先将数组转化成字符串,对字符串进行比较,通常会出错);当然,可以在此基础上,增加一个数组排序或者对象排序的方法。

Array.splice(a, b, c) 删除数组的一部分:将数组的a位置,删除b个元素,加入c元素。(c可选参数)

var a = [1, 2, 3];
var b = [4, 5, 6];
var c = a.concat(b, true);
a.push(6,7); // 这里改变数组a, 不会影响新的数组c。因为已经产生了新的数组c。
console.log(c);
// [1,2,3,4,5,6,true]

Function

// function.apply 传递一个绑定到 this 上的对象和一个可选的数组作为参数。
Function.method('bind', function(that) {
  // 返回一个函数。调用这个函数就是调用这个对象的一个方法
  var method = this;
  var slice = Array.prototype.slice;
  var args = slice.apply(arguments, [1]);
  return function() {
    return method.apply(that,
      args.concat(slice.apply(arguments, [0])));
  };
});

let x = function() {
  return this.value;
}.bind({ value: 666 });

alert(x());

数组的 call 和 apply :将一个对象的方法放在另一个对象上面,另一个对象可以使用这些方法。

Obj1.method1.call(Obj2, para1, para2); object 具有method1 方法,但是 object2 没有method1方法。所以这里Object2 借用object1 的method 方法,然后传入的参数是两个,进行继承。

Obj1.method2.apply(Obj2, [para1, para2]) apply 传入的参数是一个数组,其他的功能和效果类似。

详见 call.js 代码

Number

数值转化成不同字符串的几种方法

// toExponential() 转换成一个指数形式的字符串;
Math.PI.toExponential(2); // 3.14e+0

// toFixed 转化成一个十进制数的字符串,参数是小数点后的位数
Math.PI.toFixed(5); // 3.14159

// toPrecision 转化成一个十进制的字符串,参数是数字的精度
Math.PI.toPrecision(3); // 3.14

// toString(16) 转化成一个字符串,参数是转换的进制,默认是10
Math.PI.toSting();
Object

object.hasOwnProperty 这个方法可以监测对象的属性,但是原型链中的同名属性不会检查;

var a = {isLoading: true};
a.hasOwnProperty('isLoading') => true

var b = Object.create(a);
// 创建一个新对象b,其中的__proto__ 就是a, b对象可以访问a对象原型链上的方法和属性
b.hasOwnProperty('isLoading') => false
console.log(b.isLoading) => true
RegExp 正则表达式

exec

匹配字符串的最强大最慢的方法。如果匹配正则表达式,就返回一个数组,数组不同的项就是分组捕获的文本。如果匹配失败,就会返回 null。如果全局检索g,查找不会从这个字符串的开始位置,而是从regexp.lastIndex 开始。如果进行另一次查询,需要将 regexp.lastIndex 重置为0。

// 将HTML文本分解成标签和文本
var text = '<html><body bgcolor=linen><p>' + '</p></body></html>';
var tags = /[^<>]+|<(\/?)([A-Za-z]+)([^<>*])>/g;
var a, i ;

while ((a = tags.exec(text))) {
  for (i = 0; i < a.length; i++) {
    console.log((i + a[i]).entityify());
  }
}

test

匹配正则表达式最简答的方法,如果正则表达式匹配字符串,返回的是布尔值,不能使用全局的g标识。

var b = /&.+/.test('Tom &amp; beans');
RegExp.method('test', function(string) {
  return this.exec(string) !== null;
});
string

字符串的方法是最常用的方法

string.charAt(position); // 返回字符串中某个位置的字符,如果位置超过长度或者是负数,那么返回一个空字符串
string.charCodeAt(pos); // return 字符码位 if(pos > string.length) return NaN
string.concat('a', 'b'); //通常直接加号链接字符串即可
string.indexOf('test', 10); // 从位置10开始检错,查询test字符串,返回第一个匹配字符的位置,找不到返回-1
string.lastIndexOF('test', 3); // 从末尾开始查找
string.match(regexp) //让字符串和一个正则表达式进行匹配,依据g标识来决定怎样进行分配。如果没有g, string.match(regexp) 和 regexp.exec(string) 结果相同。如果regexp具有g标识,那么返回一个匹配的数组。
string.replace('search', 'replace'); // 如果标明g,就是替换第一个匹配的字符。
string.search(regexp); // 类似于indexOf 传入的参数是正则表达式,返回第一个匹配字符的首字符位置(没有position参数)
var text = 'hello world "anynone" tall';
var position = text.search(/["']/);

string.slice(start, end); // 复制string的一部分构造一个新的字符串。如果是负数,就从后面开始;默认end是string.length.
string.split(seperator, limit); // 将字符串按照分隔符分割成片段,并创建一个字符串数组。limit表示分割片段的数量。分隔符可以使字符串或者是正则表达式。如果分隔符是空字符,返回单字符的数组。

var ip = '192.168.0.1';
var b = ip.slice('.'); // ['192', '168', '0', '1']

string.toLowerCase();
string.toUpperCase();
string.fromCharCode(char...); //根据数字编码创建一个字符串 

截取一个字符串的方法最好使用slice, 不要使用substring 因为后者无法使用负数

第九章 代码风格

JS 对代码格式要求不要,容错性很高,这样可能造成bug。多写一个大括号可能避免if的作用范围。写合适的注释并及时更新注释(代码会更新,注释也需要更新)。详细的代码风格可以参考《代码整洁之道》这本书。

第十章 优美的特性

函数是顶级对象

函数具有词法作用域的闭包(lambda)

基于原型继承的动态对象

对象是无类别的。一个对象可以通过普通的赋值增加一个新成员属性;一个对象可以从另一个对象继承成员属性。

对象字面量和数组字面量

便捷的创建对象和数组(来源于JSON)

附录A 毒瘤

全局变量

全局变量可能造成内存泄漏;大型程序中可能和另一个变量名冲突;可以被程序的任何部分在任何时间修改(降低了程序的可靠性)

定义全局变量的三种方法:在函数外部使用 var foo = value; 给全局对象window增加一个属性 window.foo = value; 使用未声明的变量(隐式全局变量,会造成很大的麻烦)

作用域

JS具有代码块,但是没有块级作用域。其他语言中,声明变量在第一次使用是,在JS中可以在每个函数开头部分声明变量。

自动插入分号

如果return后没有加分号,就会自动增加分号 return; 返回一个undefined

typeof

typeof(null) => Object 所以不能用这种方法检测null

可以使用 null === null (true) 检测null

对于正则表达式,大部分浏览器返回 object

parseInt

parseInt 会把字符串转化成整数,如果遇到非字符就会停止解析。parseInt('16 tons') => 16.

s如果遇到的字符串第一个是0,parseInt 会按照八进制转换,这可能造成错误。

parseInt('089') => 0

解决 parseInt('089', 10) => 89 按照十进制进行转化。

加法

确保加法的两个数都是整数。如果计算货币需要圆角分,需要先转化成分进行计算(之后再还原成元)。

NaN

计算错误会产生NaN。如果一个计算结果是NaN,那么可能输入项或者计算过程中产生了NaN。可以使用 isNaN 判断。

isNaN('oop'); //true
isNaN('0'); //false
伪数组

JS 中的数组实际上是对象。不需要考虑越界的问题,但是性能比C语言中的数组差很多。判断数据不能使用 typeof,需要判断 constructor.

if (my_value && typeof(my_value) === 'object' && my_value.constructor === Array) {
  console.log('parameter is an array');
}

函数参数构成伪数组 arguments 不是一个数据,是一个对象,具有 length 属性。

hasOwnProperty 是对象的一个方法,可以判断对象内部是否有一个键;但是这个方法可以被更改(object.hasOwnProperty = null;),此时就会出错。

附录B 糟粕

避免使用 ==

两个等号比较时,如果变量类型不同会强制转换,这些规则很复杂。所以尽量避免使用两个等号,最好使用三个等号。

'' == '0' //false
0 == '' // true
0 == '0' // true

避免使用 with 语句

避免使用 eval 语句

避免在函数参数中传递字符串

减少使用 continue

减少使用 switch_case 穿越(两种情况对应一个结果,不写 return 语句。)这样在 JSLint监测效果不好

尽量使用代码块(if while) 避免因为换行程序错误。因为JS中空格不严格限制。

附录C JSlint

使用 JSLint 可以减少错误发生(代码行末加入分号等,避免语句错误)

使用 ++ — 避免在前面写空格,因为可能造成 + + 这样的错误

在选择结构中,JSLint不希望出现赋值语句。因为可能是比较语句(a == b) 少写了一个等号造成的赋值语句。

可以避免很多语法问题造成的潜在的bug。

这一点对于自己很有用。在自己的项目中最好可以这个库。

附录 JSON

JSON 主要用于不同语言数据交换。

JSON 对象可以被插入任何数据类型的值,对象可以无限层次的嵌套。但是最好的办法是扁平的数据。其他语言具有映射成JSON对象的数据类型。

可以使用 eval 函数把文本字符串转化成一个有用的数据结构,但是这种方法存在风险。

替代的方法是 JSON.parse 如果内部包含一个危险的数据,就会抛出一个错误。

本书笔记完


Last update: November 9, 2024