作用域和闭包
作用域
编译原理
JavaScript是一门编译语言
传统编译语言的流程:
JS编译流程:
理解作用域
角色:
- 引擎:从头到尾负责整个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操作:
- 创建(或者说构造)一个全新的对象
- 这个新对象会被执行[[Prototype]]连接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么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]]机制(委托!)机制的一种语法糖