notes

参考书籍:《你不知道的JavaScript》

以下例子中的foobar等变量没有特殊意义,只是占位符。例子中默认使用严格模式。

严格模式

"use strict";

严格模式会使js代码运行的更加严谨,减少怪异和不安全的地方。

严格模式也是有作用域的。

function strict(){
  "use strict";
  return "这是严格模式。";
}

function notStrict() {
  return "这是正常模式。";
}

严格模式可能会改变this指向

function fn() {
  console.log(this); // window
}

function fn() {
  "use strict";
  console.log(this); // undefined
}

参考资料

作用域

词法作用域

意思是写代码时将变量和块作用域写在哪里决定的,当词法分析器处理代码时将会保持作用域不变。

函数作用域

var声明的变量是函数作用域

块作用域

let声明的变量隐式地绑定到了所在的作用域中。(通常是块作用域) const也是块作用域。

function foo() {
  if (1) {
    let a = '123';
    var b = '123';
  }
  console.log(a); // ReferenceError: a is not defined
  console.log(b); // 123
}
foo();

声明提升

包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。

编译阶段会先找到作用域内所有的声明。

a = 2;
var a;
console.log(a); // 2
console.log(a); // undefined
var a = 2;
--- 等价于 ---
var a;
console.log(a); // undefined
a = 2;

垃圾回收

循环和闭包

this绑定规则

若不使用箭头函数,this有四种绑定规则

默认绑定

默认绑定一般是拿到函数的指针,独立调用。

function foo(a) {
  console.log(this); // window
}
function foo2(a) {
  "use strict";
  console.log(this); // undefined
}
foo(a); // foo.call(undefined, a)

foo(a);相当于调用了foo.call(undefined, a)。在非严格模式下,undefined会被替换成全局对象。

隐式绑定

隐式绑定判断调用时是否有上下文对象。

function foo() {
  console.log(this.a);
}
var obj = {
  a: 2,
  foo: foo
}
var topObj = {
  a: 3,
  obj: obj
}
var tmp = obj.foo;

obj.foo();        // 例子1:2
topObj.obj.foo(); // 例子2:2
tmp();            // 例子3:undefined

函数在没有调用的时候,里面的this是不确定的。调用foo时,需要考虑上下文对象,例子1中,上下文对象是obj,例子2中多个调用链只取最后一层。例子3中实际上是拿到函数指针直接调用,this就是undefined。

显式绑定

显式的绑定this,优先级比前两个高。

foo.call(undefined, a)
foo.bind(this);

new绑定

function foo(a) {
  this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2

箭头函数绑定

若使用了箭头函数,以上四种规则全部失效。

箭头函数默认绑定外层(作用域的)this。

var obj = {
  a: 2,
  foo: () => {
    console.log(this);
  }
}
obj.foo(); // undefined

例子中,箭头函数默认不会使用自己的this,而是会和外部this保持一致,而外部作用域是全局作用域,所以最外层的this就是undefined。

事件循环

尾调用优化

尾调用优化(Tail Call Optimization,TCO)

尾调用就是在一个函数的结尾处调用另一个函数。

function foo(x) {
  return x;
}

function bar(y) {
  return foo(y + 1);    // 尾调用
}

function baz() {
  return 1 + bar(40); // 非尾调用
}

return 1 + bar(40);这句不是尾调用是因为在bar(40)完成后,结果需要+1才能由baz()返回。

调用一个新的函数需要一块预留内存来管理调用栈,称为栈帧

所以上面的代码一般会同时需要为每个foobarbaz函数保留一个栈帧。

然而,如果JS引擎支持TCO,就能意识到return foo(y + 1);调用位于尾部,这意味着bar(..)基本上已经完成了,就不需要创建一个新的栈帧,而是可以重用已有的bar(..)的栈帧。

尤其是在处理递归时,如果递归可能会导致成百上千个栈帧的时候。有了TCO,引擎可以用同一个栈帧执行所有这类调用!

这个我深有体会,在之前做iOS的时候写过一个固件升级的功能,手机需要向智能硬件发送固件数据,一般都是通过蓝牙(4.0)发送的,蓝牙由于低功耗的特定,一次只能发送20字节的数据。要传一个几兆甚至几十兆的固件,我写了一个递归算法去读固件的数据,一次大概读20个字节,这就产生了成千上万的栈帧,Xcode报了一个类似调用栈达到最大的一个错误。现在想想原因,可能就是递归的时候没有实现尾调用。

ES6强制要求引擎实现TCO。

强制类型转换

强制类型转换发生在运行时(runtime)。

var arr = [];
if (arr) {
  console.log(arr);
}

强制转换为false的一共只有以下几种:

宽松的相等和严格的相等

==允许在比较中强制类型转换,===不允许。

原型链

JS中没有类,只有对象和基本类型。原型链的作用类似于类中的继承。

在所有的JS对象中,默认有一个prototype属性,这个属性会指向其父对象。所有prototype链最终会指向内置的Object.prototype。因为所有的JS普通对象都源于Object.prototype,所以它包含JavaScript中许多通用的功能。

来看以下的例子:

var obj = { a: 1 };
var s = obj.toString();
console.log(s);
--------------------------------- 
↓{a:1}
  a: 1
  ↓__proto__:
    constructor: ƒ Object()
    hasOwnProperty: ƒ hasOwnProperty()
    isPrototypeOf: ƒ isPrototypeOf()
    propertyIsEnumerable: ƒ propertyIsEnumerable()
    toLocaleString: ƒ toLocaleString()
    toString: ƒ toString()
    valueOf: ƒ valueOf()
    __defineGetter__: ƒ __defineGetter__()
    __defineSetter__: ƒ __defineSetter__()
    __lookupGetter__: ƒ __lookupGetter__()
    __lookupSetter__: ƒ __lookupSetter__()
    get __proto__: ƒ __proto__()
    set __proto__: ƒ __proto__()

我们在chrome的调试器里面可以看到,obj对象有一个隐藏的__proto__属性,里面有toString之类的函数,这个就是在Object.prototype中定义的,使用{}创建的对象prototype都默认指向Object.prototype

我们也可以使用Object.create()改变prototype的指向。比如:

var obj1 = {
  a: 2
};
var obj2 = Object.create(obj1);
obj2.b = 3;
console.log(obj2);
--------------------------------- 
↓{b: 3}
  b: 3
  ↓__proto__:
    a: 2
    ↓__proto__:
      constructor: ƒ Object()
      hasOwnProperty: ƒ hasOwnProperty()
      isPrototypeOf: ƒ isPrototypeOf()
      propertyIsEnumerable: ƒ propertyIsEnumerable()
      toLocaleString: ƒ toLocaleString()
      toString: ƒ toString()
      valueOf: ƒ valueOf()
      __defineGetter__: ƒ __defineGetter__()
      __defineSetter__: ƒ __defineSetter__()
      __lookupGetter__: ƒ __lookupGetter__()
      __lookupSetter__: ƒ __lookupSetter__()
      get __proto__: ƒ __proto__()
      set __proto__: ƒ __proto__()

这个时候obj2.a=2obj2.b=3

但是我们有时候不需要原型链,可以使用Object.create(null)创建对象,可以把它当做非常纯净的对象用来存储数据。

原型链这东西可能开发的时候用得比较少,但是他能让我们看懂别人写的代码,比如有时候会看到别人写这种代码:new function(){...}obj.prototype.name = function(){...},只要了解原型链这些就明了了。

参考链接

JS的编译器和解释器