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解析器

猜你喜欢

转载自www.cnblogs.com/everlose/p/12501471.html