综合面试题
函数提升变量提升
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);
解析:
(function(){} )()
,此时其实创建了闭包。- 函数提升 和变量提升,函数的提升要比变量提升的更前。
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
- v,k 是属性,并不是形参。
- 函数内的属性 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
- 首先在主协程中初始化异步函数 foo 和 bar,碰到
console.log
打印 script start - 解析到 setTimeout,初始化一个 Timer,创建一个新的 task
- 执行 bar 函数,将控制权交给协程,输出 bar start,碰到 await,执行 foo,输出 foo,创建一个 Promise 返回给主协程。可以把 await 看成是让出线程的标志。
- 将返回的 promise 添加到微任务队列,向下执行 new Promise,输出 promise executor,返回 resolve 添加到微任务队列
- 输出 script end
- 当前 task 结束之前检查微任务队列,执行第一个微任务,将控制器交给协程输出 bar end
- 执行第二个微任务,输出 promise then
- 当前任务执行完毕进入下一个任务,输出 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