javaScript作用域和闭包

·  阅读 198

这是我参与8月更文挑战的第15天,活动详情查看:8月更文挑战

1.1 编译原理

  • 分词/词法分析 这个过程是将由字符组成的字符串分解成有意义的代码块 例如 var x = 2分解为var ,x,= ,2,一个个的字符。 分词和词法分析主要差异在于词法单元的识别是通过有状态还是无状态的方式进行的。

  • 解析/语法分析 这个过程主要是将词法单元转换成一个由元素逐级嵌套组成的代表程序语法结构的树 这个树叫做抽象语法树(AST)✔

  • 代码生成 将上面生成的语法树解析为可执行代码的过程称为代码生成 而 JavaScript 引擎要比上面复杂,JavaScript 是解释型语言,会存在性能问题,不过 Google 的 V8 引擎已经优化了很多,接近 C++/C 等语言了

1.2 作用域

简洁的来说就是代码可被访问到的区域

比如声明var x = 2,用一段代码说明

//global
console.log(this) //指向空对象null,global是顶层作用域
var x = 2
function func1() {
  console.log(this) //指向global,指向上一层func1定义时所在的作用域
  for (let i = 0; i < 123; i++) {
    console.log(i) //这里是for循环的作用域
  }
  console.log(i) //ReferenceError: i is not defined,这里访问不到let定义的块作用域,又称作用域死区
  func2()
}
func1()
复制代码

编译器(关于变量声明)

这里有 LHS 和 RHS 查询,即左值查询与右值查询(右值在 C 语言是一个专业术语)这里不过多解释 。这里 LHS 和 RHS 查询分别是对等号左右的值进行查询,例如var x = 2,编译器编译到这里会对 x 进 console.log(b)行 LHS 查询,如果在当前作用域没有查到,就会向上一层作用域继续查询,目的是为 2 找到一个赋值的目标,而console.log(b)则会进行 RHS 查询,目的是对console.log()引用地址的查询,找到 b 是谁,或者说在哪。如果 RHS 在所有的作用域中都没有找到所需要的变量,会抛出异常,是 ReferenceERROR 错误,引用错误。但是 LHS 如果在顶层作用域中也没找到变量时,则会创建一个该变量。(严格模式中会直接和 RHS 一样抛出错误) 这里有一个隐式说明要注意:

function foo(a){
  console.log(a)
}
foo(2)
复制代码

词法作用域

这里有两个关键词:

  • 词法化
  • 遮蔽效应 词法作用域是由你在写代码时将变量和块作用域写在哪里决定的。 遮蔽效应是如果在当前作用域和上一层作用域存在相同名称的变量,如果在当前作用域调用该变量,则当前作用域的变量会遮蔽外层作用域的变量
var a = 1
function f1() {
  var a = 2 //这里如果注释掉,下面会输出1
  console.log(a) // 2
}
f1()

复制代码

在词法作用域中有两种欺骗词法:eval()with()

function foo(str, a) {
  eval(str) //eval()将其中的参数会解析为js代码,并运行。
  //严格模式中。eval()运行时有自己的词法作用域,这样其中的参数将不会修改所在的作用域
  console.log(a, b)
}
var b = 2
foo('var b=123', 234) //
复制代码

还有 with()

function foo(obj) {
  with (obj) {
    a = 2
  }
}
var o1 = {
  a: 3,
}
var o2 = {
  b: 3,
}
foo(o1)
console.log(o1.a)
foo(o2)
console.log(o2.a) //undefined
console.log(a) //2,a被泄露到全局作用域里了
复制代码

with()可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域 当我们给 with()传 o1 时,o1 内部有 a 属性,直接赋值,而传 o2 时,内部没有,则进行 LHS 查询到全局作用域赋值,with()实际是给传递给他的变量创造了一个新的词法作用域。

this

常见 this 理解误区

  • 1.this 指向自身
  • 2.this 指向它的作用域(这种比较容易混淆,在某些状况是正确的,在其他状况是错误的)
this 绑定规则
  • 默认绑定
function f1() {
  console.log(this.a)
}
var a = 2
f1() // 2
复制代码
  • 隐式绑定
function f1() {
  console.log(this.a)
}
var obj = {
  a: 2,
  f2: f1,
}
obj.f2()
复制代码

隐式绑定一个常见的问题是隐式绑定的函数会丢失绑定对象,会引用默认绑定

function foo() {
  console.log(this.a)
}
var obj = {
  a: 3,
  foo: foo,
}
var bar = obj.foo
a = '123'
bar()

//
这里的参数传递其实就是一种隐式赋值。传入函数时也会被隐式赋值


function foo() {
  console.log(this.a)
}
function DoFoo(f) {
  f()
  obj.foo()
}
a = '123'
obj = {
  a: 223,
  foo: foo,
}
obj.foo()
DoFoo(obj.foo)
复制代码

还有一个问题是在回调函数中很容易出现 this 丢失的情况,而在一些回调函数中,还会出现修改 this 指向的情况 判断 this 的优先级

  • 函数是否在 new 中调用,(new 绑定)?如果是的话 this 绑定的是新创建的对象
  • var bar = new foo()
  • 函数是否通过 call(),apply()或者硬绑定调用?如果是的话,this 绑定的是指定的对象
  • var bar =foo.call(obj)
  • 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象
  • var bar = obj1.foo()
  • 如果都不是的话,使用默认绑定,如果在严格模式下,就绑定到 undefined,负责绑定到全局对象。
  • var bar = foo()

被忽略的 this

如果 call(),apply(),bind(),第一个参数传入的是 null 或者 undefined,那么实际绑定为默认绑定,常见的一种做法是用 apply(null,[...]),但是总是使用null有时会产生不必要的效果,比如 this 指向了全局作用域,这样会在不知不觉中修改了全局变量,一种安全的 this 做法是,利用object.create(null)创建一个空对象来作为 this 指向。

间接引用

function foo() {
  console.log(this.a)
}
var o1 = {
  a: 1,
}
var o2 = {
  a: 3,
  foo: foo,
}
var o3 = {
  a: 4,
}
o2.foo() //3
;(o3.foo = o2.foo)() //undefined
复制代码

赋值表达式 o3.foo=o2.foo 返回值是目标函数的引用,因此调用位置是 foo()而不是 o3.foo(),于是会用默认绑定,绑定到全局作用域,而全局作用域没有定义a,输出 undefined。

this 词法

上面是正常函数定义function(){},而 ES6 中提出新的定义函数方式:箭头函数,箭头函数 this 指向有外层作用域来决定,来看一段代码。

function foo() {
  return (a) => {
    console.log(this.a)
  }
}
var obj1 = {
  a: 2,
}
var obj2 = {
  a: 3,
}
var bar = foo.call(obj1)
bar.call(obj2) //2
复制代码

foo 内部 return 的箭头函数,this 调用时会捕获 foo()的 this,而 foo 的 this 绑定到来 obj1,所以 bar 的 call()无效了。

分类:
前端
标签: