• 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. 类型识别也请直接参照我另外一篇

    第七章 正则表达式

    正则因子

    除了下列控制字符和特殊字符除外, 所有的字符都会被按照字面处理

     / [ ] ( ) { } ? + | . ^ $ -
    

    如果你需要上面列出的字符按字面去匹配,那么需要用一个前缀来转义

    如一个未被转义的 . 会匹配除了 以外的任何字符

    正则表达式转义符

    • f 是换页符、 是换行符、 是回车符、 制表符
    • d 等同于[0-9], 它匹配一个数字。 D 相反,匹配非数字,[^0-9]
    • s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于[ f v]。 S 相反
    • w 匹配包括下划线的任何单词字符。等价于“[A-Za-z0-9_]”。 W 相反
    •  匹配一个单词边界,也就是指单词和空格间的位置。例如,“er”可以匹配“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解析器

  • 相关阅读:
    hadoop集群搭建
    javamail
    编码之后的字符串和数组长度解惑
    后台架构剖析
    搜索引擎选择: Elasticsearch与Solr
    WHRER条件里的数据类型必须和字段数据类型一致
    Phantomjs
    倒排索引
    Gremlin--一种支持对图表操作的语言
    Android Intent 用法全面总结
  • 原文地址:https://www.cnblogs.com/everlose/p/12501471.html
Copyright © 2020-2023  润新知