90%面试都不会问的题,因为...

把话说完:90%面试官都不会问的题,因为面试官也不一定懂。:rabbit:

直接来看一看今天要说的题目:

  1. // 问题:foo.x的值是什么?bar.x?
  2. var foo = { n: 1 };
  3. var bar = foo;
  4. foo.x = foo = { n: 2 };
  5. console.log(foo.x) // ?
  6. console.log(bar.x) // ?
  7. // 问题:下面两题的结果是?
  8. (function(x, f = () => x) {
  9. var x;
  10. var y = x;
  11. x = 2;
  12. return [x, y, f()]; // ?
  13. })(1)
  14. (function(x, f = () => x) {
  15. var y = x;
  16. x = 2;
  17. return [x, y, f()]; // ?
  18. })(1)

这两题是我最近在一个讨论群里看到的,发出来的时候还是引起了大家非常热烈的讨论。不过大家最后都觉得这种题目没有什么意义,实际做项目的时候不会也不建议这么写(ps: 我要是在项目发现谁这样写,直接从三楼丢下去),不过从学习的角度其实还是可以研究一下的。

:snail: :snail: :snail::snail: :snail: :snail: :snail:

第一个问题:

  1. // 问题:foo.x的值是什么?bar.x?
  2. var foo = { n: 1 };
  3. var bar = foo;
  4. foo.x = foo = { n: 2 };
  5. console.log(foo.x) // ?
  6. console.log(bar.x) // ?

我一看到这个问题,第一个反应的结果是:

  1. foo.x = {n: 2};
  2. bar.x = {n: 2};

当时我的内心独白是这样的: So easy !! 这种题也有什么好问的!
然而结果是:

  1. bar.x = {n: 2};
  2. foo.x = undefined;

Why!!!!!!?????? 我表示很郁闷:see_no_evil: :see_no_evil: :see_no_evil: :see_no_evil:

然后我果断去调研了一番,下面大概总结一下~
针对这题其实要明白两点:

  1. 对于对象赋值,传递的都是引用,都是引用调用
  2. 对于赋值语句,总是先对lhs求值,再对rhs求值,然后PutValue。

可以参考一下ECMAScript标准,下面来看一下上面代码的执行。

1.第一第二行代码很简单,就是把一个对象({n: 1})赋给 foo, 然后通过 foo 再把对象赋值给 bar。这时候 bar 和 foo 存的都是对象 {n: 1} 的引用。

Ps: 这里图片有误,应该是 {n: 1}。图就不改了,大家明白意思就行。:see_no_evil:

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的引用。

到这里整个赋值过程就完成了。

第二个问题:

  1. // 问题:下面两题的结果是?
  2. (function(x, f = () => x) {
  3. var x;
  4. var y = x;
  5. x = 2;
  6. return [x, y, f()]; // [2, 1, 1]
  7. })(1)
  8. (function(x, f = () => x) {
  9. var y = x;
  10. x = 2;
  11. return [x, y, f()]; // [2, 2, 1]
  12. })(1)

对于这个问题,第二个函数相信大家都不会有啥疑问。应该集中在第一个上。

要理解这题也需要明白两个点:

1.函数体内和函数体外是两个不同的命名空间或者说作用域,函数体外的作用域是不能访问函数体内的变量的。函数的形参(x, f) 和 函数体 { } 就是两个不同的作用域。

  1. (function(a, f = () => x) {
  2. var x = 2;
  3. return [ a, f()];
  4. })(1) // Uncaught ReferenceError: x is not defined

2.函数中的默认参数可用于后面的默认参数(已经遇到的参数可用于以后的默认参数)

