看了《你不知道的JavaScript 上》,为了防止自己忘记,特此记下与我而言的部分重点
任何足够先进的技术都和魔法无异。
——Arthur C. Clarke
作用域和闭包
编译原理
- 分词/词法分析(Tokenizing/Lexing)
编程语言的所有字块与符号都会被分解为词法单元;
var a = 2;
//分解为var、a、=、2、;。
// ! 空格是否被当做词法单元,取决于空格在这门语言中是否具有意义
- 解析/语法分析(Parsing)
这个过程是将词法单元流转换成一个由元素逐级嵌套所组成的代表了程序语法解构的树(AST树)。
- 代码生成
将AST转换为可执行代码(机器指令)的过程称为代码生成
- 编译器
编译器在编译过程的第二步生成了代码,此过程中,引擎会为变量进行LHS和RHS查询。“L”"R"代表赋值操作的左侧和右侧。
词法作用域
- 严格模式下,eval有自己的词法作用域
- 词法作用域意味着作用域是由函数声明的位置决定的
函数作用域
隐藏内部实现
指在软件设计中,应该最小限度地暴露必要内容,而将其内部都隐藏起来,比如某个模块或对象的API设计。也叫最小授权或最小暴露原则
// 例如:
function doSomething(a) {
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
function doSomethingElse(a) {
return a - 1;
}
var b; doSomething( 2 ); // 15
// 优化为:
function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
doSomething( 2 ); // 15
立即执行函数表达式
IIFE,代表立即执行函数表达式 (Immediately Invoked Function Expression),由于函数被包含在一对 ( ) 括号内部,因此成为了一个表达式,通过在末尾加上另外一个 ( ) 可以立即执行这个函数,比如 (function foo(){ … })()。第一个 ( ) 将函数变成表 达式,第二个 ( ) 执行了这个函数。
var a = 2;
(function foo() {
var a = 3;
console.log( a );
// 3
})();
console.log( a ); // 2
提升
只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。如果提升改变 了代码执行的顺序,会造成非常严重的破坏。
a = 2;
var a;
//声明本身会被提升 解析为↓
var a;
a = 2;
console.log( a ); //2
如果是 var a = 2 时,JavaScript 实际上会将其看成两个 声明:var a; 和 a = 2;。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。
console.log( a );
var a = 2;
//解析为 ↓
var a;
console.log( a );
a = 2;
函数声明与函数表达式同理
函数优先
函数声明和变量声明都会被提升。但是一个值得注意的细节(这个细节可以出现在有多个 “重复”声明的代码中)是函数会首先被提升,然后才是变量。
闭包
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用 域之外执行。
下面用一些代码来解释这个定义。
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz();
// 2 —— 朋友,这就是闭包的效果。
函数 bar() 的词法作用域能够访问 foo() 的内部作用域。然后我们将 bar() 函数本身当作 一个值类型进行传递。在这个例子中,我们将 bar 所引用的函数对象本身当作返回值。
在 foo() 执行后,其返回值(也就是内部的 bar() 函数)赋值给变量 baz 并调用 baz(),实际上只是通过不同的标识符引用调用了内部的函数 bar()。
bar() 显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。
在 foo() 执行后,通常会期待 foo() 的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很自然地会考虑对其进行回收。
而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是 bar() 本身在使用。
拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar() 在之后任何时间进行引用。
bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。
this和对象
关于this
this常见的误区:1,this指向函数本身;2,this指向函数的词法作用域;
this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
this的绑定规则完全取决于调用位置
1.函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
var bar = new foo()
2.函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
var bar = foo.call(obj2)
3.函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
var bar = obj1.foo()
4.如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。
var bar = foo()
就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白this的绑定原理了。
ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样。
关于对象
属性描述符
Object.getOwnPropertyDescriptor(ObjectName,'key');
//{
// value:'',
// writable:true, 该属性是否只读
// enumerable:true, 该属性是否可枚举
// configurable:true,该属性是否可以修改配置。单项操作,无法撤销
//}
密封 Object.seal(…) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions(…) 并把所有现有属性标记为configurable:false。 所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以 修改属性的值)。
冻结 Object.freeze(…) 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(…) 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们 的值。
禁止扩展 Object.preventExtension(ObjectName)
检查对象属性是否存在
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false
Object.prototype.hasOwnProperty.call(myObject,"a")
in操作符会检查属性是否在对象及其[[Prototype]]原型链中(参见第5章)。相比之下,hasOwnProperty(…)只会检查属性是否在myObject对象中,不会检查[[Prototype]]链。
for in 遍历可枚举属性
for of 返回所有值
出于各种原因,以“继承”结尾的术语(包括“原型继承”)和其他面向对象的术语都无 法帮助你理解 JavaScript 的真实机制(不仅仅是限制我们的思维模式)。 相比之下,“委托”是一个更合适的术语,因为对象之间的关系不是复制而是委托。