读书笔记之《你不知道的JavaScript上卷》

96 阅读6分钟

作用域和闭包

作用域

编译原理

JavaScript是一门编译语言

传统编译语言的流程: image.png

JS编译流程: image.png

理解作用域

角色:

  • 引擎:从头到尾负责整个JavaScript程序的编译及执行过程
  • 编译器:负责语法分析及代码生成等脏活累活
  • 作用域:负责收集并维护所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,

作用域是根据名称查找变量的一套规则

  • LHS: 当变量在赋值操作的左侧时进行LHS查询(对变量进行赋值)
  • RHS: 当变量在赋值操作的非左侧时进行RHS查询(获取变量的值)

作用域嵌套

当一个块或函数嵌套在另一个块或函数中时

遍历嵌套作用域链的规则:引擎从当前执行作用域开始查找变量,如果找不到,就会向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。

##3 异常

ReferenceError: 如果RHS查询在所有嵌套的作用域中遍寻不到所需的变量

当引擎执行LHS查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序在非“严格模式”下,否则抛出“ReferenceError”异常

TypeError: 尝试对RHS查询到的一个变量进行不合理的操作,比如试图对一个非函数类型的值进行函数调用,或者引用null或undefined类型的值中的属性

词法作用域

词法阶段

定义在词法阶段的作用域。是由你写代码时将变量和块作用域写在哪里来决定的。

词法作用域只会查找一级标识符,比如“foo.bar.baz”,只查找“foo”

欺骗词法

在运行时来修改词法作用域

  • eval
  • with

会导致性能下降: JavaScript引擎会在编译阶段进行数项的性能优化,其中有些优化依赖于能够根据代码中的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。但如果引擎在代码中发现了“eval”、“with”,以上都做不了。

函数作用域和块作用域

函数中的作用域

属于这个函数的全部变量都可以在整个函数的范围内使用及复用

隐藏内部实现

最小特权原则/最小授权原则/最小暴露原则:
应该最小限度地暴露必要内容,而将其他内容都隐藏起来

规避冲突

函数作用域

区分函数声明和函数表达式的最简单方法是看function关键字出现在声明中的位置,如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式

块作用域

for、with、try/catch

let/const

提升

变量和函数声明从它们在代码中出现的位置被“移动”到了最上面,这个过程就叫做“提升0”

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

作用域闭包

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行

词法作用域是在写代码或者定义时确定的,而动态作用域是在运行时确定的。
词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用

this和对象原型

关于this

误解

function foo(num) {
    console.log('foo: ', num);
    this.count++;
}
foo.count = 0;
var i;
for (i = 0; i < 10; i++) {
    if (i > 5) {
        foo(i);
    }
}
console.log(foo.count); // 0 ? 为什么?

以上代码最后为啥输出0?

因为foo函数中的this是指向window的。

如果要从函数对象内部引用它自身,那只使用this是不够的,一般来说你需要通过一个指向函数对象的词法标识符(变量)来引用它。

function foo(num) {
    console.log('foo: ', num);
    foo.count++;
}

另一种方法是强制this指向foo函数对象

function foo(num) {
    console.log('foo: ', num);
    this.count++;
}
foo.count = 0;
var i;
for (i = 0; i < 10; i++) {
    if (i > 5) {
        foo.call(foo, i); // !!!
    }
}
console.log(foo.count);

this到底是什么?

this是在运行时绑定的,并不是在编写时绑定的。

this全面解析

this绑定的规则

  • 默认绑定
function foo(num) {
    console.log(this.a);
}
var a = 2;
foo(); // 2
  • 隐式绑定
function foo(num) {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
obj.foo(); // 2
  • 显式绑定
    call、apply
  • new绑定
    new操作:
  1. 创建(或者说构造)一个全新的对象
  2. 这个新对象会被执行[[Prototype]]连接
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

this优先级

new > 显示 > 隐式 > 默认

被忽略的this: 如果把null或undefined作为this的绑定对象传入call、apply、或bind,这些值在调用时被忽略,实际应用的是默认绑定规则。

箭头函数

箭头函数的绑定无法被修改

对象

混合对象类

类是一种设计模式

类意味着复制

传统的类被实例化时,它的行为会被复制到实例中,类被继承时,行为也会被复制到子类中。

多态(在继承链的不同层次名称相同但是功能不同的函数)看起来似乎是从子类引用父类,但是本质上引用的其实是复制的结果

原型

JavaScript中的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用

所有普通的[[Prototype]]链最终都会指向内置的Object.prototype

function Foo() {
    console.log(this);
}
var a = new Foo();
console.log(Object.getPrototypeOf(a) === Foo.prototype); // true
console.log(Foo.prototype.constructor === Foo); // true
console.log(a.constructor === Foo); // true

a并没有constructor属性,所以它会委托[[Prototype]]链上的Foo.prototype

函数不是构造函数,但是当且仅当使用new时,函数调用会变成“构造函数调用”

修改对象的[[Prototype]]关联:

Bar.prototype = Object.create(Foo.prototype);
Object.setPrototypeOf(Bar.prototype, Foo.prototype);
a instanceOf Foo

Foo.prototype.isPrototypeOf(a)

在a的整条[[Prototype]]链中是否有Foo.prototype指向的对象

行为委托

行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。

JavaScript的[[Prototype]]机制本质上就是行为委托机制

class基本上只是现有[[Prototype]]机制(委托!)机制的一种语法糖