zf-fe

04-复习上节课的作业题「详细讲解:VO、AO、GO以及一些其它细节知识点」

2020年5月21日

★作业

◇题一

/*阿里面试题*/
let a = {
    n: 10
};
let b = a;
b.m = b = {
    n: 20
};
console.log(a);
console.log(b);

画图得结果:

连等

关键点,连等操作,右左中……

程序实际执行结果:

result

疑问:

如果出现多个连续赋值操作呢?那这执行顺序又是咋样的呢?

◇题二

/*360面试题*/
let x = [12, 23];
function fn(y) {
    y[0] = 100;
    y = [100];
    y[1] = 200;
    console.log(y);
}
fn(x);
console.log(x);

画图得结果:

draw

实际结果:

error

正确分析:

res

你要在栈中搞个局部变量哈!

◇老师解析

题一:

题一

  1. 拿到可执行的代码
  2. 创建ECStack执行环境栈
  3. 要让全局代码执行 -> 需要先创建一个全局执行上下文 -> 为啥要这样做呢? -> 这是为了防止冲突呀(想象成把一个细胞扔到一个地方去执行):

    保护层

  4. 创建EC(Global),简写EC(G) -> 入栈执行 -> ECStack
    1. EC(G)下有个GO -> GO = { setTimeout: ()=>{}……} -> 有很多内置的对象 -> 我们的window对象就是指向这个GO的 -> 细节:在全局下用let声明的变量,同样是全局变量,只是没有放到GO旗下罢了……

只要代码执行就会创建一个执行上下文 -> 这是浏览器V8底层帮我们干的事儿

对比自己的认识,我没有意识到是把一个东西扔到ECStack里边去执行


题二:

题二

  1. ECStack
  2. EC(G)
    1. GO
    2. VO -> 变量对象 -> 只有一个作用,那就是存储当前上下文里边的变量

普通对象、数组在堆里存储的是键值对,而函数则是存两个东西:

代码字符串?我们写的整个js文件就是代码字符串,你把函数里边的代码也可以看作是一个小型的js文件

函数的dir

有人问,为啥没有看到代码字符串?

那是因为,dir查看一个东西,只是把它当作是对象来查看,而那些代码字符串,在控制台里边是看不到的,而是偷偷地在某个地方自我保存起来了,总之,我们其实是看不到堆里边的内容,但是这控制台做了处理,让我们看到了堆里边的键值对……

还有有caller就代表是函数吗?

这个不一定,arguments对象里边也有caller,我们也可以给个普通对象添加个caller属性

话说,函数执行的目的是啥?

简单来说,就是把哪些字符串代码给执行一遍

既然执行,就得跑到ECStack里边去执行

而每一个函数的执行都会形成一个全新的执行上下文

注意:JS是单线程的,这意味着一次只能执行一个东西,即一段时间内只能处理一件事,不能边吃饭边看电视

回过头来的,我们的代码是这样的:

/*360面试题*/
let x = [12, 23];
function fn(y) {
    y[0] = 100;
    y = [100];
    y[1] = 200;
    console.log(y);
}
fn(x);
console.log(x);

代码在执行到一半的时候,就得跑去执行fn这个函数里边的代码了

此时全局有一个console.log(x);还没有执行,而JS是单线程的,不能同时处理函数里边的代码以及全局剩下的 console.log(x);,所以这个「全局」上下文就被压缩到ECStack的底部了

总之,ECStack有新的东西进来,那么旧的还有剩余代码未被执行的,就被压缩到了栈的底部

压缩到底部

我们知道每个执行上下文里边都有一个存放变量的这么一个对象

在全局上下文里边,我们叫「VO」

但在函数这个执行上下文里边,我们则是叫「AO」(Activation Object) -> 活动对象

那么什么是AO呢? -> 简单理解函数形成的变量对象就叫「AO」,而全局形成的那个则是「VO」

总之,不管是AO还是VO,它们都是变量对象

回过头来,要执行这个fn(0x101)了,在执行它之前,需要init这么几件事(讲几个常用的):

  1. init arguments 这个内置的实参集合 -> arguments:{0:0x101}
  2. 创建形参变量,并给它赋值 -> y = 0x101 -> 变量提升不需要考虑了,因为ES6的出现已经把它给GG了
  3. 代码执行

注意:

第一步和第二步,在非严格模式下,会建立映射机制(后边会有题让你了解这一点),而在严格模式下则不会,而且ES6新特性箭头函数中咩有arguments实参集合

映射机制测试:

非严格模式下:

function fn(x,y) {
  console.log(x,y,arguments);
  arguments[0] = 100;
  y = 200;
  console.log(x,y,arguments);
}
fn(10,20)

我的预测结果:

结果

实际结果:

非严格模式下,有关联

严格模式下阻断了映射机制:

阻断映射机制

