0%

你不知道的js上卷学习笔记

第一部分 作用域和闭包

第一章 作用域是什么

理解作用域

变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。

如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常。值得注意的是, ReferenceError 是非常重要的异常类型。

相较之下,当引擎执行 LHS 查询时,如果在顶层(全局作用域)中也无法找到目标变量,
全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎。

如果 RHS 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作,比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的属性,那么引擎会抛出另外一种类型的异常,叫作 TypeError 。

js 没有块级作用域,而其他语言 if while for 等块级结构一般都有独立的作用域,js 的函数的作用域是独立的

js 没有块作用域的例子

1
2
3
4
if (true) {
var c = 10;
}
console.log("c", c); // 输出10
作用域嵌套

作用域是根据名称查找变量的一套规则。当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。

异常

如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError
异常。

ES5 中引入了“严格模式”。在严格模式中 LHS 查询失败时,并不会创建并返回一个全局变量,引擎会抛出同 RHS 查询失败时类似的 ReferenceError 异常。

如果 RHS 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作,比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的属性,那么引擎会抛出另外一种类型的异常,叫作 TypeError 。

第二章 词法作用域

词法阶段

大部分标准语言编译器的第一个工作阶段叫作词法化,词法作用域就是定义在词法阶段的作用域,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变。

可以大致认为每一个函数都会创建一个新的作用域。

欺骗词法

eval 可以欺骗词法作用域,对词法作用域进行修改,但是严格模式下,eval 有自己的作用域。

1
2
3
4
5
6
function foo(str, a) {
eval(str); // 欺骗!
console.log(a, b);
}
var b = 2;
foo("var b = 3;", 1); // 1, 3

with 的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var obj = {
a: 1,
b: 2,
c: 3,
};
// 单调乏味的重复 "obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 简单的快捷方式
with (obj) {
a = 3;
b = 4;
c = 5;
}

尽管 with 块可以将一个对象处理为词法作用域,但是这个块内部正常的 var 声明并不会被限制在这个块的作用域中,而是被添加到 with 所处的函数作用域中。

第三章 函数作用域和块作用域

函数中的作用域

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。

IIFE 的另一个非常普遍的进阶用法是把它们当作函数调用并传递参数进去。

块作用域

try / catch 的 catch 分句会创建一个块作
用域,其中声明的变量仅在 catch 内部有效。

块作用域与内存垃圾收集机制也有相关。

1
2
3
4
5
6
7
8
9
function process(data) {
// 在这里做点有趣的事情
}
var someReallyBigData = { .. };
process( someReallyBigData );
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt) {
console.log("button clicked");
}, /*capturingPhase=*/false );

click 函数的点击回调不需要 someReallyBigData ,但是由于 click 函数形成了一个覆盖整个作用域的闭包,js 引擎极有可能依然保存着这个结构。

使用块作用域来解决这个问题

1
2
3
4
5
6
7
8
9
10
11
12
function process(data) {
// 在这里做点有趣的事情
}
// 在这个块中定义的内容可以销毁了!
{
let someReallyBigData = { .. };
process( someReallyBigData );
}
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /*capturingPhase=*/false );

第四章 提升

var a = 1 实际 js 引擎会当作两部分处理

1
2
var a; // 这一部分在编译阶段进行
a = 1;

函数声明和变量声明都会被提升。函数会首先被提升,然后才是变量。

js 不存在函数重载,后声明的函数会覆盖掉前面。

函数表达式不会被提升。

第五章 作用域闭包

for 循环头部的 let 声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

1
2
3
4
5
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}

输出 1 2 3 4 5

单例模式的实现,可以将函数转换为 IIFE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join(" ! "));
}
return {
doSomething: doSomething,
doAnother: doAnother,
};
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

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

附录

附录 C

箭头函数的 this 指向的是当前的词法作用域

第二部分 this 和对象原型

第一章 关于 this

为什么要使用 this

this 提供了一种优雅的方式隐式传递一个对象引用,因此可以将 api 设计的更加简洁和易于复用。

误解

this 在任何情况下都不指向函数的词法作用域

this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用 调用位置就在当前正在执行的函数的前一个调用

在浏览器环境下,默认 this 指向 window 对象,在全局作用域下定义的变量和函数也会绑定到 window 下,用 this 可以访问,但是在 node 的环境下就截然不同

独立函数调用采用默认绑定

而对象的方法调用会将 this 隐式绑定到对象上下文

1
2
3
4
5
6
7
8
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log(this.a);
}
foo(); // ReferenceError: a is not defined

不能把 this 和词法作用域的查找混合使用时,这是无法实现的。

1
2
3
4
5
6
7
8
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo,
};
obj.foo(); // 2

显式绑定 apply 和 call 方法,call 接受参数列表,apply 接受一个参数数组。 Function.prototype.bind(..) 会返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数。

关于 var self = this 的写法的解释,在内部作用域无法取到外部作用域的 this, 所以要使用一个变量保存起来。

实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行 [[ 原型 ]] 连接。
  3. 这个新对象会绑定到函数调用的 this 。换句话说,函数内的 this 指向的是 这个新对象
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象
this 到底是什么

当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。 this 就是记录的其中一个属性,会在函数执行的过程中用到。

第二章 this 全面解析

下面这个例子中,this 运行时绑定的是 settimeout(该函数在 settimeout 中调用),使用箭头函数可以绑定为 foo 的词法作用域

1
2
3
4
5
6
7
8
function foo() {
setTimeout(function () {
console.log(this.a);
}, 100);
}
var obj = {
a: 2,
};

第五章 原型

属性设置和屏蔽
  • 如果 foo 不直接存在于 myObject 中而是存在于原型链上层时 myObject.foo = “bar” 赋值操作会出现的三种情况
    如果在 [[Prototype]] 链上层存在名为 foo 的普通数据访问属性(参见第 3 章)并且没
    有被标记为只读( writable:false ),那就会直接在 myObject 中添加一个名为 foo 的新
    属性,它是屏蔽属性。
    如果在 [[Prototype]] 链上层存在 foo ,但是它被标记为只读( writable:false ),那么
    无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会
    抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。 * 如果在 [[Prototype]] 链上层存在 foo 并且它是一个 setter(参见第 3 章),那就一定会
    调用这个 setter。 foo 不会被添加到(或者说屏蔽于) myObject ,也不会重新定义 foo 这
    个 setter。
  • 一些随意的对象属性引用,比如 a1.constructor ,实际上是不被信任的,它们不一
    定会指向默认的函数引用

part6 行为委托

待完成