你不知道的JavaScript_读书笔记 - childlabor/blog GitHub Wiki

你不知道的JavaScript(上)

作用域

编译:分词(词法分析)-> 解析(语法分析/AST抽象语法树)-> 代码生成

在 语法分析 和 代码生成 阶段有特定的步骤来对运行性能进行==优化==,包括对冗余元素进行优化等

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对 变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。

如果词法作用域完全由写代码期间函数所声明的位置来定义,怎样才能在运行时来“修改”(也可以说欺骗)词法作用域呢?

JavaScript 中有两种机制来实现这个目的。eval(..) 和 with 会在运行时修改或创建新的作用域,以此来欺骗其他在书写时定义的词 法作用域。

JavaScript 引擎会在编译阶段进行数项的性能优化。引擎在代码中发现了 eval(..) 或 with,所有的优化可能都是无意义的,因此最简单的做法就是完全不做任何优化。因此如果代码中大量使用 eval(..) 或 with,那么运行起来一定会变得非常慢。

当你看到 var a = 2; 时,可能会认为这是一个声明。但 JavaScript 实际上会将其看成两个 声明:var a; 和 a = 2;。第一个定义声明是在==编译阶段==进行的(提升)。第二个赋值声明会被留在 原地等待执行阶段。

// ==
a = 2;
var a; 
console.log( a );
//  实际 = >
var a;
a = 2; 
console.log( a ); // 2

// ==
console.log( a );
var a = 2;
//  实际 = >
var a;
console.log( a ); // undefined 不会报错
a = 2;

函数声明和变量声明都会被提升。

函数会首先被提升,然后才是变量。

foo(); // 1
var foo;
function foo() {
    console.log( 1 ); 
};
foo = function() { 
    console.log( 2 ); 
};
// 会输出 1 而不是 2 !这个代码片段会被引擎理解为如下形式:
function foo() {
    console.log( 1 );
};
foo(); // 1
foo = function() { 
    console.log( 2 ); 
};

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

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

this

this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调 用时的各种条件。

  1. 由 new 调用?绑定到新创建的对象。
  2. 由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
  3. 由上下文对象调用?绑定到那个上下文对象。
  4. 默认:在严格模式下绑定到 undefined,否则绑定到全局对象。
  5. ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。

你不知道的JavaScript(中)

运算符优先级

执行顺序并非我们所设想的从左到右

&& 运算符先于 || 执行,而 || 的优先级又高于 ? :

true || false && false; // true
(true || false) && false; // false
true || (false && false); // true

对 && 和 || 来说,如果从左边的操作数能够得出结果,就可以忽略右边的操作数。我们将 这种现象称为“短路”(即执行最短路径)。

浏览器在后台异步处理控制台 I/O 能够提高性能。如果在调试的过程中遇到对象在 console.log(..) 语句之后被修改,可你却看到了意料之外的结果,要意识到这可能是这种 I/O 的异步化造成的。

var a = {
 index: 1
};
// 然后
console.log( a ); // ??
// 再然后
a.index++;

// 输出{ index: 2 }

你不知道的JavaScript(下)

ES6 引入了一个新的运算符 ...,通常称为 spread rest(展开或收集)运算符,取决于 它在哪 / 如何使用。


// 展开
function foo(x,y,z) { 
 console.log( x, y, z ); 
} 
foo( ...[1,2,3] ); // 1 2 3

// 收集
function foo(x, y, ...z) { 
 console.log( x, y, z ); 
} 
foo( 1, 2, 3, 4, 5 ); // 1 2 [3,4,5]

set 固有的唯一性是它最有用的特性。举例来说:

var s = new Set( [1,2,3,4,"1",2,4,"5"] ), 
uniques = [ ...s ]; 
uniques; // [1,2,3,4,"1","5"]
// set 的唯一性不允许强制转换,所以 1 和 "1" 被认为是不同的值。

ES6 定义了一个字符串原型方法 repeat(..):

"foo".repeat( 3 ); // "foofoofoo"

Object.observe(); 已废弃

Proxy使用场景

// 1. 抽离校验模块
let numericDataStore = { 
    count: 0, 
    amount: 1234, 
    total: 14 
}; 
numericDataStore = new Proxy(numericDataStore, { 
set(target, key, value, proxy) { 
    if (typeof value !== 'number') { 
        throw Error("Properties in numericDataStore can only be numbers"); 
    } 
        return Reflect.set(target, key, value, proxy); 
    } 
}); 
// 抛出错误,因为 "foo" 不是数值 
numericDataStore.count = "foo"; 
// 赋值成功 
numericDataStore.count = 333;


// 2. 私有属性
let api = { 
    _apiKey: '123abc456def', /* mock methods that use this._apiKey */ 
    getUsers: function(){}
};
const RESTRICTED = ['_apiKey'];
api = new Proxy(api, { 
    get(target, key, proxy) { 
        if(RESTRICTED.indexOf(key) > -1) { 
            throw Error(`${key} is restricted. Please see api documentation for further info.`); 
        } 
        return Reflect.get(target, key, proxy); 
    }
});

// 3. 访问日志
...

// 4. 预警和拦截
...

// 5. 过滤操作
...

// Proxy 支持随时取消对 target 的代理
let target = {};
let handler = {};

let {proxy, revoke} = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo // 123

revoke();
proxy.foo // TypeError: Revoked

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。

将Object对象的一些明显属于==语言内部==的方法(比如Object.defineProperty),放到Reflect对象上。

Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。