You-Dont-Know-JS-Notebook

上卷

作用域闭包

  • 闭包:当函数可以记住并访问所在的**词法作用域(不仅仅是显式的变量)**时,就产生了闭包,即使函数是在当前词法作用域之外执行的。
  • let声明可以用来劫持块作用域,如:
    for(var i=0;i<10;i++){
       let j=i;    //块作用于变量
        setTimeout(function(){
            console.log(j);
       }) 
   }

但若将let声明在for循环头部时,将会有一个特殊行为——每次迭代都会声明一次,并初始化为上一次迭代结束时的值。如:

    for(let i=0;i<10;i++){      //每次循环都会声明一个新的i,并且赋值为旧的i+1。
        setTimeout(function(){
            console.log(j);
       }) 
   }

作用域

  • 词法作用域:一套关于引擎如何寻找变量以及会在何处找到变量的规则
  • 动态作用域:作用域在运行时被动态确定的形式
  • 块作用域的两个小例子
    • with
    • catch
    • let创建块作用域变量

this

  • arguments.callee 可以指向匿名函数自身,书上说已被废弃,但在最新的浏览器中仍然有效。
  • this在任何情况下都不指向函数的词法作用域。作用域“对象”无法通过代码访问
  • 当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、调用方法、传入的参数等信息。this是记录的其中一个属性,会在函数执行过程中得到。
  • 如何寻找调用位置
    • 调用位置在当前正在执行函数的前一个调用中
    function baz(){
        // 当前调用栈是:baz
        // 因此当前调用位置是全局作用域
        
        console.log("baz");
        bar();// <-- bar的调用位置
    }
    function bar(){
        // 当前调用栈是baz->bar
        // 因此,当前调用位置在baz中
        console.log("bar");
        foo();  // <-- foo的调用位置
    }
    function foo(){
        // 当前调用栈是baz -> bar -> foo
        // 因此当前调用位置在bar中
        console.log("foo");
    }
  • 严格模式下,全局对象将无法使用默认绑定。
    function foo() {
        "use strict";
        console.log( this.a );
    }
    var a = 2;
    foo(); // TypeError: this is undefined
    仅当foo()定义中使用了严格模式,才会使得this无法绑定至全局作用域。
  • 作用域绑定
    • 默认绑定
    • 隐式绑定:当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。
    • 显示绑定:
      • 硬绑定:使用bind(),call()和apply()显示地强制绑定作用域
    • new绑定
    • 判断this(优先级从上到下)
      1. 函数是否在new 中调用(new 绑定)?如果是的话this 绑定的是新创建的对象。 var bar = new foo()
      2. 函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 指定的对象。 var bar = foo.call(obj2)
      3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。 var bar = obj1.foo()
      4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到 全局对象。 var bar = foo()
    • 绑定的例外:
      • call(null),apply(null) 实际应用默认绑定规则
    function foo() {
        console.log( this.a );
    }
    var a = 2;
    var o = { a: 3, foo: foo };
    var p = { a: 4 };
    o.foo(); // 3
    (p.foo = o.foo)(); // 2
    赋值语句p.foo=o.foo返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或o.foo()
    • 软绑定:给默认绑定指定一个全局对象和undefined以外的值,实现和硬绑定相同的效果,同时保留隐式绑定或显式绑定修改this的能力。 关于书上代码的解释
    • 箭头函数使用词法作用域,根据外层作用域来决定this,不使用上述四种标准规则。

对象

  • 六个主要类型
    • 简单基本类型:string、boolean、number、null、undefined本身并不是对象
      • 语言本身的一个Bug: typeof null=='object',但其实null是基本类型
    • 复杂基本类型:object
  • 内置对象
    • String,Number,Boolean,Object,Function,Array,Date,RegExp,Error
  • ES5 Object相关方法
    • Object.assign方法中,成员属性采用=操作符赋值,并不是深拷贝。
    • Object.preventExtensions(obj) 禁止扩展
    • Object.seal(..) 创建一个"密封"的对象,实际上会在现有对象上调用Object.preventExtensions(..),并把所有现有属性标记为configurable:false
      • 禁止扩展
      • 禁止配置
    • Object.freeze(..) 创建一个冻结对象,实际上会调用Object.seal(..)并把所有"数据访问"属性标记为writable:false
      • 禁止扩展
      • 禁止配置
      • 禁止修改属性值
  • [[Put]]大致会检查下面内容
    1. 属性是否是访问描述符?如果是并且存在setter就调用setter
    2. 属性的数据描述符中writable是否为false?如果是,在非严格模式下静默失败,在严格模式下抛出TypeError异常
    3. 如果都不是,将该值设置为属性的值
  • getter和setter的另一种写法