非严格模式下,不传一个参数,会咋样?

只有一个参数

实参集合有一个,即传一个参数,那么arguments压根儿就不会产生第二项出来,即便你 y=200也不会产生映射,毕竟arguments的值,从「初始化实参集合」这一步就决定了有多少个参数是被建立了映射机制了,如你传一个,那么这一个就被映射,传两个,就两个被映射……

arguments看的是函数调用传了几个参数

总之,每次函数执行,都会形成一个全新的执行上下文:

  1. AO
    1. 初始化实参集合
    2. 创建形参变量并赋值

    这两个初始化完成,就会根据此时初始化的内容,在非严格模式下 -> 建立了对应的映射机制

  2. 代码自上而下执行

回到代码

我们执行代码:

  1. 局部的y和全局x是有关联的 -> y[0] = 100;
  2. 切断y与x间的关联 -> y = [100];
  3. y[1] = 200; -> 为y追加一个新元素
  4. console.log(y);

执行完之后,分析局部变量有没有被其它变量占用(局部变量占用其它变量可不算哈!如arguments某个元素可能是某个堆的地址)

都没有被占用,那么这个「fn的函数执行上下文」就得出栈

之前fn执行上下文压住了全局的执行上下文,就像一个石头压住了一弹簧,石头被拿走了,弹簧也就弹上了,然后执行这全局剩余的代码了……

有人问,如果fn的函数执行上下文不出栈呢?

全局上下文就会被拎上来 -> 弹簧和石头的位置颠倒了 -> 闭包也是如此 -> 总之谁要执行谁就上去,谁要没事儿就被压下去 -> 总之,出栈即是销毁,节省内存空间,如果有闭包,则不会销毁,只会把它压入到栈底


题外话(因老师的讲解,对闭包的认识又有了一丝新的理解):

有这样一份代码:

function fn() {
    let a = 1
    return () => {
        a++
        console.log(a)
    }
}

var z = fn()

按照我目前自己理解的闭包,并不是如老师说的那样,在我看来任何一个函数被执行完毕后都会被释放掉,如果有闭包的话,那么这个函数会带着一个「小背包」,即为这个函数的__proto__旗下的[[Scopes]]属性添加一个Closure函数 -> 但经过老师上边所说的 -> 我理解的闭包成了这个样子:

Closure接收一个指向fn的地址,从这个地址,我们全局上下文下的z关联了fn它的执行上下文里边的AOa属性:

自己对闭包的认识

当我们根据上图再次执行 z 这个函数,那么:

  1. z函数执行上下文入栈
  2. AO
    1. a -> 3
  3. a = a + 1 -> a -> 4
  4. console.log(a) -> 4

◇题三

var x = 10;
~ function (x) {
    console.log(x);
    x = x || 20 && 30 || 40;
    console.log(x);
}();
console.log(x);

解析:

题三

结果:

undefined
30
10

我突然意识到,如何画图才是真正的知识,即面对如上面所述这样类似的问题时,都用画图这种套路、模式、模型去解决它 -> 高中数学题、物理题应该都会有这种套路去套题解决吧!

◇题四

let x = [1, 2],
    y = [3, 4];
~ function (x) {
    x.push('A');
    x = x.slice(0);
    x.push('B');
    x = y;
    x.push('C');
    console.log(x, y);
}(x);
console.log(x, y);

解析:

题四

经测试,答案正确 -> 联想起「图灵机」 -> 程序的作用似乎就是把一个变量的值,或者说是一个内存数据,反复地摁在地上摩擦……所有内存数据组合起来的状态,在此时此刻此分,形成了我们所看到的程序所运行的样子 -> 这一切都是对数据处理,然后输出……

◇题五

let res = parseFloat('left:200px');
if(res===200){
   alert(200);
}else if(res===NaN){
   alert(NaN);
}else if(typeof res==='number'){
   alert('number');
}else{
   alert('Invalid Number');
}

解析:

考察的是 NaN这个东西吧! -> 结果 alert('number');

测试正确……

◇老师解析

1、题三

题三

  1. EC(G)
  2. VO
    1. x=10
  3. 自执行函数(创建+执行)
    1. 创建 -> 开辟堆内存 -> 代码字符串+键值对
    2. 入栈执行
    3. EC(自执行函数)
      1. AO
        1. 没有传参数 -> 所以 arguments = {};
        2. 形参变量列表 -> x=undefined
      2. 函数代码执行
  4. 自执行函数 -> 出栈
  5. 检查目前变量的状态……

知识点:

我在做的时候我是先算||的,最后才算 &&的,虽然最后结果没错,但分析过程是错的……

在真实项目中,「逻辑或」与「逻辑与」的应用场景:

A写了一个带一参数的函数function fn(x) {},B不传参直接调用 -> fn()

面对B对一个函数有参但不传的这种情况,我们有三种姿势可以处理:

