《你不知道的JavaScript(中)》——读书笔记

189 阅读22分钟

第一部分 类型和语法

第一章 类型

  • 定义:类型是值的内部特征,定义了值的行为,使其区别于其他值。
  • 分类
    • 基本类型:null | undefined | number | string | boolean | symbol
    • 复杂类型:object
  • typeof
    • 执行后有三种结果:object(object/null)、function(function)、自身(其他);
    • JavaScript中的变量没有类型,只有值才有,对变量进行的typeof操作,本质上是对变量持有的值的操作;
    • 对于一个变量a,当a未声明或者声明后未赋值,执行typeof均返回undefined,可避免直接引用未声明变量而引发的错误。

第二章 值

数组

  • 数组也是对象可设属性;
  • 可使用delete操作符,但length不会发生改变;
  • 如果字符串键值可被强转为十进制,则当做数字所引来使用,a['13']=42;//则 a.length=14, a[0], ... a[12]均为undefined
  • 转换类数组:Array.from()

字符串

  • 字符串是类数组,可以通过call的形式借用数组的map、join等方法(这些方法均为返回一个新对象)
  • 字符串是不可变的,字符串的成员函数不会改变其原始值,总是返回一个新字符串。

数字

  • JavaScript没有真正意义上的整数
  • 小数点前或后只有0可省略(0.43->.43;43.0->43.)
  • 某人情况下数字均以十进制显示,特别大或特别小点的数字使用指数格式显
  • toFixed():指定小数部分的显示位数,输出为字符串;43.toFixed(3)会报错,第一个点会被当做是数字部分即(43.)(toFixed(3)),(43).toFixed(3)、43..toFixed(3)、43 .toFixed(3)均可
  • toPrecision();//指定有效数位的显示位数
  • 数字字面量还可以使用二进制、八进制、十六进制表示。
  • 由于二进制浮点数并不准确,当十进制小数的二进制表示的有线数字超过52位时,会出现误差,所以判断两个小数是否相等不要使用相等而是判断两数之差是否在误差范围(Math.pow(2,-52)、Number.EPSILON)
    0.1+0.1===0.2 -> true
    0.1+0.2===0.3 -> false
    0.1+1.2===1.3 -> true
    0.1+2.2===2.3 -> false
    
  • 安全整数范围:±(2^53-1),即Number.MAX_SAFE_INTEGER、Number.MIN_SAFE_INTEGER;检测是否为整数:Number.isInteger();检测是否为安全整数:Number.isSafeInteger()
  • 数位操作的整数范围:±(2^31-1)

特殊数值

  • null:为特殊关键字,非标识符,不可作为变量使用和赋值
  • undefined:是标识符,可以作为变量使用和赋值,通过void 0进行安全获得
  • NaN:为数字类型;js中唯一一个不等于自身的值NaN!=NaN;判别isNaN();
  • 无穷数:±Infinity:除0可得、为Na
  • 存在-0
    -0 + "" -> "0";
    + "-0" -> -0;
    -0===0 -> true;
    Object.is(-0,0) -> false//Object.is(..)判断两值是否绝对相等 
    

值和引用

  • js中的引用指向的是值,所以一个变量不可能成为指向另一个变量的引
  • 简单值:总是通过值复制方式来复制和传参;复合值:总是通过引用复制方式来赋值和传参(当参数被重新赋值之后,其操作不再改变原参数)

第三章 原生函数

  • String()、Number()、Boolean()、Array()、Object()、Function()、RegExp()、Date()、Error()、Symbol()

    var a = new String('a');
    typeof a -> "object";
    a instanceOf String -> true;
    Object.prototype.toString.call(a) -> "object String";
    
  • Object.prototype.toString():查看对象内部[[Class]];Object.prototype.toString.call():对所有变量可用

    • string、number、boolean会被自动封装为对应的对象形式
    • null、undefind自身具备[[Class]]属性,分别为:[object Null]、[object Undefined]
  • js在必要时,会自动对基本类型值进行封装和拆封

  • var a = new Boolean(false);
    if(!a){
        //此处代码执行不到,因为a为对象
    }
    
  • Array()调用时可以不带new;当Array()内参数为单个数字时(Array(5)),表示指定长度生成空数组;

  • 稀疏数组:至少包含一个“空单元”(单元值为undefined,delete b[4]获指定length得到)

  • 常量方式的正则效率更高

  • Date.now() === (new Date()).getTime();
    
  • symbol:符号:具有唯一性(并非绝对)的特殊值;代码或控制台中都无法查看和访问它的值;构造时不可带new;

  • String.prototype.XYZ可以简写为String#XYZ;Function.prototype为空函数;RegExp.prototype为空正则表达式;Array.prototype为空数组;

第四章 强制类型转换

  • 值类型转换

    • 将值从一种类型转为另一种类型
    • 分为显式和隐式,分别对应类型转换和强制类型转换
    • 总是返回标量基本类型值
    • 显式发生在静态类型语言的编译阶段,隐式发生在动态语言的运行
  • 抽象值操作

    • ToString
      • null -> "null"
      • undefined -> "undefined"
      • 数字 -> 遵循通用规则(1.07e21 -> "1.07e21")
      • true -> "true"
      • [1,2,3] -> "1,2,3" [null] -> ""
      • 普通对象 -> 自定义toString():返回该方法的返回值,非自定义:返回内部属性[[Class]]值-
    • JSON.stringify()
      • "a" -> ""a""
      • 只转换安全的JSON值,对于不安全值
        • undefined -> 忽略
        • function -> 忽略
        • symbol -> 忽略
        • 循环引用 -> 报错
      • 若被转换对象包含toJSON方法,则使用该方法的返回值进行操作;
      • 可选参数replacer:为字符串数组时表示要处理的属性,为函数时,每个属性调用一次
      • 可选参数space:指定缩进
    • ToNumber
      • true -> 1 , false -> 0
      • undefined -> NaN
      • null -> 0
      • string -> 成功/NaN("" -> 0)
      • 对象 -> valueOf():有此方法,且返回基本类型值 -> 转换,否则 -> toString():有此方法,且返回基本类型值 -> 转换,否则 -> 报错:TypeError
    • ToBolean
      • undefined -> false
      • null -> false
      • "" -> false, 其余为true
      • 0/NaN -> false, 其余为true
      • 对象 -> true, new Boolean(false)、new String("")、new Number(0)均为true,部分浏览器可能存在假值对象
  • 显示强制类型转换

    • 常规 Number()、 String()
    • +:字符串转数字+ "3" -> 3;时间转为时间戳
    • ~:先强转为32位数字,再执行位操作;~x === -(x+1); ~~x:截除小数部分
    • parseInt:解析数字字符串:从左到右遇到非数字停止
      • parseInt("42px") -> 42 不同与 Number("42px") -> NaN
      • parseInt(0.000008) -> parseInt("0.000008") -> 0 //es5之后,默认转换为十进制
      • parseInt(0.0000008) -> parseInt("8e-7") -> 8
      • parseInt(false,16) -> parseInt("false",16) -> parseInt("fa",16) -> 250
      • parseInt(parseInt,16) -> parseInt("function...",16) -> parseInt("f",16) -> 15
    • !!:将任意值转为布尔值
  • 隐式强制类型转换

    • 对于a + b,若其中一个操作数为字符串(或者可以调用valueOf/toString),则执行字符串拼接,否则执行数字加法;a-b/(a/b)/a*b,将a转为数字;

    • []+{} -> "[object Object]";{}+[] -> 0第二个{}会被当做空代码块

    • 发生布尔因式转换的情景

      • if(...)
      • for(...;...;...)中的判断
      • while(...)和do...while(...)
      • ...?...:...三目运算
      • || 和 &&的左边操作数
    • || 与 && :返回的不一定是布尔值,而是其左右两边的一个,返回的一定是a或b,而不是条件判断的结果

      • a || b : a为true或可强转为true,则返回a,否则返回b
      • a && b : a为true或可强转为true, 则返回b,否则返回a
    • Symbol,转为string要使用String(),不可使用+ "",会报错,不可转为数字

  • 宽松相等和严格相等

    • ==允许两个操作数在相等比较中进行强制类型转换,而===不允许
    • ==时的强转规则
      • 字符串与数字 -> 将字符串转为数字
      • 布尔值与其他类型 -> 将布尔值转为数字
      • null与undefined相等,其余与其均不相等
      • 对象与非对象 -> 将对象转为基本类型(valueOf/toString)再比较
      • 对象与对象 -> 直接比较(==与===结果相同)
      • NaN == NaN -> false
      • [] == ![] -> true
      • 0 == "\n" -> true
  • 抽象关系比较

    • 对于b>a和b>=a都会被处理为a<b,a<=b
    • 比较规则:先调用valueof/toString等方法,若出现非字符串,则双方都转为数字进行比较,若都是字符串,则按字母顺序比较

