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

2,850 阅读5分钟
原文链接: www.liayal.com

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

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

// 问题: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: 我要是在项目发现谁这样写,直接从三楼丢下去),不过从学习的角度其实还是可以研究一下的。

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

第一个问题:

// 问题: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!!!!!!?????? 我表示很郁闷:see_no_evil: :see_no_evil: :see_no_evil: :see_no_evil:

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

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

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

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

foo 和 bar 都指向同一内存地址

foo 和 bar 都指向同一内存地址

2.接下来重点看 foo.x = foo = { n: 2 }。我们就按照 [ 对于赋值语句,总是先对lhs求值,再对rhs求值,然后PutValue。 ] 来解析这行代码。

第一步,首先对 foo.x 进行求值,foo 指向的是对象 { n: 2 }(下面称为:ObjectF ), ObjectF 没有属性 x ,那么为 ObjectF 添加属性x,左值的求值结果就是对刚才添加的属性 x 的引用(某个内存地址X)。

添加的x属性上的值为一个内存地址

添加的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 的值是不受影响的(console.log(arguments[0])试一下, 所以 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);

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

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