把话说完:90%面试官都不会问的题,因为面试官也不一定懂。
直接来看一看今天要说的题目:
// 问题:foo.x的值是什么?bar.x?
var foo = { n: 1 };
var bar = foo;
foo.x = foo = { n: 2 };
console.log(foo.x) // ?
console.log(bar.x) // ?
// 问题:下面两题的结果是?
(function(x, f = () => x) {
var x;
var y = x;
x = 2;
return [x, y, f()]; // ?
})(1)
(function(x, f = () => x) {
var y = x;
x = 2;
return [x, y, f()]; // ?
})(1)
这两题是我最近在一个讨论群里看到的,发出来的时候还是引起了大家非常热烈的讨论。不过大家最后都觉得这种题目没有什么意义,实际做项目的时候不会也不建议这么写(ps: 我要是在项目发现谁这样写,直接从三楼丢下去),不过从学习的角度其实还是可以研究一下的。
第一个问题:
// 问题:foo.x的值是什么?bar.x?
var foo = { n: 1 };
var bar = foo;
foo.x = foo = { n: 2 };
console.log(foo.x) // ?
console.log(bar.x) // ?
我一看到这个问题,第一个反应的结果是:
foo.x = {n: 2};
bar.x = {n: 2};
当时我的内心独白是这样的: So easy !! 这种题也有什么好问的!
然而结果是:
bar.x = {n: 2};
foo.x = undefined;
Why!!!!!!?????? 我表示很郁闷
然后我果断去调研了一番,下面大概总结一下~
针对这题其实要明白两点:
- 对于对象赋值,传递的都是引用,都是引用调用
- 对于赋值语句,总是先对lhs求值,再对rhs求值,然后PutValue。
可以参考一下ECMAScript标准,下面来看一下上面代码的执行。
1.第一第二行代码很简单,就是把一个对象({n: 1})赋给 foo, 然后通过 foo 再把对象赋值给 bar。这时候 bar 和 foo 存的都是对象 {n: 1} 的引用。
Ps: 这里图片有误,应该是 {n: 1}。图就不改了,大家明白意思就行。
2.接下来重点看 foo.x = foo = { n: 2 }
。我们就按照 [ 对于赋值语句,总是先对lhs求值,再对rhs求值,然后PutValue。 ] 来解析这行代码。
第一步,首先对 foo.x 进行求值,foo 指向的是对象 { n: 2 }(下面称为:ObjectF ), ObjectF 没有属性 x ,那么为 ObjectF 添加属性x,左值的求值结果就是对刚才添加的属性 x 的引用(某个内存地址X)。
Ps: 这里也是,应该是 {n: 1, x: … }。
第二步, 对右值进行求值,右值是 foo = {n : 2}。递归向下,先对左值求值,得到 foo,foo 还是 ObjectF 引用,然后对右值{a : 2}求值,得到 ObjectE ,接着PutValue将改变 foo 的指向到 ObjectE,赋值表达式foo = {n : 2}返回得到 ObjectE引用。
这个时候 foo 和 ObjectF 已经解绑,而且重新指向了 ObjectE,ObjectE上没有 x 这个属性,所以 foo.x 这个时候是undefined。
第三步, PutValue将左值指向 ObjectE,也就是说第一步中的内存地址X存的是ObjectE的引用。
到这里整个赋值过程就完成了。
第二个问题:
// 问题:下面两题的结果是?
(function(x, f = () => x) {
var x;
var y = x;
x = 2;
return [x, y, f()]; // [2, 1, 1]
})(1)
(function(x, f = () => x) {
var y = x;
x = 2;
return [x, y, f()]; // [2, 2, 1]
})(1)
对于这个问题,第二个函数相信大家都不会有啥疑问。应该集中在第一个上。
要理解这题也需要明白两个点:
1.函数体内和函数体外是两个不同的命名空间或者说作用域,函数体外的作用域是不能访问函数体内的变量的。函数的形参(x, f) 和 函数体 { } 就是两个不同的作用域。
(function(a, f = () => x) {
var x = 2;
return [ a, f()];
})(1) // Uncaught ReferenceError: x is not defined
2.函数中的默认参数可用于后面的默认参数(已经遇到的参数可用于以后的默认参数)。
怎么理解 【函数中的默认参数可用于后面的默认参数(已经遇到的参数可用于以后的默认参数)】,看下面的例子:
function singularAutoPlural(singular, plural = singular+"s", rallyingCry = plural + " ATTACK!!!") {
return [singular, plural, rallyingCry ];
}
//["Gecko","Geckos", "Geckos ATTACK!!!"]
singularAutoPlural("Gecko");
//["Fox","Foxes", "Foxes ATTACK!!!"]
singularAutoPlural("Fox","Foxes");
//["Deer", "Deer", "Deer ... change."]
singularAutoPlural("Deer", "Deer", "Deer peaceably and respectfully
petition the government for positive change.")
Demo来自MDN
看懂了这个,接下来就直接来解释一下这个题目~
(function(x, f = () => x) { // 首先这里给参数 f 默认赋值了一个匿名函数,根据我们之前说的第二个知识点这里的 x 就是形参 x。由于作用域的关系 函数f 是不能访问到函数内的 x 的。
var x; // !!! 注意,这里进行了变量声明,会分配新的内存地址。但是因为只进行了声明而没有赋值,所以在作用域链还会找到 形参x
var y = x; // 这里 y 的值取的还是形参 x 的值
x = 2; // 这里 对上面的 var x 进行赋值而形参x 的值是不受影响的所以 f() 返回是1,此时作用域链上会先找到函数内声明的 x。
return [x, y, f()]; // [2, 1, 1]
})(1)
(function(x, f = () => x) {
var y = x; // 这里只声明了y, x 还是形参x
x = 2; // 这里改变了形参x的值,所以 f() 返回是 2
return [x, y, f()]; // [2, 1, 2]
})(1)
这题还有一个坑点,我拿到babel里面去转一下得到的结果是
"use strict";
(function (x) {
var f = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {
return x;
};
var x;
var y = x;
x = 2;
return [x, y, f()]; // !!! 这里结果是 [2, 1, 2]
})(1);
这种神奇的代码还是尽量不要写呀!
如果有理解错误的地方,欢迎指正!
写于 2018年03月30日面试 Web Javascript 3362
如非特别注明,文章皆为原创。
转载请注明出处: https://www.liayal.com/article/5abde53da6cf4e67bc05c9ea
记小栈小程序上线啦~搜索【记小栈】或【点击扫码】体验
钟灵04-10 2018
我想说,写出这种代码的我才不会让他面试过了
IrisFor04-10 2018
但是这样讲就没有解释到bar的变化,我感觉就是就是改变了bar指向的地址A的内容,而foo指向了另一个地址B,bar.x也指向了B
记小栈04-09 2018
这种题目确实没啥实际的意义,当作学习来看看就好😶
qwj04-09 2018
妹的!这种东西也少看,看多了我怕自己都混乱了,本来好好的逻辑被这些东西给搞混乱了!特别是第二个函数这个题目,第一题还能理解,第二题没作者你的分享还真不知道!!!
记小栈04-05 2018
😁😁😁😁😁
记小栈04-05 2018
哥们儿简单粗暴,在理解上是个好办法👍🏻。我的本意是让大家了解整个赋值语句的执行过程😁
vbyzc04-04 2018
foo.x = foo = { n: 2 }; 这句,什么objectE又什么objectF的,你直接说=赋值是先左再右,虽然左边添加了x属性,但是右边又把整个foo重新再赋值为{n:2},这样把x属性给干掉了,所以foo.x已经不在,相法于:var foo = {n:2};foo.x='3';console.log(foo.x);foo={n:2};console.log(foo.x); 楼主认为如何
lulu04-01 2018
很好,很细致👍