你不知道的 JS(上)

coderljw 2024-10-13 大约 5 分钟

# 1. 编译原理

  • 编译:分词/词法分析 -> 解析/语法分析 -> 代码生成。

    分词/词法分析:将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块称为词法单元。

    代码示意
    var a = 2;  分析为  var、a、=2;
    
    1
    2

    解析/语法分析:将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树(AST)。

    可能的代码示意
    VariableDeclaration: {
      Identifier: a,
      AssignmentExpression: {
        NumericLiteral: 2
      }
    }
    
    1
    2
    3
    4
    5
    6
    7

    代码生成:将 AST 转换为可执行代码的过程被称为代码生成。

# 2. LHS 与 RHS

  • RHS 查询与简单地查找某个变量的值别无二致,而 LHS 查询则是试图找到变量的容器本身,从而可以对其赋值。

  • 最好将其理解为 “赋值操作的目标是谁(LHS)” 以及 “谁是赋值操作的源头(RHS)”。

# 3. 编译器有话说小测验

  • 找出所有的 LHS 与 RHS 查询。
function foo(a) {
  var b = a
  return a + b
}

var c = foo(2)
1
2
3
4
5
6
  • LHS 查询(3 处):c = ..、a = 2(隐式变量分配)、b = ..
  • RHS 查询(4 处):foo(2..、= a、a ..、.. b

# 4. 作用域是什么

  • LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域(一层楼),最后抵达全局作用域(顶层),无论找到或没找到都将停止。

  • 不成功的 RHS 引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛出 ReferenceError 异常(严格模式下)。

# 5. 词法作用域与动态作用域

  • 主要区别:词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定。(this 也是! )词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。
function foo() {
  console.log(a) // 词法作用域打印2,动态作用域打印3
}

function bar() {
  var a = 3
  foo()
}

var a = 2
bar()
1
2
3
4
5
6
7
8
9
10
11

# 6. 欺骗词法

  • JavaScript 中有两个机制可以 “欺骗” 词法作用域: eval(..) 和 with。前者可以对一段包含一个或多个声明的 “代码” 字符串进行演算,并借此来修改已经存在的词法作用域(在运行时)。后者本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。

    • eval()。
    function foo(str, a) {
      eval(str)
      console.log(a, b) // 1, 3
    }
    
    var b = 2
    foo('var b = 3', 1)
    
    1
    2
    3
    4
    5
    6
    7
    • with。
    function foo(obj) {
      with (obj) {
        a = 2
      }
    }
    
    var o1 = {
      a: 3,
    }
    
    var o2 = {
      b: 3,
    }
    
    foo(o1)
    console.log(o1.a) // 2
    
    foo(o2)
    console.log(o2.a) // undefined
    console.log(a) // 2 -> a被泄露到全局作用域上了!
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

# 7. 立即执行函数表达式-IIFE

;(function foo() {
  console.log(777)
})()

;(function foo() {
  console.log(777)
}())

!function foo() {
  console.log(777)
}()
1
2
3
4
5
6
7
8
9
10
11

# 8. try / catch

  • catch 分句会创建一个块作用域,其中的声明的变量仅在 catch 内部有效。
try {
  undefined()
} catch (err) {
  console.log(err)
}

console.log(err) // ReferenceError: err is not defined
1
2
3
4
5
6
7

# 9. 闭包

  • 产生原因:我们在词法作用域的环境下写代码,而其中的函数也是值,可以随意传来传去。

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

function foo() {
  var a = 1
  // var b = 1
  return function bar() {
    // debugger // b 被销毁
    a++
    console.log(a)
  }
}

var bar = foo()
bar()
1
2
3
4
5
6
7
8
9
10
11
12
function wait(message) {
  setTimeout(function timer() {
    console.log(message)
  }, 1000)
}

wait('Hello,closure!')
1
2
3
4
5
6
7

# 10. 显示绑定

  • 如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式(也就是 new String(..)、new Boolean(..)、new Number(..))。这通常被称为 “装箱”。
function foo() {
  console.log(this.TheOne)
}
Number.prototype.TheOne = 'NEO'
String.prototype.TheOne = 'NEO'
Boolean.prototype.TheOne = 'NEO'
foo.call(7)
foo.apply('Matrix')
foo.bind(true)

7..TheOne // NEO
'Matrix'.TheOne // NEO
true.TheOne // NEO
1
2
3
4
5
6
7
8
9
10
11
12
13

# 11. this 绑定规则

  • 默认绑定
function foo() {
  console.log(this.a) // 2
}

var a = 2
foo()
1
2
3
4
5
6
  • 隐式绑定
function foo() {
  console.log(this.a) // 2
}

var obj = {
  a: 2,
  foo: foo,
}

obj.foo()
1
2
3
4
5
6
7
8
9
10
  • 显示绑定
function foo() {
  console.log(this.a) // 2
}

var obj = {
  a: 2,
}

foo.call(obj)
1
2
3
4
5
6
7
8
9
  • new 绑定。

    1. 创建(或者说构造)一个全新的对象。
    2. 这个新对象会被执行[[Prototype]]连接。
    3. 这个新对象会绑定到函数调用的 this。
    4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
function Foo(a) {
  this.a = a
}

var bar = new Foo(2)
console.log(bar.a) // 2
1
2
3
4
5
6

# 12. this 全面解析

  • 如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象。

    1. 由 new 调用?绑定到新创建的对象。
    2. 由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
    3. 由上下文对象调用?绑定到那个上下文对象。
    4. 默认:在严格模式下绑定到 undefined, 否则绑定到全局对象。
  • 一定要注意,有些调用可能在无意中使用默认绑定规则。如果想 “更安全” 地忽略 this 绑定,你可以使用一个 DMZ 对象,比如 0 = object.create(null),以保护全局对象。

function foo() {
  console.log(this.a) // 2
}

var a = 2
foo.call(null)
1
2
3
4
5
6
  • ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样。

# 13. 原型

以父之名
周杰伦.mp3