《你不知道的JavaScript》 记录

376 阅读2分钟

记录<<你不知道的javascript>>书本的例子。 《高性能 JavaScript》 笔记

上卷

声明提升 ( 第四章 )

  • var a = 2; 可能会认为这是一个声明。但JavaScript 实际上会将其看成两个声明:var a; 和 a = 2 ; 第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。
  • 函数声明会被提升,但函数表达式却不会被提升。
  • 提升细节: 函数会首先被提升,然后才是变量。
foo();
var foo;
function foo(){
    console.log( 1 );
}
foo = function(){
    console.log( 2 );
}
// 结果  输出1  ,例上,var foo 属于重复声明,因此被忽略。 

尽管重复的var 声明会被忽略掉,但出现在后面的函数声明还是可以覆盖前面的。

foo()
function foo(){
    console.log(1);
}
var foo = function(){
    console.log(2);
}
function foo(){
    console.log( 3 );
}
 //  结果为 3 

作用域闭包 ( 第五章 )

闭包使得函数可以继续访问定义时的词法作用域。

 //典型闭包效果   
 // 1  return 形式
 function foo(){
    var a = 2;
    function bar(){
        console.log( a );
    }
    return bar;
}
var baz = foo();
baz();  //   这就是闭包效果
//2  把内部函数传递出
function foo(){
    var a = 2;
    function baz(){
        console.log( a )
    }
    bar( baz );
}
function bar( fn ){
    fn();  // 闭包效果
}
// 3  将其分配给全局变量
var fn;
function foo(){
    var a = 2;
    function baz(){
        console.log( a )
    }
    fn = baz
}
function bar( fn ){
    fn();
}
 foo();
 bar();  //2

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
现代的模块机制
大多数模块依赖加载器/管理器本质上都是将这种模块定义封装进一个友好的API。 ☆

var MyModels = (function (){
  var modeles = {};
  function define(name, deps, impl){
    for(var i=0;i< deps.length; i++){
      deps[i] = modeles[deps[i]]
    }
    // 扩展参数属性(变相引入模块) 这样以形参形式注入 ☆
    modeles[name] = impl.apply(impl, deps)
  }
  function get(name){
    return modeles[name]
  }
  return {
    define,
    get
  }
})()
// 定义模块
MyModels.define('bar', [], function(){
  function hello(){
    return 'xxx'
  }
  return{
    hello
  }
})
// 定义依赖模块
MyModels.define('foo', ['bar'], function(aa){
  var test = 'oo'
  function awesome(){
    console.log(aa.hello().toUpperCase())
  }
  return {
    awesome
  }
})
var foo = MyModels.get('foo');
console.log( foo.awesome() )

this 全面解析(第二章)

setTimeout 隐式绑定解析

function foo(){
    console.log( this.a );
}
function doFoo(fn){
    fn()   // 调用位置!
}
var obj = {
    a:2,
    foo:foo
}
var a = 'opps,global';
doFoo()   // 结果为 全局的值 opps,global  .参数传递其实就是一种隐式赋值。
// 这个时候,你用
setTimeout( obj.foo , 100 );   // 也是隐式赋值的  
setTimeout 内部行为 伪代码:
function setTimeout(fn,delay){
    fn();
}

这时就得用call or apply 来实现this 的绑定。当然也可以用ES5 提供的内置方法Function.prototype.bind 来实现绑定。

// 简单的辅助绑定函数 bind 
function bind(fn,obj){
    return function(){
            return fn.apply( obj , arguments );
    }
}

实际上,es5中内置的Function.prototype.bind(…) 更加复杂。下面是MDN 提供的一种bind(..) 实现。

if( !Function.prototype.bind ){
    Function.prototype.bind = function( oThis ){
        if( typeof this !== 'function'){
            // 与 ecmaScript 5 最接近
            throw new TypeError('bound is not callable')
        }
        var aArgs = Array.prototype.slice.call( arguments , 1 ),
            fToBind = this ,
            fNOP = function(){},
            fBound = function(){
                return fToBind.apply(( this instanceof fNOP && oThis ? this : oThis ),aArgs.concat( Array.prototype.slice.call( arguments ) ))

            };
            fNOP.prototype = this.prototype;
            fBound.prototype = new fNOP();
            return fBound;
    }
}

this 小结

按着下面的四个规则来判断this 的绑定对象。

  • 由 new 调用 ? 绑定到新创建的对象。
  • 由call 或者apply 、 or bind 调用?绑定到指定的对象。
  • 由上下文对象调用?绑定到那个上下文对象。
  • 默认: 在严格模式下绑定到undefined ,否则绑定到全局对象。

混合对象‘类’ 第四章

面向对象概念

面向对象编程强调的是数据和操作数据的行为本质上是互相关联的。(我们往往关心的不是数据是什么,而是可以对数据做什么,即行为)

常见的例子,“汽车”可以被看作“交通工具”的一种特例。在软件中可以定义一个Vehicle 类和一个car 类进行建模。 Vehicle 的定义可能包含推进器(比如引擎)、载人能力等,这些都是Vehicle 的行为。我们在Vehicle中定义的是几乎所有类型的交通工具都包含的东西。 在定义car 时,只要声明它继承了Vehicle这个基础就行。car的定义就是对通用Vehicle 定义的特殊化。 这就是类、继承和实例化。

(原型)继承

典型的“原型风格”

function Foo( name ){
    this.name = name;
}
Foo.prototype.myName = function(){
    return this.name;
}
function Bar( name , label ){
    Foo.call( this , name );
    this.label = label;
}
// 我们创建了一个新的bar.prototype 对象并关联到Foo.prototype
Bar.prototype = Object.create( Foo.prototype );  //  es6之前 需抛弃默认的Bar.prototype
//  es6  Object.setPrototypeOf( Bar.prototype , Foo.prototype ); 
//  
// 注意,现在没有Bar.prototype.constructor 了,如果你需要这个属性的话,可能需要手动修复它

Bar.prototype.mylabel = function(){
    return this.label
}

var a = new Bar('a','obj a')

a.myName();  //  a
a.mylabel();  // obj a

上述代码的核心部分就是 Bar.prototype = Object.create( Foo.prototype ) 。 调用Object.create(…) 会创建一个新对象,并把该对象的prototype 关联到你指定的对象里头。 常见的错误做法:

  • Bar.prototype = Foo.prototype // 仅引用而已,修改bar 原型的同时,foo 也会被修改
  • Bar.prototype = new Foo() // 多出附属属性

中卷

类型

Javascript 有七种内置类型,分别是 null 、undefined 、boolean 、number、string、object、Symbol(ES6 新增)。 typeof 安全防范机制。

typeof undefined === "undefined"
typeof true === "boolean"
typeof 42 === "number"
typeof '42' === "string"
typeof {} === "object"
typeof Symbol() === "symbol"
// 值得注意的是null
typeof null === "object"  // true
// 因此可以使用复合条件来检测null值
var a = null;
( !a && typeof a === "object" );  // true

