3. advanced_scoped.md

Lexical & Dynamic Scoped

lexical scoped: 指的是编译器, 解析器, 处理器执行之前,进行了一此预编译, 这就是词法作用域。词法作用域不是在运行时决定的, 是在编译的时候决定的。

Dynamic Scoped: 像bash是一种解释型语言, 他的作用域是动态作用域。因此它没有词法作用域, 不是编译型语言。

Lexical Scoped

  1. 词法作用域,其实映射的概念就是预编译,js在运行之前进行了预编译,把作用域, 和作用域内的变量以及函数信息都保存起来, 供引擎运行时使用。
  2. 词法作用域定下来就确定了, 一次编译,运行时就可以对此进行优化。
  3. 作用域之间是层级关系。

Dynamic Scoped

  1. 动态作用域指的是, 黑箱, 我只有打开它(运行的时候)才知道是怎么回事。
  2. 动态作用域下, 你对一个变量引用的结果取决于你的上下文环境。
  3. 所以, 动态作用域是在运行时候确定的, 而词法作用域是在编写代码时确定的。但是js有一种机制, 提供了类似的动态作用域的效果。

function scoped

  1. 函数作用域给了我们隔离外部作用域的机会(提供了命名空间), 我们应该尽量的保持函数内操作的变量私有。
  2. 并且持续遵循最小变量原则。函数内的变量函数内自己去创建和消化(object记得要深拷贝)。

IIFE Pattern(立即执行函数表达式模式)

  1. 使用函数表达式,创建一个作用域, 立刻调用它, 产生了作用域后, 执行函数体内容, 作用域消失。只运行一次,就消失了, 不会留下副作用。比如变量污染。
  2. IIFE是表达式, 而不是函数定义式。
  3. 如果你需要一个函数表达式,那么你完全可以使用IIFE去做这件事。比如try-catch语句
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    let a = (() => {
    try {
    someFn() // return value.
    }catch() {
    return 'defaultValue'
    }
    }())

    let value = "some";
    ((value)=> {
    console.log(value);
    value = "big";
    })(value);
    console.log(value); // some

Block Scoping

块作用域的特点: 只有在使用let/const定义时的作用域有效, 出了定义的作用域就无效.

块作用域的产生:

  1. 使用IIFE
    1
    2
    3
    4
    5
    6
    let value = "some";
    ((value)=> {
    console.log(value);
    value = "big";
    })(value);
    console.log(value); // some
  2. 在花括号内使用let/const定义
    1
    let a = 2; {let a = 1; console.log(a)};console.log(a)

let vs var (个人认为: const > let > var)

有作用域好过没有作用域。不用考虑边界情况.

当然,除非你想利用hosting(提升), 那么可以使用var.

一个有趣的冷知识:

1
2
3
4
var a = 1;
var a = 2;

// a在内存中中是同一个地址, 因为在第一次定义的时候在内存中已经有a这个变量了, 第二次定义做了一次查询而已.

explicit let block

1
2
3
4
5
6
7
8
9
(sum) => {
{ // 大胡子推荐这种写法,显示的去告诉大家我需要let的块级作用域, 明确变量的范围
let a = 1,
b = 2,
c = 3;
sum = a + b + c;
}
return sum;
}

const

你应该优先使用const, 之后再是let.

当时,const也存在一定的问题, 使用const, 不可以二次赋值。但是如果const定义的不是基础类型(string/boolea/number), 而是数组的话, 那么const的行为就有些怪异了。历史因素导致的, 当我们使用object类型(数组)的时候, 变量内存的是一个地址,而,这个地址中存的是对象属性中的地址。而我们修改属性的地址时, 实际上, 变量的地址是没有改变的。来看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const arr = [1, 2, 3]
arr = [2, 3, 4] // Throw Error
arr[0] = 2;
arr[1] = 3;
arr[2] = 4;

const obj = {
a: 1
}

obj = {} // Throw Error
obj.a = 2;

// 解决方案: freeze 但是注意, 分浅冻结和深冻结
Object.freeze();

从语义上理解, const是常量,意味着我不会再给他重新赋值。

遵循两个使用原则: 1. 原语类型 2. 只赋值一次, 不会再次赋值

Hoisting

在es的规范中,没有提及提升,我们所说的提升其实是一个比喻。就像我们说的词法作用域,都是一种比喻,一种理解这个概念的介质, 来看个例子:

1
2
3
4
a; // undefined
b; // undefined
var a = 1;
var b = 2;

实际上,js引擎把以上的代码转换成了这样: 把变量定义,函数定义自动的放在函数执行的顶部。

1
2
3
4
5
6
7
8
var a;
var b;


a; // undefined
b; // undefined
a = 1;
b = 2;

提升的特性: 变量定义提升,函数定义提升, 函数表达式不提升。

1
2
3
4
5
6
7
8
a(); // a
function a () {
console.log("a");
}

b(); // b is not a function
var b = () => {console.log("b");};
b(); // b

关于函数提升这一点,我们能理解到, 函数提升, 其实本质上就是做了C语言中,提前把要用的函数暴露出来,这一点他们是一致的。

同时,我们也可以用预编译来解释这件事, 预编译只对LSH做处理,也就是只处理收集了var b这个信息,而没有获取到RSH的信息,所以在使用b()的时候,没能获取到函数体本身.获取到函数体本身要到执行阶段才能获取到.

Hoisting explames

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 'a';
function b () {
console.log(a); // undefined
var a = 'b';
}

// 实际上,js引擎做了以下处理
var a = 'a';
function b () {
var a; // 初始化未赋值, 那么所有的初始值都是 undefined
console.log(a); // undefined
a = 'b';
}

其实得出这个结果一点也不怪异, 想想之前说的, 预编译阶段,在预编译阶段,function b 中就存有自己的变量a, 那么在执行阶段,console.log语句拿a变量其实是从,b函数的作用域中拿a。

let doesn’t hoist? No.

1
2
3
4
{
a = 'a'; // TDZ error
let a;
}

这有个概念叫临时死区, 在用let定义之前的区域都为死区,不可以在该区域使用a变量, 否则会报错。

大胡子说, 其实var和let/const都进行了提升,只是提升的行为不一样, var提升了变量定义,并且初始化了这个变量, 赋予初始值undefined.但是let/const也进行了提升, 只是没有进行初始, 这里没有进行初始化等价于不可以去使用它(是不是很像临时死区)