2020年5月21日
/*阿里面试题*/
let a = {
n: 10
};
let b = a;
b.m = b = {
n: 20
};
console.log(a);
console.log(b);
画图得结果:
关键点,连等操作,右左中……
程序实际执行结果:
疑问:
如果出现多个连续赋值操作呢?那这执行顺序又是咋样的呢?
/*360面试题*/
let x = [12, 23];
function fn(y) {
y[0] = 100;
y = [100];
y[1] = 200;
console.log(y);
}
fn(x);
console.log(x);
画图得结果:
实际结果:
正确分析:
你要在栈中搞个局部变量哈!
题一:
要让全局代码执行 -> 需要先创建一个全局执行上下文 -> 为啥要这样做呢? -> 这是为了防止冲突呀(想象成把一个细胞扔到一个地方去执行):
GO = { setTimeout: ()=>{}……}
-> 有很多内置的对象 -> 我们的window对象就是指向这个GO的 -> 细节:在全局下用let声明的变量,同样是全局变量,只是没有放到GO旗下罢了……只要代码执行就会创建一个执行上下文 -> 这是浏览器V8底层帮我们干的事儿
对比自己的认识,我没有意识到是把一个东西扔到ECStack里边去执行
题二:
普通对象、数组在堆里存储的是键值对,而函数则是存两个东西:
'y[0] = 100';……
length:1
-> 这个函数的形参个数为1name:'fn'
prototype:0x101
-> 面向对象常用__proto__:0x102
……
代码字符串?我们写的整个js文件就是代码字符串,你把函数里边的代码也可以看作是一个小型的js文件
有人问,为啥没有看到代码字符串?
那是因为,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这么几件事(讲几个常用的):
arguments
这个内置的实参集合 -> arguments:{0:0x101}
y = 0x101
-> 变量提升不需要考虑了,因为ES6的出现已经把它给GG了注意:
第一步和第二步,在非严格模式下,会建立映射机制(后边会有题让你了解这一点),而在严格模式下则不会,而且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的值,从「初始化实参集合」这一步就决定了有多少个参数是被建立了映射机制了,如你传一个,那么这一个就被映射,传两个,就两个被映射……
总之,每次函数执行,都会形成一个全新的执行上下文:
这两个初始化完成,就会根据此时初始化的内容,在非严格模式下 -> 建立了对应的映射机制
回到代码
我们执行代码:
y[0] = 100;
y = [100];
y[1] = 200;
-> 为y
追加一个新元素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它的执行上下文里边的AO
的a
属性:
当我们根据上图再次执行 z
这个函数,那么:
z
函数执行上下文入栈a -> 3
a = a + 1
-> a -> 4
console.log(a)
-> 4var 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');
测试正确……
arguments = {};
x=undefined
知识点:
A||B
(一个行,就不用看后边了)
A&&B
(一个行,看后边,一个不行,GG)
&&
优先级高于 ||
-> 6 level vs 5 level -> 从左到右我在做的时候我是先算
||
的,最后才算&&
的,虽然最后结果没错,但分析过程是错的……
在真实项目中,「逻辑或」与「逻辑与」的应用场景:
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
}
说白了,我们用 ||
来兜底哈!即0
是x
的兜底值……
&&
的应用场景:
在封装插件的时候经常会用到:
// 用户不传函数:
fn && fn()
// 一种相较于上边这种比较严谨的方式,当然一般会用上边这种,毕竟简单哈!
typeof fn === 'function' ? fn() : null;
用户不传函数,那就是undefined,既然是undefined,那就不执行fn呗,这样也就不会出现报错的情况了,反之,如果传了一个函数,那就执行呗!
总之,我们在用 fn && fn()
这种姿势的时候,是默认用户,只要传了这参数,那就是函数,而不是其它的如'hi'
这样返回true
的值(truly),所以说这种姿势挺不严谨的:
function fn(callback) {
// 默认认为传的就是函数,要不然则不传
callback && callback();
}
知识点:
函数执行上下文里边,无外乎只有两种变量:
而所有在变量对象(AO)中的变量都是私有的
知识点:
let x = [1,2] , y = [3,4];
<=> let x;let y;
(简单起见,没有赋值)let x = y = 20;
<=> let x; y;
疑问 -> let有优先级吗?
是
let x = 20
y = 20
还是
y = 20
-> 返回值20
let x = (y = 20)
想了想,我觉得是这样一种解释比较合理:
let x
y = 20
-> 返回20
y = 20
的返回值赋给 x
而不是:
let x = 20;
y = 20;
当然,这结果,并没有区别……
知识点:
图中那个橙色的是要被回收的,因为咩有变量引用它了
思考题:
NaN
不等于自己如:
a = b = c = [1,2,3]
猜测执行顺序:
同样等级下,赋值运算的优先级从左到右执行 -> 而且一般都是这样的,除非不同等级,当然,也有从右向左的,如这样
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])))
所以 b.m
的优先级要高于 =
:
let a = {
n: 10
};
let b = a;
b.m = b = {
n: 20
};
console.log(a);
console.log(b);
因此,正确的执行过程是这样的:
b.m = {n:20}
b = {n:20}
那么老师的理解是错的咯!虽然执行过程确是这样,但是这理解是错的,即认为多个连续=
是从左往右执行的,殊不知是因为 b.m
的特殊性,即优先级更高
我开始明白为啥王垠大佬说(Schema的语法):
把所有的结构都用括号括起来,轻松地避免了别的语言里面可能发生的“歧义”。程序员不再需要记忆任何“运算符优先级”
➹:Operator precedence - JavaScript - MDN
➹:谈语法
想想图灵机:
前者赋值给一个变量是有返回值的,而后者是会发生奇怪的事儿!
本质是一样的,只是函数变量存储的这个值是个函数类型的值哈!
可不是乱写的,要么写在全局代码的第一行,要么写在函数代码的第一行(只有这个函数是严格模式,其它的则是非严格模式),不然,会有问题的!
➹:2020高考物理解题模型,拿回去学习,物理不下90!转需 - 知乎
➹:80页高中物理解题模型详解,清北学霸总结,带你2020高考逆袭 - 知乎
VO是变量对象,只有全局对象才叫GO
全局下的变量对象叫VO
变量对象和全局对象是不一样的,简单理解全局对象是那个 window
哈,而VO则是存放变量的地方
而函数里边存放变量的地方则叫「AO」
自执行函数咩有变量引它,所以不需要放在VO里边