hujiejeff / hujiejeff.github.io

https://hujiejeff.github.io
0 stars 0 forks source link

你不知道的JavaScript(上)笔记 #5

Open hujiejeff opened 4 years ago

hujiejeff commented 4 years ago

作用域是什么?

首先JS脚本执行过程分为:编译,运行。但是编译不会生成像Java编译结果,而是进行一些词法分析优化等 作用域是一套规则,用于帮助JS引擎查询标识符在何出、以及如何查询,通俗理解为一个领域。 JS脚本编译期间会识别声明语句,将声明的标识符放在适当作用域中。执行期间JS引擎则根据作用域去查找标志符,这也就是函数声明写在开头结尾都是可以,因为被编译器进行识别了。

LHS

左查询,查找某个标识符即将赋值,执行期间JS引擎遇见赋值语句比如:

var a = 2;

此时JS引擎会去作用域中查找a这个标识符,查询从当前作用域开始、一直查询到全局作用域。 如果没有查询到,如果在非严格模式下LHS查询会自动隐式的创建一个变量并且加入作用域,而严格模式下则并不会隐式创建,抛出ReferenceError错误。 这样也就能解释原来的一些怪异现象,隐式赋值到window对象。

RHS

右查询,查询某个标志符,获取内容

let b = 3;//LHS
console.log(b);//RHS
let a = b;//a是LHS,b是RHS

同样JS引擎会去作用域找RHS标识符,也是同样查询到全局作用域。接下这个查询过程的结果,会遇见我们很熟悉的两个错误。

hujiejeff commented 4 years ago

JS作用域的工作模型-词法作用域

编译阶段,词法作用域根据函数定义位置来决定作用域。 image 这样每个函数的作用域嵌套也就确定下来了。 标志符查找从当前作用域开始,向上查找,一旦查找到了就会停止,也即内部优先。 但是js有两个部分会临时产生作用域,即evalwith

eval,动态加载字符串,进行解析运行,并且生成一个新的作用域,比如

function show(str, b) {
   eval(str);
   console.log({a,b});
}
var a = 10;
show('var a = 20', 100);

这个部分运行时调用eval,声明了一个a=20,屏蔽了外层作用域的a,这个修改时发生在运行期间。原本的行为是会加载外出作用域的a的。

with

使用某个对象作为作用域,但是如果没有查找到则会,发生之前的作用域的查找失败动作。

let obj = {a: 'ff'};
with(obj) {
  a = 10;
  b = 20;//此作用域不存b,非严格模式下将在外层查找,直到没找到,隐式创建全局。
}
hujiejeff commented 4 years ago

块作用域?

一般来说,作用域分为,全局作用域和函数作用域,正常情况下,包括其它的编程语言都存在块作用域,出了这个就无法访问块作用域,片片JS的var就不是这样的23333,比如

var a = true;
if (a) {
    var b = 'hello';
}

console.log(b);//hello

原因就是var没有把标志放到块作用域中,而是外层的作用域,所以导致这样的结果。 在ES6,出现了letconst这两个显示指定放入块作用域的声明关键字,这样声明的标志就会只属于当前的块作用域,而不会流至外层。

那么ES6之前要怎么样实现块作用域,通过throw catch语句可以实现


    try {
        throw undefined;
    } catch(aa) {
        aa = 6;
        console.log(aa)
    }
    console.log(aa);//这里就访问不到了
hujiejeff commented 4 years ago

编译的时候,声明和赋值的是怎样操作的

考虑下面的代码

console.log(a);
var a = 9;
console.log(a);

会出现什么结果? 答案是

undefined
9

这就牵扯到了,编译器到底对于声明和赋值语句做了什么样的动作了,也就是所有声明会放在开头,而赋值则原地不动。 上述代码等同于

var a;
console.log(a);
a = 9;
console.log(a);

这样以来也就能对应我们的输出结果了。

结论

编译器会将代码的声明提升,而赋值不动 var a = 5实际上在编译器理解下是一个声明语句加上一个赋值语句,记住这一点。

函数声明优先于变量声明

这两种声明都会提升,但是函数声明会被优先提升

foo();
var foo = 10;
function foo() {
  console.log('xxx');
}

这种情况下可以正常运行输出xxx,不会提示TypeError,因为函数优先(一等公民不是盖的😀)

hujiejeff commented 4 years ago

闭包

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

即函数引用了外部作用域,即使这个外部作用域的执行环境已经不存在。这就叫闭包

于是闭包比我们想像中要常见得多

我们喜欢拿来当作闭包例子,函数返回函数引用。这可能是看到的教程中都会用这样类似的例子,告诉这里就有一个闭包效果。


function exampel() {
    let msg = 'hello';
    return function() {
        console.log(msg);
    }
}

let show = exampel();
show();

其实还有其它的例子

function foo() {
    var a = 2;
    function baz() {
        console.log(a); // 2 }bar( baz ); 
    }
    bar(baz)
}

function bar(fn) {
    fn(); // 妈妈快看呀,这就是闭包! 
}

以及我们经常用到的timer

function delayShow(msg, delay) {
    setTimeout(function() {
        console.log(msg)
    }, delay);
}

匿名函数对于外部的msg引用着,即使离开delayShow这个函数执行环境,在下一次的事件轮询队列中,匿名函数仍然能够访问msg,这也是一个闭包。确实这个不太容易注意到,因为太常见了呃呃

以及其它的回调其实都在使用闭包,而不是只是我们第一个例子的返回函数闭包

hujiejeff commented 4 years ago

this绑定优先级

new绑定>显示绑定>隐式绑定>默认绑定 在同时使用了两种绑定的情况下,this的指向问题取决于绑定方式的优先级,即使是显式绑定的bind绑定也会this变成new的this

默认绑定

function show() {
  console.log(this.a)
}
var a = '默认绑定';

默认绑定给了window,或者全局对象globalThis

隐式绑定

function show() {
  console.log(this.a);
}

let obj = {a: '隐式绑定'};
obj.show = show;
obj.show();//隐式绑定

显示绑定

function show() {
   console.log(this.a);
}
let obj = {a: '显式绑定'};
show.call(obj);

new绑定

function show() {
  this.a = 'aa';
  console.log(a);
}
let obj = new show();

被忽略的this

当绑定的this是null或者undefined是,会忽略绑定,进而走向默认绑定