第五章 语法

语句和表达式

  • 语句都有一个结果值,代码块为最后一个语句的结果值
  • delete obj.a 成功为true,失败为false
  • 带标签的循环,用于goto,back等跳转
  • js中无else if语句,而是else {if()..}的简写

运算符优先级

  • 优先级:与>或>三目>等号>逗号 --> (&&) > (||) > (... ? ... : ...) > (=) > (,)

  • =与?:为右关联,即右部分先做计算;&&与||为左关联

自动分号插入

  • js解释器发现代码行可能因缺失分号而致错,则会自动添加。只有在代码末尾与换行符之间除空格和注释之外没有别的内容时,才会添加。

错误

  • 运行时错误:TypeError、ReferenceError、SyntaxError
  • 编译时错误:早期错误:执行之前我发用try catch捕获,导致解析编译失败(语法错误)
  • 暂时性死区:TDZ:由于代码中的变量还没有初始化而不能被引用的情况

函数参数

  • 参数被省略或被赋予undefined效果一样,但argument参数不同(es6之后被弃用)

try..finally

  • finally代码总是会在try..catch之后执行。即使try/catch中包含return也不例外
  • finally中抛出异常,则函数在此处终止,try/catch的返回追也会被丢弃
  • finally的return值会覆盖try/catch中的return值

switch

  • switch中的条件判断为===而不是==
  • default为可选项,并非必不可少

附录A

混合环境

A1 AnnextB(ECMAScript)

  • JavaScript的官方名为ECMAScript

A2 宿主对象

A3 全局DOm变量

  • 声明一个全局变量时,还会在global对象中创建一个同名属性
  • 创建带有id属性的DOM时也会创建同名的全局变量

A4 原生原型

  • 最佳实践:不要扩展原生原型
  • polyfill能有效地为不符合最新规范的老板本浏览器填补缺失的功能

A5 script标签

  • 全局变量作用域提升机制在多个script的代码中不适用
  • script内联代码会被当作结构标签来处理
  • 内联代码的script标签无charset属性,使用所在页面文件的字符集,而外联的则根据charset属性

A6 保留字

  • 分类
    • 关键字:function、switch
    • 预留关键字:enum、class、extend、interface
    • null常量
    • true/false常量
  • ES5之前,保留字不可用来作为对象常量中的属性或键值,ES6之后可以

A7 现实中的限制

  • 字符串常量允许的最大字符数
  • 可以作为参数传递到函数中的数据的大小(栈大小)
  • 函数声明中参数的个数
  • 未经优化调用栈(如递归)的最大层数,即函数调用链的最大长度
  • 变量名的最大长度

第二部分 异步和性能

第一章 现在与将来

分块的程序

  • console方法并不是JavaScript正式的一部分,而是宿主环境添加到js中的,所以不同的浏览器实现不同,会出现功能异常(调试使用debugger)

事件循环

  • ES6之后,才真正内建有直接的异步概念,以前只是js引擎在需要的时候在给定的任意时刻执行程序中单个代码块
  • 事件循环:
    • 宿主环境都提供了一种机制来处理程序中多个块的执行,执行时调用js引擎
    • js引擎本身并没有事件概念,只按需执行其中任意代码片段
    • "事件"调度总是由包含它的宿主环境进行的(ES6之后由引擎来做)
    • 循环的每一轮称为一个tick,每个tick中如果队列中有等待事件,则会依次执行
  • setTimeOut():精度不高:只是设置一个定时器,定时时间到时,宿主环境将回调函数放入事件循环中,则在未来的某个时刻的tick中执行

并行线程

  • 并行:是关于能够同时发生的事(异步是关于与现在和将来的问题)
  • 进程和线程:独立运行且可能同时进行,多个线程共享单个进程的内存
  • 事件循环:把自身的工作分成一个个任务并顺序执行,不允许对共享内存的并行访问和修改,通过分立线程中彼此合作的循环达到并行
  • 完整运行:js为单线程,每个执行块内的代码具有原子性(一旦执行就会执行完毕,不会被中断)
  • 竞态条件:函数执行顺序的不确定性

并发

  • 两个或多个进程同时执行就出现了并发(可以看作进程级的,而不是运算级的)
  • 单线程事件循环是并发的一种形式
  • 进程:
    • 相互不影响:不确定性可接受
    • 有交互
      • 门:gate:二者都完成
      • 闩:latch:胜者为王
      • 并发协作:将一个长期进行的"进程",分割成多个步骤或任务

任务

  • 任务队列:
    • 建立在事件循环队列之上,挂在事件循环队列的每一个tick之后的一个队列
    • 在事件循环的每个tick中,出现的异步动作不会添加到事件循环中。而会在当前tick的任务队列末尾添加一个任务
    • 可能会引发一个任务队列,从而永远到达不了下一个事件循环
    • promise异步特性是基于任务的

总结

  • js程序总是至少分为两个块,第一块现在执行,第二块将来执行以响应某个事件。尽管程序是一块一块执行的,但所有这些块共享对程序作用域和状态的访问,所以对状态的修改都是在之前积累的修改上进行的
  • 一旦有事件要运行,事件循环就会运行,直到队列清空。事件循环的每一轮称为一个tick。用户交互、IO和定时器会向事件队列中加入事件。任意时刻一次只能从队列中处理一个事件,执行时间时,可能会引发后续事件。
  • 并发是指两个或多个事件链随时间发展交替执行,以至于从更高层看,就像是同时在运行。

第二章 回调

  • 回调是这门语言的最基础的异步形式
  • 回调函数包裹封装了程序的延续
  • 回调的缺陷
    • 嵌套缩进
    • 要为所有情况事先做好方案,代码非常复杂
    • 控制发生了转移(对函数的控制),控制反转,导致信任链的断裂
  • 回调优化
    • 分离回调:会掉的两个参数,一个用于成功通知,一个用于出错通知
    • error-first风格/node风格:第一个为错误参数,第二个为成功参数

第三章 Promise

什么是Promise

  • Promise是一种封装和组合未来值的易于复制的机制,一种在异步任务中作为两个或更多步骤的流程控制机制
  • 决议结果
    • 完成值:总是编程给出的
    • 拒绝值:拒绝原因:程序逻辑直接设置或者运行异常得出的值
  • then:接受两个参数,第一个用于完成,第二个用于拒绝
  • 一旦Promise决议,它就永远保持在这个状态,为不变值,可根据需求多次查看
  • new Promise(fn):fn为立即执行函数,有两个参数,一个决议完成resolve,一个决议拒绝reject

具有then方法的鸭子类型

  • 鸭子类型:一种类型检查的方法,根据一个值的形态,具有哪些属性,对这个值的类型做出一些假定
  • 任何具有then()方法的对象,就是与Promise一致的thenable

Promsie 信任问题

  • 过早调用:对一个Promise调用then()时,即使promise已决议,提供给then()的回调也总是异步调用
  • 调用过晚:一个promise决议后,这个promise上所有通过then()注册的回调都会在下一个异步时机点依次被立即调用(调用顺序无法可靠预测)
  • 回调未调用:没有任何东西能阻止promise向外发布决议,但promise可能会永久不被决议(所以在制造promise时要防止这种情况发生)
  • 调用次数过少:由于promsie只会调用一次,所以和未调用一致
  • 调用次数过多:如果出现多次调用resolve或reject,则只会接受第一次决议,之后的都会忽略
  • 未能传递参数/环境值:如果未显示决议,则决议值为undefined,resolve和reject只可传递出一个参数,多余的会忽略(通过将多个值封装成一个来传递)
  • 吞掉错误或异常:如果promise的创建过程中或在查看其他决议结果过程中任何时间点上出现了一个js异常错误,则被捕获,并决议为拒绝且为异步,若在then()中出现错误,由于promise总是返回一个promise,所以会在返回值的决议中决议为拒绝,而不是在then的同级catch中进行捕获。
  • 不可信promise:
    • 如果向promise.resolve()传递一个非Promise非thenable的立即值,就会得到一个用这个值填充的promise
    • 如果向promise.resolve()传递一个正正的promise,就会返回这个promise
    • 如果向promise.resolve()传递一个个非Promise的thenable值,前者就会试图展开这个值,而且展开过程将持续到提取一个具体的非类promise最终值