// ES6赋值初始值
function fn(x=0) {}

// 传统方式
// 判断大法 or 逻辑或
function fn(x) {
  if(typeof x === 'undefined') {
    x = 0;
  }
  // or
  x = x || 0
}

说白了,我们用 || 来兜底哈!即0x的兜底值……

&&的应用场景:

在封装插件的时候经常会用到:

// 用户不传函数:
fn && fn()

// 一种相较于上边这种比较严谨的方式,当然一般会用上边这种,毕竟简单哈!

typeof fn === 'function' ? fn() : null;

用户不传函数,那就是undefined,既然是undefined,那就不执行fn呗,这样也就不会出现报错的情况了,反之,如果传了一个函数,那就执行呗!

总之,我们在用 fn && fn() 这种姿势的时候,是默认用户,只要传了这参数,那就是函数,而不是其它的如'hi'这样返回true的值(truly),所以说这种姿势挺不严谨的:

function fn(callback) {
  // 默认认为传的就是函数,要不然则不传
  callback && callback();
}

知识点:

函数执行上下文里边,无外乎只有两种变量:

  1. 形参
  2. 在函数体里边声明的变量

而所有在变量对象(AO)中的变量都是私有的


2、题四

题四


知识点:

疑问 -> let有优先级吗?

  1. let x = 20
  2. y = 20

还是

  1. y = 20 -> 返回值20
  2. let x = (y = 20)

想了想,我觉得是这样一种解释比较合理:

  1. let x
  2. y = 20 -> 返回20
  3. y = 20的返回值赋给 x

而不是:

  1. let x = 20;
  2. y = 20;

当然,这结果,并没有区别……


知识点:

图中那个橙色的是要被回收的,因为咩有变量引用它了

思考题:

  1. 内存泄漏怎么来的?(参考资料「红宝书」)

3、题五

★总结

★Q&A

1)连续赋值?

如:

a = b = c = [1,2,3]

猜测执行顺序:

  1. 创建一个数组,给个地址
  2. a = 地址
  3. b = 地址
  4. c = 地址

同样等级下,赋值运算的优先级从左到右执行 -> 而且一般都是这样的,除非不同等级,当然,也有从右向左的,如这样 typeof typeof 'hi'

运算符的优先级决定了表达式中运算执行的先后顺序,优先级高的运算符最先被执行。 -> 总之,优先级较高的运算符成为优先级较低的运算符的操作数

我看到MDN是这样描述的:

赋值操作符是右关联的(right-associative),所以你可以写:

a = b = 5; // same as writing a = (b = 5);

所以,难道如果是引用值的话,就是从左到右?毕竟这符合我们所期望的效果呀!

又或者确实如MDN所说:

a = b = c = [1,2,3] // (a = (b = (c = [1,2,3])))

点的level

所以 b.m 的优先级要高于 =

let a = {
    n: 10
};
let b = a;
b.m = b = {
    n: 20
};
console.log(a);
console.log(b);

因此,正确的执行过程是这样的:

  1. b.m = {n:20}
  2. b = {n:20}

那么老师的理解是错的咯!虽然执行过程确是这样,但是这理解是错的,即认为多个连续=是从左往右执行的,殊不知是因为 b.m的特殊性,即优先级更高

我开始明白为啥王垠大佬说(Schema的语法):

把所有的结构都用括号括起来,轻松地避免了别的语言里面可能发生的“歧义”。程序员不再需要记忆任何“运算符优先级”

➹:Operator precedence - JavaScript - MDN

➹:谈语法

2)ECStack 和 EC 的区别?

想想图灵机:

图灵机

➹:第 2 章 图灵的计算王国-图灵社区

3)关于自执行函数,这种`(()=>{})()`与这种 `~function(){}()`的区别?

前者赋值给一个变量是有返回值的,而后者是会发生奇怪的事儿!

自执行函数

4)函数变量与let、var声明的变量一样吗?

本质是一样的,只是函数变量存储的这个值是个函数类型的值哈!

5)`use strict;`的书写位置?

可不是乱写的,要么写在全局代码的第一行,要么写在函数代码的第一行(只有这个函数是严格模式,其它的则是非严格模式),不然,会有问题的!

6)解题模型

➹:2020高考物理解题模型,拿回去学习,物理不下90!转需 - 知乎

➹:高中数学:数学4种解题模型及解法,你清楚吗? - 知乎

➹:80页高中物理解题模型详解,清北学霸总结,带你2020高考逆袭 - 知乎

7)VO 与 GO?

VO是变量对象,只有全局对象才叫GO

全局下的变量对象叫VO

变量对象和全局对象是不一样的,简单理解全局对象是那个 window哈,而VO则是存放变量的地方

而函数里边存放变量的地方则叫「AO」

自执行函数咩有变量引它,所以不需要放在VO里边