你不知道的JavaScript小结汇总

你不知道的JavaScript(上卷)

什么是作用域?

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

赋值操作会导致LHS查询。=操作符或调用函数时传入的参数都会导致关联作用域的赋值操作

JavaScript引擎会在代码执行前对其进行编译,在这个过程中,像 var = 2 这样的声明会被分解成两个步骤:
首先,var a 在其作用域中声明变量,这会在最开始的阶段,也就是代码执行前进行(预解析)
接下来,a=2 会查询(LHS查询)变量a并对其进行赋值。

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

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

//小测试
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
  1. 找出所有的 LHS 查询(这里有 3 处!)
    c = foo(2); a = 2(隐式变量分配)、b = a
  2. 找出所有的 RHS 查询(这里有 4 处!)
    =foo(2); = a;、a + b(这里是两次)

词法作用域

词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段(预解析阶段)基本能够知道全部标识符在哪里以及如何声明的,从而能够预测在执行过程中如何对他们进行查找。

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

这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎的认为这样的优化是无效的,使用这其中任何一个机制都将导致代码运行变慢,不要使用他们。

函数作用域和块作用域

函数是JavaScript中最常见的作用域单元。本质上,声明在函数内部的变量或函数会在所处的作用域中“隐藏”起来,这是有意而为之的良好软件的设计原则。

但函数不是唯一的作用域单元。块级作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指{…}内部)。

从ES3开始,try/catch结构在catch分句中具有块作用域。

在ES6中引入了let关键字(var 关键字的表亲),用来在任意代码块中声明变量。if(…){let a = 2;}会声明一个劫持了if的{…}块的变量,并且将这个变量添加到这个块中。

有些人认为块作用域不应该完全作为函数作用域的替代方案。这两种功能应该同时存在,开发者可以并且也应该根据需要选择使用何种作用域,创造可读、可维护的优良代码。

提升

我们习惯将 var a = 2; 看做是一个声明,而实际上JavaScript引擎并不这么认为。他将 var a 和 a = 2 当做是两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务。

这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。
可以将这个过程形象的想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。

声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。

要注意避免重复声明,特别是当普通的var声明和函数声明混合在一起的时候,否则会引起很多危险的问题!

作用域闭包

闭包就好像从JavaScript中分离出来的一个充满神秘色彩的的未开化的世界,只有最勇敢的人才能够到达那里。但实际上他只是一个标准,显然就是关于如何在函数作为值按需传递的词法环境中书写代码的。

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

如果没能认出闭包,也不了解他的工作原理,在使用它的过程中就很容易犯错,比如在循环中。但同时闭包也是一个非常强大的工具,可以用多种形式来实现模块等模式。

模块有两个主要特征:
(1)为创建内部作用域而调用了一个包装函数;
(2)包装函数的返回值必须至少包括一个内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。

现在我们会发现代码中到处都有闭包存在,并且我们能够识别闭包然后用他来做一些有用的事!

第二部分 this和原型

this和对象原型

对于那些那些没有投入时间学习this机制的JavaScript开发者来说,this的绑定一定一直是意见非常令人困惑的事。this是非常重要的,但是猜测,尝试并出错和盲目的从Stack Overflow上覆制和粘贴答案并不能让你真正理解this的机制

想学习this的第一步是明白this既不指向函数自身也不指向函数的词法作用域,你也许被斜眼的解释舞蹈过,但其实他们都是错误的。

this实际上是在函数被调用时发生的绑定,他指向什么完全取决于函数在哪里被调用。

this全面解析

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

1.由new调用?绑定到新创建的对象。
2.由call或者apply(或者bind)调用?绑定到指定的对象。
3.由上下文调用?绑定到那个上下文对象。
4.默认:在严格模式下绑定到undefined,否则绑定到全局对象。

一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”的忽略this绑定,你可以使用一个DMZ对象,比如$ = Object.create(null),以保护全局对象

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

对象

许多人都以为“JavaScript中万物都是对象”,这是错误的,对象是6个(或者是7个,取决于你的观点)基础类型之一。对象有包括function在内的子类型,不同子类型具有不同的行为,比如内部标签[object Array]表示这是对象的子类型数组。

对象就是键/值对的集合。可以通过.propName或者[“propName”]语法来获取属性值。访问属性时,引擎实际上会调用内部的[[Get]]操作(在设置属性时是[[Put]]),[[Get]]操作会检查对象本身是否包含这个属性,如果没找的的话还会查找[[Prototype]]链。

属性的特性可以通过属性描述符来控制,比如writable和configurable。此外,可以使用Object.preventExtensions(…)、Object.seal(…)、和Object.freeze(…)来设置对象(及其属性的不可变性级别)。

属性不一定包含值——他们可能是具备getter/setter的“访问描述符”。此外,属性可以是可枚举和不可枚举的,这决定了它是否会出现在for…in循环中。

你可以使用ES6的for…of语法来遍历数据结构(数组,对象,等等)中的值,for…of会寻找内置或者自定义的@@iterator对象并调用她的next()方法来遍历数据值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章