链式流

  • 可以将多个promise连接到一起表示一系列异步步骤;每次对promise调用then都会创建并返回一个新的promise;不管从then()调用的完成回调返回值是什么它都会被自动设置为被连接promise的完成
  • promise链不仅是一个表达异步序列的流程控制还是一个从一个步骤到下一个步骤传递消息的消息通道
    • 如果promise链的某个步骤出错,会在下一个promise中catch,其catch处理后的结果会传给下下个,结果可能是resolve则回调下一个then,也可能是reject则回调下一个catch
    • promise链如果某个then没有适当有效的函数作为完成处理函数的参数,就会将接收到的任何值默认传递到下一步
  • resolve中可能会返回一个拒绝状态(处理的结果为拒绝或是出错)

错误处理

  • try...catch只能处理同步错误,无法用于异步
  • promise的错误处理为分离回调风格,一个用于完成,一个用于拒绝
  • promise链的一个最佳实践就是最后总以一个catch结束(但catch中仍会出错)
    • 注册"全局未处理拒绝"处理函数,如果promise被拒绝而定时器出发之前都没有错误处理函数被注册,则为全局未处理拒绝
    • 浏览器可以跟踪并了解所有对象被丢弃以及被垃圾回收的时机
    • promise改进:如果一个promise被拒绝,向开发者终端报告而不是现在的沉默,要么被处理要么被报告

Promise模式

  • Promise.all(p):门:一个失败全部失败,当p为空数组时,立即决议
  • Promise.race(p):闩:第一个成功则成功,第一个失败则失败,当p为空数组时,永远不会决议
  • none():所有都被拒绝
  • any():只需完成一个
  • first():只要第一个完成,则忽略后续任何(无论是完成还是拒绝)
  • last():最后一个胜出
  • map():并发迭代

Promise API 概述

  • 构造器: new Proomise(fn)
    • fn为同步,立即调用
    • fn有两个参数,resolve:完成或拒绝,reject:拒绝
    • 若resolve(p)中p为真正Promise或thenable值,则p会被递归展开,并且将取其最终决议值或状态
  • Promsie.resolve(),Promise.reject():快捷方式
  • then()和catch()
    • 每个promise实例都有这两个方法
    • promise决议后会立即调用其一(不会都调),总是异步调用
    • then(p1,p2):
      • p1为完成时回调,p2为拒绝回调
      • p1或p2被忽略或传递非函数值,则会启用默认回调,默认回调成功:只传递消息,拒绝:重新抛出错误
    • catch():只有一个拒绝回调,等价于then(null,p2)
  • Promse.all(),Promise.race()

Promise 局限性

  • 顺序错误处理:链中其中一步做了错误处理,之后就丢失了
  • 单一值:只有一个完成值或一个拒绝理由,多个值需封装为一个对象
  • 单决议:只能被决议一次,只能触发一次
  • 惯性:为原来的ajax提供promise封装函数
  • 无法取消
  • 性能:比回调略低

第四章 生成器

打破完整运行

  • 生成器:一个新的函数类型,不符合一个函数一旦开始执行就会进行到结束
    • 定义风格:三种均可
      • function* foo(){...}
      • function *foo(){...}
      • function*foo(){...}
    • 调用生成器只会生成一个迭代器,生成器内代码不会执行

生成器生产值

  • 迭代器:一个定义良好的接口,用于从一个生产者一步步得到一系列值
    • next:
      • 调用next会从生成器函数上一次停止位置(第一次为代码开始位置)开始执行,停在下一个yeild处或生成器函数结束处(return或代码最后)
      • 返回值为一个对象,包含value和done两个属性,分别为停顿处返回值和生成器函数是否运行完毕标识
      • 通过next传递参数,但第一个next中的参数会被丢弃
    • yeild:在生成器中即可作为参数也可作为返回值,构成双向消息传递
    • return:可以终止生成器
    • 同一个生成器的多个迭代器互相独立,通过共享全局变量可实现数据通信
  • for...of循环由迭代器构造,对于数组有默认的迭代器,在进行循环时,自动使用迭代器遍历数组,手工调用:it = a[Symbol.iterator] ()
  • iterable:可迭代:指包含可以在其值上迭代的迭代器对象,从iterable上提取迭代器it = a[Symbol.iterator] ()

异步迭代生成器

  • it.throw(err):向生成器内抛入错误
  • 生成器内代码可以通过try...catch捕获

生成器委托

  • 在一个生成器内调用另一个生成器,可以将内部迭代器委托于外部,使用*foo()模式调用
  • yeild委托的主要目的是代码组织,以达到与普通函数调用对称
  • yeild委托不要求必须转到另一个生成器,也可以转到一个非生成器的一般iterable,异常可以被委托
  • 委托分为消息委托、异步委托、递归委托

形实转换程序

  • js中的形实转换程序是指用于调用另一个函数的函数,无参数

ES6之前的生成器

  • 进行polyfill:闭包+状态控制

第五章 程序性能

web worker

  • web worker是宿主环境提供的功能(浏览器),js当前没有任何支持多线程执行的功能
  • var w1 = new Worker("地址"):专用worker
  • worker之间以及他们与主程序之间,不会共享任何作用域或资源,而是通过一个基本事件消息机制相互通信。
  • 专用worker和创建他们的主程序之间是一一对应关系。
  • worker也可以实例化自己的子worker,称为subworker(chrome不支持,firefox可以)
  • w1.terminate():终止
  • 在worker内部无法访问主程序的任何资源,但可以执行网络操作,设定定时器以及访问几个重要的全局变量的本地副本,包括navigation、location、json以及application cache
  • importScript("",""):向worker内加载而外的js,加载为同步,会阻塞至完成
  • webworker通常用于:
    • 处理密集型数学计算
    • 大数据排序
    • 数据处理(压缩,音频分析、图像处理)
    • 高流量网络通信
  • 数据传递:
    • 双向序列化、反序列化为字符串,两倍内存
    • 结构化克隆算法,两倍内存
    • Transferable对象,对象所有权的转移,单倍
  • 共享worker,shareworker
    • 使用port对象进行通信
    • 端口连接必须初始化
    • 需要额外处理connect事件
  • 模拟webworker:如果浏览器不支持,即使使用iframe也无法模拟,其本质都是运行在单个线程上,可以基于事件循环队列,通过异步实现伪worker

SIMD

  • 单指令多数据:一种数据并行方式,与webworker(任务并行)相对,不再是把程序逻辑分成并行的块,而是并行处理数据的多个位。

asm.js

  • asm.js标签是指JavaScript语言中可以高度优化的一个子集,通过小心避免某些难以优化的机制模式(垃圾回收,强类型转换),asm.js代码可以被JavaScript引擎识别并进行特别激进的底层优化

第六章 性能测试与调优

性能测试

  • endTime - startTime:测试不准确:
    • 浏览器精度有关
    • 测试环境有关
    • 获取时间戳有延迟
  • 单纯重复循环测试不准确
  • 任何有意义且可靠的性能测试都应该基于统计学上合理的实践

环境为王

  • 对性能测试来说,要注重检查测试环境

jsPerf.com:提供多种测试环境

写好测试

  • 想写好测试就需要认真分析和思考两个测试用例之间的区别,以及这些区别是否有意义

微性能

  • 不要沉迷于微性能,浏览器引擎处理代码的方式都不同

尾调用优化

  • 尾调用:一个出现在另一个函数结尾处的函数调用,这个函数调用结束后,就无其他操作(可能会返回值)
  • 栈帧:调用一个新的函数需要额外的一块预留内存来管理调用栈称为栈帧
  • 尾调用优化:进行尾调用时使用原来的内存而不是重新开辟(尤其是递归)
  • ES6要求引擎实现尾调用优化TCO

附录A

  • asynquence库

附录B

  • 高级异步模式