var myObj={
    get a(){
      return this.val;
    },
    set a(newVal){
      this.val=newVal;
    }
}
  • 检查属性是否存在
    • "xx" in Obj:会检查属性是否在对象及其原型链中
    • hasOwnProperty(..)只会检查属性是否在Obj对象中,不会检查原型链
  • 枚举
    • “可枚举”相当于“可以出现在对象属性的遍历中”
    • Object.propertyIsEnumerable(..)会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足enumerable:true
    • Object.keys(..)返回一个数组,包含所有可枚举属性。
    • Object.getOwnPropertyNames(..)返回一个数组,包含所有属性,无论它们是否可枚举。
  • 遍历
    • ES6新增 for..of循环语法来遍历value而非key
    • 数组对象有迭代器Symbol.iterator,而普通对象没有。可以自行添加一个:
    var obj={
        a:2,
        b:3
    }
    Object.defineProperty(obj,Symbol.iterator,{
        enumerable:false,
        writable:false,
        configurable:false,
        value:function(){
            var o=this;
            var idx=0;
            var ks=Object.keys(o);
            return{
                next:function(){
                    return {
                        value:o[ks[idx++]],
                        done:idx>ks.length
                    }
                }
            }
        }
    })

类 继承

  • 类实际上是一种设计模式。
  • 寄生继承
// “寄生类” Car
function Car() {
     // 首先,car 是一个Vehicle
     var car = new Vehicle();
     // 接着我们对car 进行定制
     car.wheels = 4;
     // 保存到Vehicle::drive() 的特殊引用
     var vehDrive = car.drive;
     // 重写Vehicle::drive()
     car.drive = function() {
     vehDrive.call( this );
     console.log(
          "Rolling on all " + this.wheels + " wheels!"
     );
     return car;
}
  • 隐式混入
var Something = {
    cool: function() {
        this.greeting = "Hello World";
        this.count = this.count ? this.count + 1 : 1;
    }
};
Something.cool();
Something.greeting; // "Hello World"
Something.count; // 1
var Another = {
    cool: function() {
    // 隐式把Something 混入Another
        Something.cool.call( this );
    }
};
Another.cool();
Another.greeting; // "Hello World"
Another.count; // 1 (count 不是共享状态)

原型

  • [[Prototype]]原型链
    • 假设有obj.foo="foo"的语句。如果foo属性不直接存在于obj中而是存在于原型链上层时,上述语句会出现三种情况:
      1. 如果在[[Prototype]] 链上层存在名为foo 的普通数据访问属性(参见第3 章)并且没有被标记为只(writable:false),那就会直接在myObject 中添加一个名为foo 的新属性,它是屏蔽属性。
      2. 如果在[[Prototype]] 链上层存在foo,但是它被标记为只读(writable:false),那么无法修改已有属性或在obj上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
      3. 如果在[[Prototype]] 链上层存在foo 并且它是一个setter(参见第3 章),那就一定会调用这个setter。foo 不会被添加到(或者说屏蔽于)myObject,也不会重新定义foo 这个setter。
      • 如果希望在上述第二、三种情况下也屏蔽foo,那就需要使用Object.defineProperty来向obj添加foo
  • 原型继承
    • 方法一:newObj.prototype=Object.create(oldObj.prototype)
    • 方法二:newObj.prototype=new oldObj()
    • ES6新增的修改对象原型的API:Object.setPrototypeOf(obj,other.prototype)
      • polyfill:
      Object.setPrototypeOf=Object.setPrototypeOf||function(obj,proto){
          obj.__proto__=proto;
          return obj;
      }
  • 检查"类"关系
    • a instanceof Foo 返回a的整条原型链中是否有指向Foo.prototype的对象。但仅限对象与函数之间的关系检查。
    • obj1.isPrototypeOf(obj2) 返回obj1是否出现在obj2的原型链中。
    • Object.getPrototypeOf(..) 获取原型链(_proto_)
  • class语法
    • ES6中,class无法定义类成员属性,只能定义类成员方法
    • super只能用于constructor()函数中。在子类中,只有在调用super()之后,才能使用this指针。

行为委托

  • 委托行为:在找不到属性或者方法引用时,会把这个请求委托给另一个对象。js中的表现是委托原型链上的对象。
  • 使用函数对象的name标识符:
var Foo={
    bar:function bar(){}
}

好处是方便自我引用和追踪调用栈。

  • 自省:检查实例的类型
    • 主要目的:通过创建方式来判断对象的结构和功能
  • JS的原型链机制本质上就是行为委托机制。

中卷

类型

  • symbol是ES6新增的第七种类型
  • typeof null === "object" // true

  • 数组
    • length 由数组最大的下标决定,之前的所有元素若未赋值,则为undefined。例如:
    var a=[]
    a[2]='1';
    a.length===3 //true
  • 数字
    • js中没有真正意义上的整数。“整数”即是没有小数的十进制数。
    • .tofixed(..)用来指定小数部分的显示位数
    • .topRecision(..)用来指定有效数位的显示位数
    • 浮点数相加通常不精确。解决方案是设置一个误差范围值,称为"机器精度"。ES6中,该值定义为Number.EPSILON中
    • 能够呈现的最大浮点数:Number.MAX_VALUE;最小浮点数:Number.MIN_VALUE
    • 非自反值:NaN

原生函数

  • js会自动为基本类型值包装一个封装对象
    • 不需要对诸如.length这样的场景做提前的性能优化,原因是浏览器已经对此做了优化,直接使用封装对象来"提前优化"反而会降低执行效率。
  • 优先使用基本类型而非封装对象
  • Function.prototype是一个空函数;RegExp.prototype是一个"空"的正则表达式(非空对象);Array.prototype是一个空数组
  • 函数调用的详细过程:
    • 当调用一个函数时,一个新的执行上下文就会被创建。
    1. 创建阶段:执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向。
      • 创建变量对象的过程:
      1. 建立arguments对象。
      2. 检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
      3. 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。
    2. 代码执行阶段:创建完成之后,开始执行代码。这个时候,会完成变量赋值,函数引用,以及执行其他代码。 3.执行完毕后出栈,等待被回收

强制类型转换

  • 值类型转换
    • 发生在运行时(runtime)
    • 隐式强制类型转换 var a=42; var b=a+""
    • 显式强制类型转换 var c=String(a);
  • 抽象值操作
    • toString
      • 对于普通对象,除非自行定义,否则toString()返回内部属性[[Class]]的值
    • JSON
      • JSON.stringify()可以字符串化所有安全的JSON值
      • 不安全的JSON值:undefined,function,symbol(ES6+)和包含循环引用(对象之间互相引用,形成一个无线循环)的对象。
      • toJSON() 如果对象中定义了toJSON()方法,JSON字符串化时会首先调用该方法,然后用它的值来进行序列化。
    • ToNumber
      • Number()
      • undefined->NaN,null->0
      • 处理失败时返回NaN
      • "","\n"," "等空字符串被ToNumber强制类型转换为0
    • ToBoolean
      • 假值(falsy value):undefined,null,false,+0,-0,NaN,""。转换为false
      • 假值对象(falsy object)
        • 并非封装了假值的对象
        • document.all是一个假值对象。经常通过将其转换为Boolean来判断浏览器是否是老版本IE。
      if(document.all){ /* it's IE */}
      • 真值:除""外的其他字符串,[],{},function(){}等
    • 对象的toPrimitive操作:调用valueOf(),若有并返回基本类型值,则使用该值进行强制类型转;若没有则调用toString()。如果两者都没有则产生TypeError错误。
    • 显示强制转换
      • 例: var c="3.14"; var d=+c; //3.14
      • 日期显示转换为数字
        • 获取时间戳的快速方法:var timestamp=+new Date();
      • ~运算符
        • 只适用于32位整数。强制操作数使用32位格式
        • 首先将值强制类型转换为32位数字,然后执行字位操作"非"
        • 对负数的处理与Math.floor(..)不同
      Math.floor(-49.6);  // -50
      ~~-49.6;            // -49
        - 用途:将字符串转为整数,取整,将值截为32位整数等
      
    • 显式解析数字字符串
      • parseInt(..)
        • 先将参数强制类型转换为字符串再进行解析。
        • 一些奇怪的行为:
      parseInt( 0.000008 ); // 0 ("0" 来自于 "0.000008")

parseInt( 0.0000008 ); // 8 ("8" 来自于 "8e-7") parseInt( false, 16 ); // 250 ("fa" 来自于 "false") parseInt( parseInt, 16 ); // 15 ("f" 来自于 "function..") parseInt( "0x10" ); // 16 parseInt( "103", 2 ); // 2 ```

  • 相等比较
    • 对于x==
      • 若type(x)为数字,type(y)为字符串,则返回x==ToNumber(y)的结果
      • 若type(x)为bool,type(y)为数字,则返回toNumber(x)==y的结果
      • 若type(x)为字符串或数字,type(y)为对象,则返回x==ToPrimitive(y)的结果
    • 在==中,null和undefined是一回事。除此之外其他值都不存在这种情况。
    var a = null;

var b; a == b; // true a == null; // true b == null; // true a == false; // false b == false; // false a == ""; // false b == ""; // false a == 0; // false b == 0; // false

- 根据规范,<= 被处理为 >,然后将结果翻转。例如a<=b被处理为a>b,然后将该结反转得到原结果值。

### 混合环境javascript
- 全局DOM变量
- 声明一个全局变量不仅仅是创建了一个全局变量,还会在global对象(浏览器中为window)中创建一个同名属性
- 由于浏览器演进的历史遗留问题,在创建带有id属性的DOM元素时也会创建同名的全局变量:
```javascript
<div id="foo"></div>
console.log(foo)  //HTML元素
  • 部分引擎的一些限制:
    • 字符串常量中允许的最大字符数(并非只是针对字符串值);
    • 可以作为参数传递到函数中的数据大小(也称为栈大小,以字节为单位);
    • 函数声明中的参数个数;
    • 未经优化的调用栈(例如递归)的最大层数,即函数调用链的最大长度;
    • JavaScript 程序以阻塞方式在浏览器中运行的最长时间(秒);
    • 变量名的最大长度。

异步

  • 并发
    • 严格来说,setTimeout(..,0)并不直接把项目插入到事件循环队列。定时器会在“有机会”的时候插入事件。因此两个连续的setTimeout(..,0)调用并不能严格保证其顺序处理。
  • 任务
    • ES6引入了任务对列,建立在事件循环队列之上。
      • 挂在事件循环队列的每个tick之后的一个队列
      • 在每次事件循环中要确保任务对列中的事件都做完(实际上用了嵌套循环)

Promise

  • Promise调用then(..)的时候,即使这个Promise已经决议,一同给then(..)的回调也总会被异步调用。
  • 一个Promise决议后,这个Promise上所有的通过then(..)注册的回调都会在下一个异步时机点依次被立即调用。
  • 不要依赖于Promise间回调的顺序和调度。
  • 若把同一个回调注册了不止一次(p.then(f);p.then(f)),那它被调用的次数就会和注册次数相同。
  • Promise回调中发生的异常错误,都会导致Promise拒绝(reject)
  • 关于Promise从创建到状态改变的执行顺序:
let promise = new Promise(function(resolve, reject) {
    console.log('Promise');
    resolve();
});
promise.then(function() {
    console.log('Resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// Resolved
  • Promise.catch():最好使用catch(..)来捕获error。原因:catch既可以捕捉Promise回调中的错误,也可以捕捉在它之前的then(..)回调中的错误。
  • 将多个Promise实例包装成一个新的Promise实例
    • Promise.all([]):仅当所有Promise为resolve,状态才变为resolve;如果有其中一个reject,状态就变为reject
    • Promise.race([]):当其中有一个实例改变了状态,则最终状态发生改变。那个率先改变状态的Promise实例返回的值便传递给then()(或catch())回调函数。
  • Promise.resolve()(Promise.reject())将现有对象转换成Promise对象。具体请看《ECMAScript6入门》

生成器

  • 任意一个数组对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。
let myIterable={};
let it=myIterable[Symbol.iterator]()
it.next()

for..of 应用的对象必须有Symbol.iterator方法

  • Generator函数就是遍历器生产函数,因此可以把Generator赋值给对象的Symbol.iterator属性
  • 若Generator函数中有赋值yield的语句,则next()传入的参数影响到函数内变量的值。例如:
function* foo(x) {
    var y = 2 * (yield (x + 1));
    var z = yield (y / 3);
    return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

程序性能

  • web worker
    • worker之间以及它们与主程序之间不会共享任何作用域或资源
    • 实例化worker:
    var w1=new Worker("http://xxx/index.js")  //应该指向一个javascript位置
    • 共享workernew SharedWorker(url)
      • 用于创建一个可以共享的中心Worker,节省资源
      • 调用程序必须使用worker的port对象用于通信:
      w1.port.addEventListener(...)
      w1.port.postMessage(..)
  • SIMD(单指令多数据)是一种数据并行方式,与Web Worker的任务并行相对。
    • 重点:并行处理数据的多个位
  • asm.js 指js中可以高度优化的一个子集

性能测试与调优

  • Benchmark.js 已经实现了许多有效的性能测试逻辑
  • 写好测试
    • 不要试图窄化到真实代码的微小片段,以及脱离上下文而只测量这一小部分的性能,因为 包含更大(仍然有意义的)上下文时功能测试和性能测试才会更好。这些测试可能也会运 行得慢一点,这意味着环境中发现的任何差异都更有意义。
  • 尾调用优化(TCO)
    • 尾调用:出现在另一个函数“结尾”处的函数调用,并且调用结束后没有其余事情要做了
    function foo(x){
        return x;
    }
    function bar(y){
        return foo(y+1);  // 尾调用
    }
    function baz(){
        return 1+bar(40); // 非尾调用
    }
    • 优化:支持TCO的引擎能够意识到foo(y+1)位于尾部,意味着bar(..)基本上已经完成了,那么在调用foo(..)时,就不需要创建一个新的栈帧,而是可以重用已有的bar(..)的栈帧。这样不仅速度快,也更节省内存。