怎么理解 【函数中的默认参数可用于后面的默认参数(已经遇到的参数可用于以后的默认参数)】,看下面的例子:

  1. function singularAutoPlural(singular, plural = singular+"s", rallyingCry = plural + " ATTACK!!!") {
  2. return [singular, plural, rallyingCry ];
  3. }
  4. //["Gecko","Geckos", "Geckos ATTACK!!!"]
  5. singularAutoPlural("Gecko");
  6. //["Fox","Foxes", "Foxes ATTACK!!!"]
  7. singularAutoPlural("Fox","Foxes");
  8. //["Deer", "Deer", "Deer ... change."]
  9. singularAutoPlural("Deer", "Deer", "Deer peaceably and respectfully
  10. petition the government for positive change.")

Demo来自MDN

看懂了这个,接下来就直接来解释一下这个题目~

  1. (function(x, f = () => x) { // 首先这里给参数 f 默认赋值了一个匿名函数,根据我们之前说的第二个知识点这里的 x 就是形参 x。由于作用域的关系 函数f 是不能访问到函数内的 x 的。
  2. var x; // !!! 注意,这里进行了变量声明,会分配新的内存地址。但是因为只进行了声明而没有赋值,所以在作用域链还会找到 形参x
  3. var y = x; // 这里 y 的值取的还是形参 x 的值
  4. x = 2; // 这里 对上面的 var x 进行赋值而形参x 的值是不受影响的所以 f() 返回是1,此时作用域链上会先找到函数内声明的 x。
  5. return [x, y, f()]; // [2, 1, 1]
  6. })(1)
  7. (function(x, f = () => x) {
  8. var y = x; // 这里只声明了y, x 还是形参x
  9. x = 2; // 这里改变了形参x的值,所以 f() 返回是 2
  10. return [x, y, f()]; // [2, 1, 2]
  11. })(1)

这题还有一个坑点,我拿到babel里面去转一下得到的结果是

  1. "use strict";
  2. (function (x) {
  3. var f = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {
  4. return x;
  5. };
  6. var x;
  7. var y = x;
  8. x = 2;
  9. return [x, y, f()]; // !!! 这里结果是 [2, 1, 2]
  10. })(1);

这种神奇的代码还是尽量不要写呀!

如果有理解错误的地方,欢迎指正!:octocat:

写于 2018年3月30日面试 Web Javascript 1756

如非特别注明,文章皆为原创。

转载请注明出处: https://www.liayal.com/article/5abde53da6cf4e67bc05c9ea

你不想说点啥么?
😀😃😄😁😆😅😂🤣☺️😊😇🙂🙃😉😌😍😘😗😙😚😋😜😝😛🤑🤗🤓😎🤡🤠😏😒😞😔😟😕🙁☹️😣😖😫😩😤😠😡😶😐😑😯😦😧😮😲😵😳😱😨😰😢😥🤤😭😓😪😴🙄🤔🤥😬🤐🤢🤧😷🤒🤕😈👿👹👺💩👻💀☠️👽👾🤖🎃😺😸😹😻😼😽🙀😿😾👐👐🏻👐🏼👐🏽👐🏾👐🏿🙌🙌🏻🙌🏼🙌🏽🙌🏾🙌🏿👏👏🏻👏🏼👏🏽👏🏾👏🏿🙏🙏🏻🙏🏼🙏🏽🙏🏾🙏🏿🤝👍👍🏻👍🏼👍🏽👍🏾👍🏿👎👎🏻👎🏼👎🏽👎🏾👎🏿👊👊🏻👊🏼👊🏽👊🏾👊🏿✊🏻✊🏼✊🏽✊🏾✊🏿

评论

钟灵04-10 18:18

我想说,写出这种代码的我才不会让他面试过了

IrisFor04-10 10:26
@vbyzc: 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); 楼主认为如何

但是这样讲就没有解释到bar的变化,我感觉就是就是改变了bar指向的地址A的内容,而foo指向了另一个地址B,bar.x也指向了B

记小栈04-09 14:53
@qwj: 妹的!这种东西也少看,看多了我怕自己都混乱了,本来好好的逻辑被这些东西给搞混乱了!特别是第二个函数这个题目,第一题还能理解,第二题没作者你的分享还真不知道!!!

这种题目确实没啥实际的意义,当作学习来看看就好😶

qwj04-09 13:57

妹的!这种东西也少看,看多了我怕自己都混乱了,本来好好的逻辑被这些东西给搞混乱了!特别是第二个函数这个题目,第一题还能理解,第二题没作者你的分享还真不知道!!!

记小栈04-05 23:27
@lulu: 很好,很细致👍

😁😁😁😁😁

记小栈04-05 23:26
@vbyzc: 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); 楼主认为如何

哥们儿简单粗暴,在理解上是个好办法👍🏻。我的本意是让大家了解整个赋值语句的执行过程😁

vbyzc04-04 16:23

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 18:45

很好,很细致👍