javascript 语言精粹读书笔记
重读了一遍以前留下的读书笔记,发现也就一个柯里化这个概念记不清而已了。
而且书的内容真的老了,读起来恍如隔世,所以在下文里加了不少的 Ps.
------ 2020年3月更新
第一章 精华
Javascript建立在一些非常优秀的想法和少数非常糟糕的想法之上,
优秀的想法包括函数、弱类型、动态对象和富有表现力的对象字面量表示法。
那些糟糕的想法包括基于全局变量的编程模型。
第二章 语法
数字:
javascript只有一个数字类型,在内部被表示为64位的浮点数。Infinity表示所有大于1.79769313486231570e+308的值。
NaN表示不能产生正常结果的值,NaN不等于任何值,包括自己。可以使用ES5的函数isNaN(number)来检测NaN。
Ps. 到 ES 10,JS 已经有了第二种表示数字的类型,BigInt
语句:
下面的值判断时被当做假: false、 null、 undefined、 空字符串 ' ' 、数字0、 数字NaN。
Ps. 请直接参照 ES 设计标准中的 ToBoolean 函数的描述
第三章 对象
Javascript的简单数据类型包括数字、字符串、布尔值、null值和undefined值。其他的所有值都是对象。
数字、字符串、布尔值“貌似”对象,因为它们拥有方法,但它们是不可变的。
在Javascript中数组是对象,函数是对象、正则表达式是对象,当然,对象自然也是对象。
Ps. ES6 新加 Symbol类型,ES10 新加 BigInt 类型,所以简单数据类型现在有 7 种
参数传递-值传递和引用传递
对象通过引用来传递,它们永远不会被复制。基础类型通过值传递
function add(num){
num+=10;
return num;
}
num=10;
alert(add(num));
aelrt(num);
//输出20,10
对于这里的输出20,10,按照JS的官方解释就是在基本类型参数传递的时候,做了一件复制栈帧的拷贝动作,这样外部声明的变量num和函数参数的num,拥有完全相同的值,但拥有完全不同的参数地址,两者谁都不认识谁,在函数调用返回的时候弹出函数参数num栈帧。
所以改变函数参数num,对原有的外部变量没有一点影响。
function setName(obj){
obj.name="ted";
obj=new Object();
obj.name="marry";
}
var obj=new Object();
setName(obj);
alert(obj.name);
//输出ted
setName函数传入obj的地址,所以第2行做法,对第6行的obj有影响。但是第3行的做法使函数内的obj的地址改变成新的堆栈空间,详情请参见这篇文章
Ps. 值传递和引用传递老生常谈的东西罢了
枚举
对象枚举采用for in循环, 如果有必要过滤掉那些不想要的值。最常用的过滤器是hasOwnProperty方法, 或者使用typeof来排除函数。
var stooge = {
'first-name': 'Jerome',
'last-name': 'Howard'
}
for (var name in stooge) {
if (typeof stooge[name] !== 'function') {
console.log(name + ': ' + stooge[name]);
}
}
数组采用for循环, 这样可以以正确的顺序遍历,并且也不用担心枚举出原型链中的属性。
Ps. 我个人更喜欢用 Object.keys(obj).forEach(index => {}) 的方法来做对象的枚举
删除
delete运算符可以用来删除对象的属性。如果对象包含该属性,那么该属性就会被移除。他不会触及原型链中的任何对象。
删除对象的属性可能会让来自原型链中的属性透现出来
stooge.__proto__.nickname = 'Curly';
stooge.nickname = 'Moe';
stooge.nickname //'Moe'
delete stooge.nickname;
stooge.nickname //'Curly'
Ps. delete 慎用,若要用那也只是用来删除对象上的属性,别删其他的东西。并且严格和非严格模式下会有区别。参见 MND-delete 操作符
第四章 函数
调用
在Javascript中一共有4种调用模式:方法调用模式、函数调用模式、构造器调用模式、apply调用模式。这些调用模式在如何初始化关键参数this上存在差异。
方法调用模式:可以使用this访问自己所属的对象。
var myObject = {
value: 0,
increment: function(inc){
this.value += typeof inc === 'number' ? inc : 1;
}
};
myObject.increment();
console.log(myObject.value); // 1
myObject.increment(2);
console.log(myObject.value); // 3
函数调用模式:此模式的this被绑定到全局对象。
var someFn = function () {
return this === window; //true
}
构造器调用模式
而在函数前面带上一个new来调用,那么背地里将会创建一个连接到该函数的prototype成员的新对象。同时this会被绑定到那个新对象上。
var Quo = function (string) {
this.status = string;
}
Quo.prototype.get_status = function(){
return this.status;
}
var myQuo = new Quo('confused');
console.log(myQuo.get_status()); //'confused'
apply调用模式: 让我们构建一个参数数组传递给调用函数,允许我们选择this的值。
var statusObject = {
status: 'A-OK'
};
var status = Quo.prototype.get_status.apply(statusObject);
(call、apply、bind的区别参见这里)[http://blog.itpub.net/29592957/viewspace-1159067/]
var xw = {
name : "小王",
gender : "男",
age : 24,
say : function(school,grade) {
alert(this.name + " , " + this.gender + " ,今年" + this.age + " ,在" + school + "上" + grade);
}
}
var xh = {
name : "小红",
gender : "女",
age : 18
}
//对于call来说是这样的
xw.say.call(xh,"实验小学","六年级");
//而对于apply来说是这样的
xw.say.apply(xh,["实验小学","六年级郑州牛皮癣医院"]);
//看到区别了吗,call后面的参数与say方法中是一一对应的,而apply的第二个参数是一个数组,数组中的元素是和say方法中一一对应的,这就是两者最大的区别。
//那么bind怎么传参呢?它可以像call那样传参。
xw.say.bind(xh,"实验小学","六年级")();
//但是由于bind返回的仍然是一个函数,所以我们还可以在调用的时候再进行传参。
xw.say.bind(xh)("实验小学","六年级");
Ps. 2020年了,大家 react 写的这么多,bind 之类的函数怕无人不晓了吧
参数
函数被调用的时候,会得到一个免费配送的参数,就是arguments数组。一个语言设计上的错误,arguments并不是一个真正的数组,它只是一个“类似数组”的对象。虽然它有length属性,但是并没有任何数组的办法。要使用数组的方法需要用call函数。
var sum = function () {
var i, sum = 0;
for (i = 0; i < arguments.length; i += 1) {
sum += arguments[i];
}
return sum;
};
sum(4, 8, 15, 16, 23, 42); //108
返回
一个函数总是会返回一个值。如果没有指定返回值,则返回undefined。
如果函数调用时在前面加了new前缀,且返回值不是一个对象,则返回this(该新对象)
异常
如果处理手段取决于异常类型,那么异常处理器必须检查异常对象的name属性来确定异常的类型。
var add = function(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw {
name: 'TypeError',
message: 'add needs numbers'
};
return a + b;
}
}
//构造一个try_it函数,以不正确的方式调用之前的add函数
var try_it = function(){
try{
add('seven');
} catch(e) {
console.log(e.name + ': ' + e.message);
}
}
try_it();
作用域
javascript代码并不支持块级作用域,只有函数作用域。很多现代语言都推荐尽可能延迟声明变量。而用在javascript上的话却会成为糟糕的建议,
因为它缺少块级作用域。最好的做法是在函数体的顶部声明函数中可能用到的所有变量。
Ps. 毒瘤,不过 ES6 已经推出了 let 声明变量,所以书上所说的这个情况已经不是问题了。
闭包
闭包就是函数中的内部函数,可以引用定义在其外部作用于的变量。闭包比创建他们的函数有更长的生命周期,并且闭包内部存储的是其外部变量的引用。
function box(){
var val = undefined;
return {
set: function(newVal) { val = newVal; },
get: function() { return val; },
type: function() { return typeof val; }
};
}
var b = box();
b.type(); //"undefined"
b.set(98.6);
b.get(); //98.6
b.type(); //"number"
理解绑定与赋值的区别。
闭包通过引用而不是值捕获它们的外部变量。
使用立即调用的函数表达式(IIFE)来创建局部作用域。
先看一段BUG程序:
function wrapElements(a) {
var result = [];
for(var i = 0, n = a.length; i < n; i++) {
result[i] = function() { return a[i]; };
}
return result;
}
var wrapped = wrapElements([10, 20, 30, 40, 50]);
var f = wrapped[0];
f(); //undefined
这段代码非常具有欺骗性, 程序员可能希望这段程序输出10, 但是输出的是undefined值。
这是由于function() { return a[i]; };
这段闭包里的i储存的是外部i变量的地址, 每当for循环继续,产生新的闭包时,i值都会被更新。所以取得的i值永远都是for循环结束后的i值。
改正的话应该去创建一个立即调用的函数
function wrapElements(a) {
var result = [];
for(var i = 0, n = a.length; i < n; i++){
(function(j){
result[i] = function() {return a[j];};
})(i);
}
return result;
}
Ps. 老套路了,但冷不丁在你写循环去做异步操作时还是会阴到你。
模块
利用函数和闭包构造模块,模块是一个可以提供接口却隐藏状态的函数或者对象。通过模块,我们几乎可以完全摒弃全局变量使用。
级联
如果让方法返回this而不是undefined,就可以启用级联(链式编程)
getElement('myBoxDiv')
.move(350, 150)
.width(100)
.height(100)
.color('red');
函数柯里化
函数也是值,我们可以用又去的方式操作函数值,柯里化允许我们把函数与传递给它的参数结合,产生一个新的函数。
兼容现代浏览器以及IE浏览器的事件添加方法:
var addEvent = (function(){
if (window.addEventListener) {
return function(el, sType, fn, capture) {
el.addEventListener(sType, function(e) {
fn.call(el, e);
}, (capture));
};
} else if (window.attachEvent) {
return function(el, sType, fn, capture) {
el.attachEvent("on" + sType, function(e) {
fn.call(el, e);
});
};
}
})();
这么做就只需要判定一次, 不用每次调用addEvent都得判断ie6 7 8的代码。这就是典型的柯里化
初始addEvent的执行其实值实现了部分的应用(只有一次的if...else if...判定),而剩余的参数应用都是其返回函数实现的,典型的柯里化。
Ps. 看了这么多次的柯里化,虽然知道是这么回事,但总转眼就忘了他的名字。
第五章 继承
伪类
//定义一个构造器,并扩充它的原型
var Mammal = function (name) {
this.name = name;
};
Mammal.prototype.get_name = function(){
return this.name;
};
Mammal.prototype.says = function () {
return this.saying || '';
};
//构造伪类去继承Mammal,替换它的prototype为一个Mammal的实例来实现
var Cat = function(name) {
this.name = name;
this.saying = 'meow';
};
Cat.prototype = new Mammal();
//扩充新原型对象
Cat.prototype.get_name = function () {
return this.says() + ' ' + this.name + ' ' + this.says();
};
//创建实例
var myCat = new Cat('Henrietta');
myCat.says(); //'meow'
myCat.purr(5); //'r-r-r-r-r'
myCat.get_name(); //'meow Henrietta meow'
伪类的缺点很多, 比如说没有私有环境,所有属性都是公开的;无法访问父类的方法。更糟糕的是,创建实例的时候忘记加上new前缀,则构造函数里面的this将会被绑定到window对象上,从而破坏了全局变量环境。
Ps. ES6 直接写 Class 省事儿吧,但怕总有面试官会问起怎么做的,那还是直接参照我的另外一篇 ES5 继承最优解
函数化
继承模式的一个弱点就是没法保护隐私,对象的属性都是可见的,可以使用应用模块模式来解决。
var mammal = function (spec) {
var that = {};
that.get_name = function() {
return spec.name;
};
that.says = function() {
return spec.saying || '';
};
return that;
};
var myMammal = mammal({name: Herb});
var cat = function(spec){
spec.saying = spec.saying || 'meow';
var that = mammal(spec);
that.get_name = function(){
return that.says() + '' + spec.name + ' ' + that.says();
};
return that;
};
var myCat = cat({name: 'Henrietta'});
//定义一个处理父类的方法的方法
Function.prototype.method=function(name, func){
this.prototype[name]=func;
return this;
}
Object.method('superior', function(name){
var that = this,
method = that[name];
return function(){
return method.apply(that, arguments);
};
});
var coolcat = function(spec){
var that = cat(spec),
super_get_name = that.superior('get_name');
that.get_name = function(n){
//调用父类方法
return 'like ' + super_get_name() + ' baby';
};
return that;
};
var myCoolcat = coolcat({name: 'Bix'});
var name = myCoolCat.get_name(); // 'like meow Bix meow baby'
比起伪类模式来说, 使用函数话模式能得到更好的封装和信息隐藏,以及访问父类的能力
第六章 数组
容易混淆的地方
js对于数组和对象的区别是混乱的。typeof运算符报告数组的类型是 'object' 这没有任何意义。
我们可以通过自定义的is_array函数来弥补这个缺憾
var is_array = function(value) {
return value &&
typeof value === 'object' &&
value.constructor === Array;
};
数遗憾的是,它在识别从不同的窗口(window)或帧(frame)里构造的数组时会失败。有一个更好地办法去判断
var is_array = function (value) {
return Object.prototype.toString.call(value) === '[object Array]';
}
Ps. 类型识别也请直接参照我另外一篇
第七章 正则表达式
正则因子
除了下列控制字符和特殊字符除外, 所有的字符都会被按照字面处理
\ / [ ] ( ) { } ? + | . ^ $ -
如果你需要上面列出的字符按字面去匹配,那么需要用一个\
前缀来转义
如一个未被转义的 . 会匹配除了 \n 以外的任何字符
正则表达式转义符
- \f 是换页符、\n是换行符、\r是回车符、\t制表符
- \d 等同于[0-9], 它匹配一个数字。 \D 相反,匹配非数字,[^0-9]
- \s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]。 \S 相反
- \w 匹配包括下划线的任何单词字符。等价于“[A-Za-z0-9_]”。 \W 相反
- \b 匹配一个单词边界,也就是指单词和空格间的位置。例如,“er\b”可以匹配“never”中的“er”,但不能匹配“verb”中的“er”。
Ps. 正则请直接参照速查表
第八章 方法
Ps. 太费事了,现在直接参见 MDN 就完事
附录A,JS毒瘤
- 全局变量: JS代码基于全局变量编程,全局变量是魔鬼。大型程序中全局变量将会非常难以维护,并且隐式的全局变量会让查找bug变得很困难。
- 作用域: JS代码缺少块级作用域,只有函数作用域。
- null不是对象。解释: 虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object 。
- 自动插入分号:自动插入的分号可能会掩盖更为严重的错误。下列程序出错自动插入的分号让它变成了返回undefined,没有任何警告, 解决的办法就是花括号写在return语句后面
//错误实例
return
{
status: true;
};
//正确做法
return {
status: true;
};
- 保留字: 类似class byte这样的单词在js里被保留, 不能用来做命名变量或者参数,而这些单词大多数并没有再语言中使用。
- 编码问题: js设计之初,Unicode预期65536个字符,js字符是16位的, 足以覆盖原有的65536,现在Unicode慢慢增长为100W个字符。
剩下百万字符中每一个都可以用一对字符来表示。Unicode把一对字符视为一个单一的字符,而js认为一对字符是两个不同的字符. - typeof : typeof操作符并不能辨别出null和对象。typeof对正则表达式的类型识别上,各浏览器实现不太一致,IE/FF/opera都返回'object', Safari 3.x版本返回'function', 5.x版本'object'
typeof操作符也不能辨别出数组和对象。 - + : 连接字符串,也可以执行加法,这种复杂的行为是bug的常见来源。如果打算做加法运算,请确保两个运算符都是整数。
- 浮点数 : js中0.1+0.2并不等于0.3,但浮点数中整数的运算是精确的,所以最好能将小数转化为整数运算后,再转化为小数。
- 伪数组: js没有真正的数组,js的数组确实很好用,不必设置维度而且永远不会产生越界错误,但是性能和真正的数组比差太多。typeof操作符也检查不出是数组和对象的分别。需要借助其他函数
- 假值:0、NaN、''(空字符串)、false、null、undefined。 在js的逻辑判断中都是假值。 如果使用 == 判断的时候很容易得到意想不到的结果
附录B,JS糟粕
- =和: 运算会强制转化运算数的数据类型,转化规则复杂诡异,建议判断时都要用=符号.
- with语句:结果会不可预料,而且严重影响js处理速度,避免使用。
- eval:eval形式代码更难读,使性能显著下降,因为它需要运行编译,会使JSlint之类的检测工具检测能力大打折扣;还减弱了程序的安全性。
Function构造器也是eval另一种形式,也要避免使用。同理serTimeout和setInterval函数能接受字符串参数和函数参数,使用字符串参数的时候,也会像eval
那样去处理,避免使用其字符串参数形式。
- continue : continue语句跳到循环顶部。但是使性能下降,避免使用。
- switch穿越:case条件向下穿越到另一个case条件时,就是switch穿越。这是一个常见的错误来源,并且它很难通过查看代码发现错误。避免使用穿越。
- 位运算符:js里并没有整数类型,使用位运算符要先转化要整数,接着才能执行运算,所以执行速度很慢。js比较少来执行位操作符,使用位操作符也使得bug更容易被隐藏。
- function 语句对比 function 表达式:推荐使用 function 表达式,因为它能明确表示 foo 是一个包含函数值的变量。理解好这门语言,理解函数就是数值很重要。
var foo = function () {};
function 语句解析时会发生作用域提升,所以不要在 if 里在去使用 function 语句
- 类型的包装对象:例如 new Boolean(false); 返回的是一个对象,typeof 操作符判断时object类型,会造成一些困扰。所以避免使用 new Boolean、new Number、 new String。
此外也避免使用 new Object、new Array 写法,请用 {} 和 [] 来代替。
- new 操作符:如果漏了new操作符,构造函数被调用时 this 被绑到全局对象,后果十分严重。
- void:很多语言中,void是一种类型,表示没有值。而在js里,void是一个运算符,它接受一个运算数并返回undefined。这并没有什么用,应该避免它。额,虽然这么说,但是我还是会喜欢用 void 0 来获取 undefined 值。
附录E,JSON
json对象转化为字符串格式相互转化,见下面的代码
var str = JSON.stringify({what:'sss'}) //"{"what":"sss"}"
JSON.parse(str) // Object {what: "sss"}
一个JSON解析器 ,见另一篇文章JSON解析器