跳到主要内容

综合面试题

函数提升变量提升

void (function() {
console.log(a);
a();
var a = function() {
console.log(1);
};
function a() {
console.log(2);
}
console.log(a);
a();
var c = (d = a);
})();
console.log(d);
console.log(c);
// 结果如下
// ƒ a() {
// console.log(2);
// }
// 2
// ƒ () {
// console.log(1);
// }
// 1
// ƒ () {
// console.log(1);
// }
// Uncaught ReferenceError: c is not defined

补充隐藏代码及调整顺序后,等效于以下代码:

var d;
void (function() {
function a() {
console.log(2);
}
console.log(a);
a();
var a = function() {
console.log(1);
};
console.log(a);
a();
var c = (d = a);
})();
console.log(d);
console.log(c);

解析:

  1. (function(){} )(),此时其实创建了闭包。
  2. 函数提升和变量提升,函数的提升要比变量提升的更前。
  3. var c = (d = a)。实际是 d = a,var c = d;,d 是全局变量,c 只存在于函数内,所以函数外 c 是 undefined

作用域传值

function test1(m) {
m = { v: 5 };
}
var m = { k: 30 };
test1(m);
console.log(m.v); // undefined

function test2(n) {
a = { v: 5 };
}
var n = { k: 30 };
test2(n);
console.log(a.v); // 5
  1. v,k 是属性,并不是形参。
  2. 函数内的属性 m 会被传入的 m 覆盖,因为参数名相同,不同名则不会覆盖。
var a = 1;
function foo(a) {
console.log(this.a === a);
a = a * 10;
}
foo(a);
console.log(a);
var a = 1;
function foo(a) {
console.log(this.a === a);
this.a = a * 10; // 变动点,this.a
}
foo(a);
console.log(a);
function f1(a = 9) {
console.log(a);
a = function a() {};
console.log(a);
var a = 1;
console.log(a);
}
f1();

变量交换

如何不用临时变量,进行两个值的交换?

var foo = 1,
bar = 2;
var foo = 1,
bar = 2;
foo = [bar, (bar = foo)][0];
console.log(foo);
console.log(bar);

// ES6之后
[foo, bar] = [bar, foo];

ES6 之前,既然不能用临时变量,那么就需要把两个变量的值合到一个变量,然后再处理。

优先级,变量赋值

运算符的优先级决定了表达式中运算执行的先后顺序,优先级高的运算符最先被执行。全新对象赋值是新开 heap,对对象属性赋值则继续指向旧有的 heap。

说实话,有些不常用的也确实会忘记,但是常用的不要忘记。

var a = { n: 1 };
var b = a;
a.x = a = { n: 2 };
console.log(a.x); // --> undefined
console.log(b.x); // --> {n:2}

来自网络上的解析答案:

var a = {n: 1}; // 对象类型,a指向一个内存地址,也即 heap(堆)
var b = a; // b = {n: 1},a,b 指向同一个内存地址,此时都是{n: 1}
a.x = a = {n: 2};
// `.` 的执行优先级是高于 `=` 操作的,一开始 `a.x` 就指向了 `{n: 1}` 的 heap 空间,这个空间里并没有x属性。
// 等效于如下
a.x = (a = {n: 2});
// 此时a被赋值了一个新的对象{n: 2},开辟了新的内存地址(此时这个是一个新的“a”)
// 而a.x(包括引用的b)则还是继续指向旧的内存地址,也被赋值{n:2}
// 最终的结果如下:
a = {n: 2}
b = {
n: 1
x: {
n: 2
}
}
// 故 a.x --> undefined, b.x --> {n:2}

另一种解析方案(深入底层):

// 参照说明
var x = (y = 100);
// 这里的 x 是一个标识符(不是表达式),而 y 和 100 都是表达式,且y = 100是一个赋值表达式。
a.x = a = { n: 2 };

a.x 是一个表达式,而 a = {n:2} 也是表达式,并且后者的每一个操作数(本质上)也都是表达式。“语句与表达式”是不同的。var x 从来都不进行计算求值,所以也就不能写成 var a.x …

此处是真正的、两个连续赋值的表达式。按照之前的理解,a.x总是最先被计算求值的(从左至右)。

“a.x”这个表达式的语义是:

  • 计算单值表达式 a,得到 a 的引用;
  • 将右侧的名字 x 理解为一个标识符,并作为“.”运算的右操作数;
  • 计算“a.x”表达式的结果(Result)。表达式“a.x”的计算结果是一个引用(Reference),从代码中可见,保存在“a.x”这个引用中的“a”是当前的“{n:1}”这个对象。
var a = { n: 1 };
a.x = a = // <- `a` is {n:1},这里用 a0 表示
// <- `a` is {n:1}
{ n: 2 }; // 赋值,覆盖当前的左操作数(变量`a`)。这里真实地发生了一次a = {n:2},此时变量a产生了新的值,这里用a1表示。

