第一部分·作用域和闭包
1.LHS和RHS引用
- 官方定义:
赋值操作的左侧和右侧。赋值操作不一定是=。- LHS:查找赋值操作的对象,即变量赋值并存入内存。
- RHS:取源值,即从内存中读取。
- 思考以下代码:
function foo(a){ // 对a进行LHS
console.log(a); // 对a进行RHS
}
foo(2); // 对foo执行RHS引用
- 为什么要区分LHS和RHS(以下是非严格模式下区别)
- LHS引用在作用域中找不到变量,那就会在全局作用域中声明一个。
- RHS引用在作用域中找不到变量,那就会报错ReferenceError。
- 思考如下代码:
function foo(a){
console.log(a+b);
b = a;
}
foo(2);
function foo(a){
console.log(a+b); // 对a,b进行RHS
b = a; // 对a进行RHS,对b进行LHS
}
foo(2);
2.词法作用域
- 定义:定义在词法阶段的作用域。
- 注意:变量的查找过程是由内而外查找,直到找到为止,若直到全局还没找到会报错。因此破坏词法作用域会降低性能。
- 破坏词法作用域的方法
(仅限于在非严格模式下),不推荐,而且会降低可读性,降低行能。
eval()--严格模式下eval会有自己的词法作用域,不会影响其他的 思考如下代码:
function foo(str){ eval(str); console.log(a); // 2 } foo("var a = 2")with思考如下代码:
function foo(obj){ with(obj){ a = 2; } } var o1 = { a: 3 }; foo(o1); console.log(o1.a); // 2 var o2 = { b: 3 }; console.log(o2.a); // undifined console.log(a); // 2,a被泄漏到了全局with会生成自己的词法作用域,但是其内部a=2也只是对a进行了LHS引用,当找不到时会在全局声明,所以o2就泄露到了全局。
3.函数作用域
IIFE匿名自执行函数
4.块级作用域
withtry/catch中catch会创建块级作用域let
5.提升
- 函数和变量声明会先执行,这种现象叫做
函数/变量声明提升。需要注意的是:如果函数名和变量名重复,函数名会优先。
6.闭包
- 当函数可以记住并访问
所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用 域之外执行。
7.动态作用域
- 与词法作用域区别:
- 词法作用域是在写代码或者说
定义时确定的,而动态作用域是在运行时确定的。(this) - 词法作用域关注函数
在何处声明,而动态作用域关注函数从何处调用。
- 词法作用域是在写代码或者说
8.块作用域的替代方案
- es6之前的块作用域是通过
try/catch来实现的,包括将es6的块级作用域转换成es5。 - 参考以下代码,理解es6转换为es5
// es6
{
let a = 2;
console.log(a); // 2
}
console.log(a); // ReferenceError
// es5
try{
throw 2;
}catch(a){
console.log(a); // 2
}
console.log(a); // ReferenceError
第二部分·this和对象原型
1.关于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);
}
}
// foo:6 foo:7 foo:8 foo:9
console.log(foo.count); // 0
2.this全面解析
- 绑定规则
- 默认绑定:无法应用其他规则时
function foo(){ console.log(this.a) } var a = 2; foo(); // 2- 隐式绑定: 调用位置是否有上下文对象, 或者说是否被某个对象拥有或者包
含(this指向
直接调用它的对象)
function foo(){ console.log(this.a) } var obj = { a:2, foo:foo } obj.foo(); // 2- 隐式丢失
然 bar 是 obj.foo 的一个引用, 但是实际上, 它引用的是 foo 函数本身, 因此,此时的bar() 其实是一个不带任何修饰的函数调用, 因此应用了默认绑定。function foo(){ cosole.log(this.a) } var obj = { a: 2, foo: foo } var bar = obj.foo; var a = "oops, gobal" bar(); // oops, gobal- 显示绑定:call()、apply()、bind()
function foo() { console.log(this.a) } var obj = { a: 2 } foo.call(obj); // 2- API调用
上下文: 第三方库的许多函数, 以及 JavaScript 语言和宿主环境中许多新的内置函数, 都提供了一个可选的参数, 通常被称为“上下文”(context), 其作用和 bind(..) 一样, 确保你的回调函数使用指定的 this。实际上是用call和apply实现了绑定
function foo(el){ console.log(el, el.this.id) } var obj = { id: "awesome" } [1,2,3].forEach(foo, obj);- new绑定:this指向new出来的实例化对象
function foo(val){ this.a = val; } var bar = new foo(2); console.log(bar.a); // 2 - 优先级:显示绑定 > 隐式绑定
- 间接引用
赋值表达式function foo(){ console.log(this.a) }; var a = 2; var o = {a:3,foo:foo}; var p= {a:4}; o.foo(); // 3 (p.foo = o.foo)(); // 2(p.foo = o.foo)()返回值是目标函数的引用,因此调用位置是foo(),而不是p.foo()或者o.foo()。因此这里使用默认绑定;
3.对象
- 语法
注 1: 原理是这样的, 不同的对象在底层都表示为二进制, 在 JavaScript 中二进制前三位都为 0 的话会被判断为 object 类型, null 的二进制表示是全 0, 自然前三位也是 0, 所以执行 typeof 时会返回“object”。
- 属性描述符
writable: 可写enumerable: 可枚举, “可枚举” 就相当于“可以出现在对象属性的遍历中”。
let myObject = {}; myObject.defineproperty(myObject, 'a', {value:2, enumerable: false}); myObject.defineproperty(myObject, 'b', {value: 3, enumetable: true}); for(let key in myObject){ console.log(key, myObject[key]); // 'b' 3 }configurable: 可配置
- 对象方法
Object.defineproperty:可以修改属性描述符, 注意: 如你所见, 把 configurable 修改成false 是单向操作, 无法撤销!Object.preventExtensions:禁止一个对象添加新属性且保留已有属性Object.seal:密封一个对象,即对该对象调用Object.preventExtensions并且把所有现有属性标记为configurable: false。Object.freeze:创建一个冻结对象,即对该对象执行Object.seal并且把所有现有属性标记为writable: false。
4.原型
- [[Prototype]]
在查找对象属性的时候,如果对象本身没有找到,那就会根据他的原型链一直向上查找,知道
Object.property。
for...in循环也是,如果只想查找对象本身,就需要hasOwnProperty()判断