javascript 中的变量是没有类型的,只有值才有 es6新加入了一个工具方法Object.is(..) 来判断两个值是否绝对相等。(0===-0 // true (要的是false) ,NaN === NaN // false) object.is(…) 主要用来处理那些特殊的相等比较。 下例是polyfill 版:

if( !Object.is ){
    Object.is = function( v1 , v2 ){
        // 判断是否是 -0
        if( v1 === 0 && v2 === 0 ){
            return 1/v1 === 1/v2 ;  // 即 -Infinity === Infinity  // false
        }
        // 判断是否是NaN
        if( v1 !== v1 ){
            return v2 !== v2;
        }
        // 其他情况
        return v1 === v2;
    }
}

值和引用

简单值总是通过值复制的方式来赋值/传递,包括null 、undefined、 字符串、数字、布尔和es6中的symbol 。 复合值对象(包括数组和封装的对象)和函数,则通过引用复制的方式来赋值/传递。

function foo(x){
    x.push(4);
    console.log( x );
    x = [4,3,4];   //  !! 新的引用 , 原来的还是原来的。
    x.push(5)
    console.log(x);
}
 var a = [1,2,3];
 foo( a );
console.log( a )  // 是[1,2,3,4] ,不是[4,3,4,5]

用slice能实现浅拷贝

第三章 原生函数

常用的原生函数有:

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

不推荐直接使用封装对象

var a = new String('abc');// new String('abc')创建的是字符串'abc'的封装对象,而非基本类型值'abc'.
typeof a ;   //  是object ,不是string   !!
a instanceof String   // true
Object.prototype.toString.call( a )  //  [object String] 
// Object.prototype.toString.call( [1,2,3] )  // [object Array]
//  Obect.prototype.toString.call( /regex-literal/i )  // [object RegExp]

封装对象转为基本类型, 使用valueOf()函数。相较于其他的构造函数,Date(..) 和 Error(..)用的就比较多,因为它们没有对应的常量形式。

   var a = new String('abc')
   a.valueOf() // abc 

Date.now() // 的兼容封装

   if( !Date.now ){
       Date.now = function(){
           return ( new Date() ).getTime();
       }
   }

_ 下划线前缀通常用于命名私有或特殊属性。 -- 开发习惯

第四章 强制类型转换

JSON.stringify(…) 在对象中遇到 undefined 、function 和 symbol 时会自动将其忽略,在数组中则会返回null。

JSON.stringify( undefined );   // undefined
JSON.stringify( function(){} );  // undefined
JSON.stringify( [1,undefined,function(){},4] );  // [1,null,null,4] 

**toJSON !!!**如果对象中定义了toJSON( ) 方法,JSON字符串化时会首先调用该方法,然后用它的返回值来进行序列化。如果要对含有非法JSON值得对象做字符串化,或者对象中的某些值无法被序列化时,就需要定义toJSON( ) 来返回安全的JSON 。 例子:

var o = {};
var a = {
    b:42,
    c:o,
    d:function(){}
}

//  在 a 中创建一个循环引用
a.e = o
// 这个时候JSON.stringify( a );  //  报错 Converting circular structure to JSON

//  如果自定义了JSON 序列化
a.toJSON = function(){
    return { b: this.b };
}
// 此时序列化
JSON.stringify( a )  //  "{"b":42}"   ☆

也就是说,toJSON( ) 应该“ 返回一个能够被字符串化的安全的JSON 值 ” JSON.stringify 的第二个参数是数组,那么它必须是字符串数组,其中包含序列化要处理的对象的属性名称。 如果第二个参数是函数,它会对对象本身调用一次,然后对对象中的每个属性各调用一次,每次传递连个参数,键和值。如果要忽略某个键就返回undefined,否则返回指定的值。

var a = {
    b:42,
    c:'23',
    d:[2,3,5]
};
JSON.stringify( a , ['c','d'] );   //  "{"c":"23","d":[2,3,5]}" 

4.2.2 ToNumber

为了将值转换为相应的基本类型值,会首先检查该值是否有valueOf() 方法 。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用toString() 的返回值,来进行强制类型转换。 eg:

var a = {
    valueOf:function(){
        return '123'
    }
};
var b = {
    toString:function(){
        return '345'
    }
};
var c = [4,2] ;
c.toString = function(){
    return this.join('')  // 42
}

Number( a );  // 123
Number( b );  //345
Number( c );  // 42

4.2.3 ToBoolean

除以下假值: undefined 、 null 、 false 、 +0 、 -0 和 NaN 、 “” 。 都是真值。
Javascript 有效数字范围是0-9和a-i(区分大小写 ) 。
除以下假值: undefined 、 null 、 false 、 +0 、 -0 和 NaN 、 “” 。 都是真值。 Javascript 有效数字范围是0-9和a-i(区分大小写 ) 。
正确的解释是:“==允许在相等比较重进行强制类型转换,而=== 不允许” 遵守以下两个原则可以让我们有效避免出错

  • 如果两边的值中有true 或者false , 千万不要使用 == 。
  • 如果两边的值有 [] 、‘ ’ 或者 0 ,尽量不要使用 == 。 因为,[] == ![] // true [] == false //true 了解下图即可:

第二部分 异步和性能

var a = {
    index : 1
};
console.log( a ); // { index : 2 }
a.index++; 

是什么造成上诉结果呢? 你应该意识到这可能是I/O 的异步化(console.log )造成的。 这个时候,你可以利用JSON.stringify( … ) 来实现” 快照 ” 。

第三章 Promise

回调地狱的一个例子:

function add( getX , getY , cb ){
    var x , y;
    getX( function(xVal){
        x = xVal;
        if( y != undefined ){
            cb( x + y );
        }
    } ) ;
    getY( function( yVal ) ){
        y = yVal;
        if( x != undefined ){
            cb( x ,y  );
        }
    }
}
// fetchX \ fetchY 是同步或者异步函数。
add( fetchX , fetchY , function(sum){
    console.log( sum ) // 如果不保证有想想x,y 的值 会照成 NaN
} )

promise 模式

var p1 = request('http://some.url.1/')
var p2 = request('http://some.url.2') // 等p1 和p2 两个再执行then
Promise.all([p1,p2]).then( function(msgs){
    return request( 'http://some.url.3..'+msg )
} )
 .then(function( msg ){
        console.log( msg )
    })

// 竞态形式  那个先完成就执行  Promise.last。。。 最后一个完成
Promise.race([p1,p2]).then( function(){
      //  ... 
} )
.then(function(msg){

})

创建一个已被拒绝的Promise,以下两个方式是等级的:

var p1 = new Promise( function( reslove,reject ){
    reject('Oops')
} );
// 等价于
var p2 = Promise.reject('Oops'); 

Promise 链式流

  • 每次你对promise 调用then( .. ) ,它都会创建并返回一个新的Promise ,我们可以将其链接起来。
  • 不管从then( .. ) 调用的完成回调( 第一个参数 ) 返回的值是什么,它都会被自动设置为被链接Promise(第一点中的) 的完成。 例子:
request( 'http://some.url.1' )
.then(function( response1 ){
    return request('http://some.url..')
})
.then(function(response2){
    console.log( response2 )
})
// 以此类推,实现链式

完整写法: 异步产生两个值。

function getY(x){
    return new Promise( function(resolve , reject){
        setTimeout( function(){
            resolve( (3 * x) -1 );
        },100 )
    })
}

function foo(bar ,baz){
    var x = bar * baz;
    // 返回两个promise
    return [ Promise.resolve( x ) , getY( x ) ];
}
Promise.all( foo( 10 , 20 ) ).then( function([x,y]){
    console.log( x ,y  ); 
})

jsPerf 可进行页面性能测试。

性能测试与调优

在考虑对代码进行性能测试时,你应该习惯的第一件事情就是你所写的代码并不总是引擎真正运行的代码。 举例来说:

function foo(x){
    return x;
}
function bar(y){
    return foo( y + 1 );
}
 function baz(){
    return 1 + bar( 40 )
}
baz();

foo( y + 1 ) 是bar( … ) 中的尾调用,因为在foo( .. ) 完成后,bar( … ) 也完成了,并且只需要返回foo( … ) 调用的结果。然后bar( 40 ) 执行之后,还得加上 1 才能得到baz() 的返回值。 尾调用优化的本质, 调用一个新的函数需要额外的一块预留内存来管理调用栈,称为栈帧,所以上面代码的foo 、 bar 、baz 都需为其保留栈帧。因此,对应支持TCO 的引擎能够意识到foo( y + 1 ) 调用位于尾部,这意味着bar( .. ) 基本完成了,那么在调用foo(.. )时,它就不需创建新的栈帧,而是可以重用已有的bar(..) 的栈帧。这样不仅速度更快,也更节省内存。 尾调用优化:

function factorial(n){
    if( n < 2 ) return 1;
    return n * factorial( n - 1 );
}
factorial( 5 );  // 120

//  尾调用优化后的 !。
function factorial(n){
    function fact( n , res){
        if( n < 2 ) return res;
        return fact( n - 1 , n * res );
    }
    return fact( n , 1 );
}
factorial( 5 ) // 120

ES5 Getter/Setter

ES5 定义了getter/setter 字面量形式,未来很可能(ES6)更广泛地使用。

    var o = {
        _id:10,
        get id(){ return this._id++ ;  },
        set id(v){ this._id = v }
    }
    o.id;  //  10
    o.id;  //  11
    o.id=1;  // 1
    o.id;  // 1 
    o.id;   // 2

ES6 对对象字面定义新增了一个语法,用来支持指定一个要计算的表达式。

var prefix = 'user_'
var o = {
    baz:function(){},
    [prefix+'foo']:function(){},
    [prefix+'bar']:function(){}
}

在之前我们可能要用o[prefix+’foo’] 的形式进行扩展。

for of 循环

在底层,for..of 循环向iterable 请求一个迭代器,然后反复调用这个迭代器把它产生的值赋给循环迭代变量。 for .. of 循环也可以通过 break 、 continue 、return 提前终止。 for…of 直接消耗一个符号规范的iterable . 常见写法:

for( var v of it ){
    console.log(v)
}
// 等价于  !!!!!!!
for( var v ,res; (res = it.next() ) && !res.done; ){
    v = res.value;
    console.log( v )
}

next() 迭代

var arr = [3,4,5]
var it = arr[Symbol.iterator]()
it.next();   // { value :1 , done:false }
it.next();  // { value :2 , done:false }
it.next();  // { value :3 , done:false }

it.next();   // { value :undefined , done:true }

自定义迭代器

除了标准的内置迭代器,你也可以构造自己的迭代器,是其能用(for…of…) 等操作。自定义迭代器,产生一个Fibonacci 序列:

var Fib = {
    [Symbol.iterator](){
        var n1 =1,n2 = 1;
        return {
            // 使迭代器成为 iterable
            [Symbol.iterator](){ return this; },
            next(){
                var current = n2;
                n2 = n1;
                n1 = n1 + current;
                return { value:current , done:false };
            },
            return (v){
                console.log('fibonacci sequence abandoned.')
                return { value:v ,done:true };
            }
        }
    }

}

for( var v of Fib ){
    console.log(v)
    if(v > 50) break;
}

// 1 1 2 3 5 8 13 21 34 55
//  fibonacci sequence abandoned 
 //  无 break 条件的话,会无线循环下去

数组解构可以部分或完全消耗一个迭代器:

var a = [3,4,5,6,7]
var it = a[Symbol.iterator]()
var [x,y] = it;  // 部分解构出 
var [z, ...w] = it; // 取得其余所有元素
it.next();    //  { value:undefined , done:true }
// x  3  // y 4  z  5  w  [6,7] 

生成器 在3.2 章节

//  ☆
function *foo(){
    // ... 
}

异步流控制

Promise 不是对回调的替代。Promise在回调代码和将要执行这个任务的异步代码之间提供了一种可靠的中间机制来管理回调。 set 固有的唯一性是它最有用的特性。(set是一个值的集合)
类数组对象转为数组形式
var arr = Array.prototype.slice.call( [ …. ] )
利用slice() 来复制一个真正的数组
var arr2 = arr.slice() or Array.from([]) 这种形式。
copyWithin(…) 从一个数组中复制一部分到同一个数组的另一个位置