那么剩下的问题就是回到了表达式最开始被保留在“一个结果值(Result)”中的引用 a 会更新吗?换句话说就是这个 a 指向的是 a0 还是 a1?

答案是不会的,也就是继续指向 a0。这是因为那是一个“运算结果(Result)”,这个结果有且仅有引擎知道,它现在是一个引擎才理解的“引用(规范对象)”,对于它的可能操作只有:取值或置值(GetValue/PutValue),以及作为一个引用向别的地方传递等。

现在,在整个语句行的最左侧“空悬”了一个已经求值过的“a.x”。当它作为赋值表达式的左操作数时,它是一个被赋值的引用(这里是指将 a.x 的整体作为一个引用规范对象)。而它作为结果(Result)所保留的“a”,是在被第一次赋值操作覆盖之前的、那个“原始的变量 a”,即 a0。也就是说,如果你试图访问它的“a.n”,那应该是值“1”。

小结:

  • 有一个新的 a 产生,它覆盖了原始的变量 a,它的值是{n:2}
  • 最左侧的“a.x”的计算结果中的“原始的变量 a”在引用传递的过程中丢失了,且“a.x”被同时丢弃。

所以,第二次赋值操作 a.x = … 实际是无意义的。因为它所操作的对象,也就是“原始的变量 a”(即 a0)被废弃了。但是,如果有其它的东西,如变量、属性或者闭包等,持有了这个“原始的变量 a”,那么上面的代码的影响仍然是可见的。

PS:严格地说,并不存在连续赋值运算,因为 var x = … 是值绑定操作,而不是“将…赋值给 x”。在代码var x = y = 100;中实际只存在一个赋值运算,那就是 y = 100

常规对比:

var a = { n: 1 };
var b = a;
a = { n: 2 };
console.log(a, b); // --> {n: 2} {n: 1}
var a = { n: 1 };
var b = a;
var c = a;
a = { n: 2 };
c.a = { n: 3 };
console.log(a, b, c); // --> {n: 2}, {a: {n: 3}, n: 1}, {a: {n: 3}, n: 1}

题目补充:

  • 这道面试题与运算符优先级无关;
  • 这里的运算过程与“栈”操作无关;
  • 这里的“引用”与传统语言中的“指针”没有可比性;
  • 这里没有变量泄露;
  • 这行代码与上一讲的例子有本质的不同;
  • 上一讲的例子 var x = y = 100 严格说来并不是连续赋值。

执行顺序

async function foo() {
console.log("foo");
}
async function bar() {
console.log("bar start");
await foo();
console.log("bar end");
}
console.log("script start");
setTimeout(function() {
console.log("setTimeout");
}, 0);
bar();
new Promise(function(resolve) {
console.log("promise executor");
resolve();
}).then(function() {
console.log("promise then");
});
console.log("script end");

// 执行结果如下
// script start
// bar start
// foo
// promise executor
// script end
// bar end
// promise then
// setTimeout
  1. 首先在主协程中初始化异步函数 foo 和 bar,碰到console.log打印 script start
  2. 解析到 setTimeout,初始化一个 Timer,创建一个新的 task
  3. 执行 bar 函数,将控制权交给协程,输出 bar start,碰到 await,执行 foo,输出 foo,创建一个 Promise 返回给主协程。可以把 await 看成是让出线程的标志。
  4. 将返回的 promise 添加到微任务队列,向下执行 new Promise,输出 promise executor,返回 resolve 添加到微任务队列
  5. 输出 script end
  6. 当前 task 结束之前检查微任务队列,执行第一个微任务,将控制器交给协程输出 bar end
  7. 执行第二个微任务,输出 promise then
  8. 当前任务执行完毕进入下一个任务,输出 setTimeout
// bar() 其实等效于
new Promise((resolve, reject) => {
console.log("bar start");
console.log("foo");
// Promise.resolve() 将代码插入微任务队列尾部
// resolve 再次插入微任务队列尾部
resolve(Promise.resolve());
}).then(() => {
console.log("bar end");
});

Node 队列任务

async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}

async function async2() {
console.log('async2')
}

console.log('script start')

setTimeout(function () {
console.log('setTimeout0')
}, 0)

setTimeout(function () {
console.log('setTimeout2')
}, 300)

setImmediate(() => console.log('setImmediate'));

process.nextTick(() => console.log('nextTick1'));

async1();

process.nextTick(() => console.log('nextTick2'));

new Promise(function (resolve) {
console.log('promise1')
resolve();
console.log('promise2')
}).then(function () {
console.log('promise3')
})

console.log('script end')


// script start
// async1 start
// async2
// promise1
// promise2
// script end
// nextTick1
// nextTick2
// async1 end
// promise3
// setTimeout0
// setImmediate
// setTimeout2