# 代码题

🔒 请写出弹出值,并解释为什么。

alert(a);
a();
var a = 3;
function a() {
  alert(10);
}
alert(a);
a = 6;
a();
1
2
3
4
5
6
7
8
9

答案解析:

  • undefinednot defined 不是一个东西。undefined 说明这个变量没赋值,但是是存在的;not defined 报错,说明这个变量压根就不存在。

  • js 在预编译的时候会把所有带 var 的变量提升到函数顶端。下面就是 js 中的私有变量,在函数外部是访问不到的。

function init() {
  // 私有变量
  var a = 20;
}
init();
console.log(a); // a is not defined
1
2
3
4
5
6
  • 函数和变量都会提升,但是函数提升的优先级高于变量提升。

下面这样写,函数能够正常执行并输出 123。

test(); // 123
function test() {
  console.log(123);
}
1
2
3
4

但是变量按照这样写的话就不行了。

console.log(a);
var a = 20; // undefined
1
2

如果改成下面这样就可以了。这是因为变量还有词法作用域。

var a;
console.log(a); // 直接复制到chrome开发工具中是输出 20,但是在文件中打开输出的是 undefined
a = 20;
1
2
3
  • 匿名函数
(function() {
  console.log(123);
  debugger;
})();
1
2
3
4

上面这段代码中,有一个匿名函数,执行代码后在浏览器中可以看到堆栈调用的显示。

jstest

但是奇怪的是,明明我们只写了一个匿名函数,在堆栈中却看到有两个匿名函数。这是因为 js 在执行代码时,会像 C 和 Java 那样把所有的代码放到一个统一的主入口中去执行,在 C 和 Java 中这个入口就是 main 函数。但是在 js 中没有 main 函数,而是一个匿名函数。

因此,上面的代码其实在 js 中是包裹到这样一个匿名函数中执行的,我们才会看到有两个匿名函数。

(function() {
  (function() {
    console.log(123);
    debugger;
  })();
})();
1
2
3
4
5
6
  • 块级作用域

ES6 的 let 和 const 都具有块级作用域。

{
  a = 30;
}
console.log(a); // 30

{
  const a = 30;
}
console.log(a); // a is not defined
1
2
3
4
5
6
7
8
9

块级作用域会造成暂时性死区。

a 在 const 声明之前使用,会报错。

var a = 30
{
  a = 40
  const a
}
console.log(a) // Uncaught SyntaxError: Missing initializer in const declaration
1
2
3
4
5
6

函数没有块级作用域。

{
  function test() {}
}
console.log(test); // ƒ test () {}
1
2
3
4

下面这样写就会有问题。

{
  const test = function test() {};
}
console.log(test); // Uncaught ReferenceError: test is not defined

{
  var test = function test() {};
}
console.log(test); // ƒ test () {}
1
2
3
4
5
6
7
8
9

下面这几点也是需要注意的!

// 由于词法作用域的存在,下面的变量 shenzhen 会把函数 shenzhen 覆盖掉,输出1
function shenzhen() {}
var shenzhen = 1;
console.log(shenzhen); // 1

// 不同浏览器表现不一样,以Chrome为主
// 变量 shenzhen 没有赋值,为 undefined,浏览器会把它忽略掉,所以输出 shenzhen 这个函数
function shenzhen() {}
var shenzhen;
console.log(shenzhen); // ƒ shenzhen () {}

// 但是如果我们显示给它赋值,输出的就是 undefined 了
function shenzhen() {}
var shenzhen = undefined;
console.log(shenzhen); // undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

因此,对于这一题,我们可以对它 “翻译” 如下,就很容易得到它的运行结果:

var a;
function a() {
  alert(10);
}
alert(a); // function a() { alert(10) }
a(); // 10
a = 3;
alert(a); // 3
a = 6;
a(); // Uncaught TypeError: a is not a function
1
2
3
4
5
6
7
8
9
10

函数提升还有一有些需要注意的地方,比如下面这些例子。

function fn() {
  console.log(1);
}
// 相当于在这里有一句声明:var fn
// 但是因为在这外面还定义了另一个 fn,所以覆盖掉了
if (false) {
  // 此处函数会发生提升
  function fn() {
    console.log(2);
  }
}
console.log(fn);
/*
  ƒ fn () {
    console.log(1)
  }
*/

function fn() {
  console.log(1);
}
function init() {
  // 相当于这里其实有一句声明:var fn
  if (false) {
    // 此处函数会提升到外层函数顶端,但是提升的只是函数名,不是函数体
    function fn() {
      console.log(2);
    }
  }
  console.log(fn);
}
init(); // undefined

function fn() {
  console.log(1);
}
function init() {
  console.log(fn);
  if (false) {
    function fn() {
      console.log(2);
    }
  }
}
init(); // undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

# 代码题

🔒 下面这道题在上一题的基础上加强一下。

var x = 1,
  y = 0,
  z = 0;
function add(x) {
  return (x = x + 1);
}
y = add(x);
console.log(y);
function add(x) {
  return (x = x + 3);
}
z = add(x);
console.log(z);
1
2
3
4
5
6
7
8
9
10
11
12
13

答案解析:

对于这道题,首先来看上半部分:

var x = 1,
  y = 0,
  z = 0;
function add(x) {
  return (x = x + 1);
}
y = add(x);
console.log(y); // 2
1
2
3
4
5
6
7
8

可以看到输出 y 的值为 2。

这时候如果把下半部分还回来:

var x = 1,
  y = 0,
  z = 0;
function add(x) {
  return (x = x + 1);
}
y = add(x);
console.log(y); // 4
function add(x) {
  return (x = x + 3);
}
z = add(x);
console.log(z); // 4
1
2
3
4
5
6
7
8
9
10
11
12
13

可以看到输出 y 和 z 值都是 4。这是因为两个函数都发生了提升,并且下面的 add 函数把上面的 add 函数重写覆盖了,所以最后起作用的函数就是下面那个。需要注意的是,这里 (x = x + 1) 其实就是一句赋值语句而已,跟 x = x + 1 没什么区别,加个括号只是想干扰而已。

# 代码题

🔒 请写出如下输出值,并解释为什么。

this.a = 20;
function go() {
  console.log(this.a);
  this.a = 30;
}
go.prototype.a = 40;
var test = {
  a: 50,
  init: function(fn) {
    fn();
    console.log(this.a);
    return fn;
  }
};
console.log(new go().a);
test.init(go);
var p = test.init(go);
p();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

答案解析:

  • globalThis

现在有一个全局属性 globalThis (opens new window) 包含全局的 this 值,类似于全局对象。

JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在各种实现里面是不统一的。

浏览器里面,顶层对象是 window,但 Node 和 Web Worker 没有 window。浏览器和 Web Worker 里面,self 也指向顶层对象,但是 Node 没有 self。Node 里面,顶层对象是 global,但其他环境都不支持。

为了能够在任何情况下,都能够取到顶层对象,ES6 引入了 globalThis 作为顶层对象。在任何环境下,globalThis 都是存在的,都可以从它拿到顶层对象,指向全局环境下的 this。

  • self 属性

self (opens new window) 属性返回对窗口自身的只读引用。等价于 window (opens new window) 属性。

我们平时在保存 this 时不要写成 var self = this,因为这样会把 self 属性重写了,可以用 var that = this 这种方式。

self.self === self; // true
1

现在我们把这道题拆开来看:

this.a = 20;
var test = {
  a: 50,
  init: function(fn) {
    console.log(this.a);
  }
};
test.init(); // 50
1
2
3
4
5
6
7
8

谁调用它,this 就指向谁。在这里,test 调用了的 init 函数,因此这里的 this 指向了 test,所以输出的值为 50。

但如果这时候使用的是箭头函数,就不一样了。

this.a = 20;
var test = {
  a: 50,
  init: () => {
    console.log(this.a);
  }
};
test.init(); // 20
1
2
3
4
5
6
7
8

箭头函数会让 this 指向离它最近的外层作用域,这里就是 window。

在此基础上,再对它复杂化一些:

this.a = 20;
var test = {
  a: 50,
  init: function(fn) {
    function go() {
      console.log(this.a);
    }
    go();
  }
};
test.init(); // 20
1
2
3
4
5
6
7
8
9
10
11

这段代码中,go 函数执行的时候没有人去调用它,因此它里面的 this 指向的是 window 对象,所以输出的值为 20。

但是在严格模式下,不让 this 指向 window 对象,会报错。

"use strict";
this.a = 20;
var test = {
  a: 50,
  init: function(fn) {
    function go() {
      console.log(this.a);
    }
    go();
  }
};
test.init(); // Uncaught TypeError: Cannot read property 'a' of undefined
1
2
3
4
5
6
7
8
9
10
11
12

改成箭头函数:

this.a = 20;
var test = {
  a: 50,
  init: () => {
    function go() {
      console.log(this.a);
    }
    go();
  }
};
test.init(); // 20
1
2
3
4
5
6
7
8
9
10
11

输出还是一样

但是在严格模式下也会报错:

"use strict";
this.a = 20;
var test = {
  a: 50,
  init: () => {
    function go() {
      console.log(this.a);
    }
    go();
  }
};
test.init(); // Uncaught TypeError: Cannot read property 'a' of undefined
1
2
3
4
5
6
7
8
9
10
11
12

接下来,让我们把代码再改一下:

this.a = 20;
var test = {
  a: 50,
  init: function(fn) {
    function go() {
      console.log(this.a);
    }
    return go;
  }
};
var go = test.init();
go(); // 20
1
2
3
4
5
6
7
8
9
10
11
12

这种情况下,相当于是把 init 函数里的 go 挪到外面来执行了,这时候的 this 指向的还是 window 对象,所以还是输出 20。

继续改代码:

this.a = 20;
function go() {
  console.log(this.a);
  this.a = 30;
}
go.prototype.a = 40;
var test = {
  a: 50,
  init: function(fn) {}
};
console.log(new go().a); // 40 30
1
2
3
4
5
6
7
8
9
10
11

如果 go 函数被 new 了的话,那么 go 函数里面的 a 的优先级比 go 函数的原型链上的 a 优先级高。但是如果不 new go 函数,那么它里面的 a 一点用没有。

原型链解决的是一个属性共享、内存复用(内存中对 a 的复用)的问题,即使 new 了无数个 go,指向的都是它,只不过可能 a 的值不一样而已。

上面这段代码的输出结果是 40 和 30。其中,go 函数里面输出的值是 40,而外边 go 的实例输出的值为 30。前者是因为在找的时候,并没有 go 函数里面找到 a 的值,因为 this.a = 30 在 console.log(this.a) 后面,所以就去 go 的原型链上面找,找到了 40。而外边因为 go 被 new 了,所以 this.a = 30 的优先级高于原型链上的 a 的值,所以实例上 a 的值为 30。

再改改代码:

this.a = 20;
function go() {
  console.log(this.a);
  this.a = 30;
}
go.prototype.a = 40;
var test = {
  a: 50,
  init: function(fn) {
    fn();
    return fn;
  }
};
test.init(go); // 20
1
2
3
4
5
6
7
8
9
10
11
12
13
14

可以看到这段代码输出的值为 20。

我们用一个 x 来接收 test.init(go) 的值,并输出看看:

var x = test.init(go);
console.log(x);
/*
ƒ go () {
  console.log(this.a)
  this.a = 30
}
 */
1
2
3
4
5
6
7
8

可以看到输出出来的值为 go 函数。

让我们执行下 x :

x(); // 20 30
1

此时输出的是 20 和 30。

综合起来,这道题的输出结果就如下:

this.a = 20;
function go() {
  console.log(this.a);
  this.a = 30;
}
go.prototype.a = 40;
var test = {
  a: 50,
  init: function(fn) {
    fn();
    console.log(this.a);
    return fn;
  }
};
console.log(new go().a); // 40 30
test.init(go); // 20 50
var p = test.init(go);
p(); // 30 50 30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

最后一句输出 30 是因为倒数第二句 test.init(go) 已经执行过一遍 go 函数了,此时 a 的值已经有 30 了,所以最后 p 执行的时候才会输出 30。

# this 指向

🔒 请写出以下代码执行结果

var num = 1;

function yideng() {
  "use strict";
  console.log(this.num++);
}
function yideng2() {
  console.log(++this.num);
}
(function() {
  "use strict";
  yideng2(); // 2
})();
yideng(); // Uncaught TypeError: Cannot read property 'num' of undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14

答案解析:

严格模式只对当前的函数生效。因此 yideng2 () 里的 this 能够指向 window 对象,输出 2;但是 yideng() 里的 this 因为受到严格模式的限制,没法指向 window 对象,所以 this.num 的值为 undefined,执行运算时就报错了。

# 原型链

🔒 拓展题(请写出以下代码执行结果)

function C1(name) {
  if (name) this.name = name;
}
function C2(name) {
  this.name = name;
}
function C3(name) {
  this.name = name || "fe";
}
C1.prototype.name = "C1";
C2.prototype.name = "C2";
C3.prototype.name = "C3";
console.log(new C1().name + new C2().name + new C3().name);
1
2
3
4
5
6
7
8
9
10
11
12
13

答案解析:

C1、undefined、fe

  • 对于 C1,里面有一个判断,当没传 name 的时候,压根不会执行里面的语句,所以会去原型链上找,输出 C1;

  • 对于 C2,也没传 name,所以 name 的值为 undefined,但是注意,undefined 也是有意义的,只有为 not undefined 的时候才会去原型链上找,所以输出结果是 undefined,而不会去原型链上找;

  • 对于 C3,虽然也没传 name,但是它里面有一个短路求值,当没传 name 的时候,默认就会赋值为 fe,压根不会去原型链上找,所以输出 fe。

# JS 给 ul 下的所有 li 绑定点击事件,点击使其弹出下标和内容

🔒 请写出如下点击 li 的输出值,并用尽可能多的方法正确输出 li 里的内容和索引。

<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
  <li>6</li>
</ul>
1
2
3
4
5
6
7
8
var list_li = document.getElementsByTagName("li");
for (var i = 0; i < list_li.length; i++) {
  list_li[i].onclick = function() {
    console.log(i);
  };
}
1
2
3
4
5
6

答案解析:

现在不管你点击哪一个数字,输出的值都是 6,这显然是不对的。

为什么 js 是单线程,这是因为一开始 js 是用来操作 DOM 的,为了保证操作的简单性,因此一开始设计成了单线程。

这道题有以下几种解决方法:

  • 使用 let
var items = document.getElementsByTagName("li");
for (let i = 0; i < items.length; i++) {
  items[i].onclick = function() {
    console.log(`${items[i].innerHTML}-${i}`);
  };
}
1
2
3
4
5
6
  • 使用闭包,可以保存 i。
var items = document.getElementsByTagName("li");
for (var i = 0; i < items.length; i++) {
  (function(n) {
    items[n].onclick = function() {
      console.log(`${items[n].innerHTML}-${n}`);
    };
  })(i);
}

// 或者

var items = document.getElementsByTagName("li");
for (var i = 0; i < items.length; i++) {
  items[i].onclick = (function(n) {
    return function() {
      console.log(`${items[n].innerHTML}-${n}`);
    };
  })(i);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 最简单直接的方式,使用 this。
var items = document.getElementsByTagName("li");
for (var i = 0; i < items.length; i++) {
  items[i].onclick = function() {
    console.log(`${this.innerHTML}-${i}`);
  };
}
1
2
3
4
5
6

以上几种方法都可以实现循环绑定,但是频繁的操作 DOM 是非常消耗性能的,如果有 1000 个 li,怎么办?还有另外一种思路,就是使用事件代理,又称事件委托。简单的来讲就是利用 JS 中事件的冒泡属性,把原本需要绑定的事件委托给父元素,让父元素担当事件监听的职务。

var items = document.querySelector("ul");
items.onclick = function(e) {
  e = e || window.event; //这一行及下一行是为兼容 IE8 及以下版本
  var target = e.target || e.srcElement;
  if (target.tagName.toLowerCase() === "li") {
    var li = this.querySelectorAll("li");
    // 这里是因为 querySelectorAll 方法获取到的元素列表不是数组,和函数的 arguments 一样是一种类数组类型,所以不可以直接使用数组的方法
    index = Array.prototype.indexOf.call(li, target);
    console.log(`${target.innerHTML}-${index}`);
  }
};
1
2
3
4
5
6
7
8
9
10
11

使用 jQuery 实现事件代理如下:

$(document).ready(function() {
  $("ul").on("click", function(event) {
    var target = $(event.target);
    console.log(`${target.html()}-${target.index()}`);
  });
});
1
2
3
4
5
6

# 按值传递和按引用传递

🔒 请写出输出值,并解释为什么。

function test(m) {
  m = {
    v: 5
  };
}
var m = {
  k: 30
};
test(m);
alert(m.v); // undefined
1
2
3
4
5
6
7
8
9
10

答案解析:

这道题主要考察的就是按值传递和按引用传递(按址传递)。

  • 按值传递:
var a = 1;
var b = a;
b = 2;
console.log(a); // 1
1
2
3
4
  • 按引用传递(按址传递):
var a = { x: 1 };
var b = a;
b.x = 3;
console.log(a.x); // 3
1
2
3
4

正常情况下,a、b 指向的是同一个地址,但是如果 b 重写了,那么这两个对象或数组就没有任何关系了。比如:

var a = { x: 1 };
var b = { k: 2 };
b.x = 3;
console.log(a.x); // 1
1
2
3
4

因此,在这道题中,因为函数外边把 m 重写了再传给 test,所以此时 m 上已经找不到 v 这个属性了,输出为 undefined。注意,这里外边的 m 只是一个符号而已,改成别的也不影响:

function test(m) {
  m = {
    v: 5
  };
}
var n = {
  k: 30
};
test(n);
alert(n.v); // undefined
1
2
3
4
5
6
7
8
9
10

如果改成下面这样,就能正确访问了。

function test(m) {
  m.v = 5;
}
var m = {
  k: 30
};
test(m);
alert(m.v); // 5
1
2
3
4
5
6
7
8

# 代码题

🔒 请写出代码执行结果,并解释为什么。

function yideng() {
  console.log(1);
}
(function() {
  if (false) {
    function yideng() {
      console.log(2);
    }
  }
  yideng();
})();
1
2
3
4
5
6
7
8
9
10
11

答案解析:

会报错,Uncaught TypeError: yideng is not a function。

# 转换思维

🔒 请用一句话算出 0-100 之间学生的学生等级,如 90-100 输出为 1 等生、80-90 为 2 等生以此类推。不允许使用 if switch 等。

答案解析:

使用 switch 要注意避免下面这个坑:

var speed = 300;
switch (speed) {
  case 400 - speed > 0:
    console.log(123);
    break;
  default:
    console.log(456);
}
1
2
3
4
5
6
7
8

运行程序会发现,并没有像我们预想的那样输出 123,而是输出了 456,说明程序并没有进那个分支。这是因为 switch 实际上是拿你传进来的值跟 case 后面的值做对比的,而不会执行表达式。正常应该写成下面这样:

var speed = 300;
switch (speed) {
  case 300:
    console.log(123);
    break;
  default:
    console.log(456);
}
1
2
3
4
5
6
7
8

输出 123。

回到这道题,这道题其实得用数学思维来思考,而不是通过写十个 if 或 switch。方法其实很简单:

var level = 10 - Math.floor(98 / 10);
console.log(level);
1
2

比如得了 98 分,上面代码最后算出来的结果就是 1 等生,一句代码搞定。以后在写代码的过程中一定要少些 if 或 switch。

# 练习 call、apply、bind

🔒 请用一句话遍历变量 a。(禁止用 for,已知 var a = 'abc')。

答案解析:

var a = "abc";
var b = Array.from(a);
console.log(b); // ["a", "b", "c"]

var a = "abc";
var b = [...a];
console.log(b); // ["a", "b", "c"]
1
2
3
4
5
6
7

这道题其实有很多种解法,但最主要的是想考察能否想到下面这种方法,练习 call、apply、bind:

var slice = Array.prototype.slice;
var b = slice.apply("abc");
console.log(b); // ["a", "b", "c"]
1
2
3

这种方法借用的写法很重要,在很多前端库中都会出现。

# 混合式继承

🔒 请在下面写出 JavaScript 面向对象编程的混合式继承。并写出 ES6 版本的继承。 要求:汽车是父类,Cruze 是子类。父类有颜色、价格属性,有售卖的方法。Cruze 子类实现父类颜色是红色,价格是 140000,售卖方法实现输出如下语句:将红色的 Cruze 卖给了小王,价格是 14 万。

答案解析:

🔔 ES6 中静态属性可以被继承。其他语言是不行的。

class Car {
  static color = 1;
}
class Cruze extends Car {}
console.log(Cruze.color); // 1
1
2
3
4
5

这道题用 ES6 的写法很简单:

class Car {
  static color = 1;
  constructor(price) {
    this.price = price;
  }
  test() {
    console.log(this.price);
  }
}
class Cruze extends Car {
  constructor(price) {
    super(price);
  }
}
const cruze = new Cruze(3000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

但是用 ES5 的写法就没那么简单了:

"use strict";
function Car(price) {
  this.price = price;
}

Car.color = "red"; // 静态属性
Car.prototype.test = function() {
  console.log(this.price);
};

function Cruze(price) {
  Car.call(this, price);
}

var staticKeys = Object.entries(Car);
for (var i = 0; i < staticKeys.length; i++) {
  var key = staticKeys[i][0];
  var value = staticKeys[i][1];
  Cruze[key] = value;
}
// 这种写法不可取,子类会把父类给污染了
// Cruze.prototype = Car.prototype

// 这种写法也不可取,因为构造函数会被执行两次
// Cruze.prototype = new Car()

// 这样写会把 Cruze 的原型链指向 Car,显然也不行,需要手动去修正
// Cruze.prototype = Object.create(Car.prototype)
// 这是对上面写法的修正,写法是对的,但是太常见了
// var _proto = Object.create(Car.prototype)
// _proto.constructor = Cruze
// Cruze.prototype = _proto

// 一步到位的写法,不用手动去修正原型链
Cruze.prototype = Object.create(Car.prototype, {
  constructor: {
    value: Cruze,
    writable: false
  }
});
console.log(Cruze["color"]); // red
var cruze = new Cruze(3000);
cruze.test(); // 3000
console.log(cruze);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 优化多步异步嵌套的代码

🔒 请写出如何利用 EcmaScript6/7(小 Demo)优化多步异步嵌套的代码。

答案解析:

这道题主要就是先了解下 async...await。

# RegExp.prototype.test()

🔒 考考基础。

var regex = /abc/g;
console.log(regex.test("abc"));
console.log(regex.test("abc"));
console.log(regex.test("abc"));
console.log(regex.test("abc"));
1
2
3
4
5

答案解析:

这道题考察的就是正则表达式 RegExp.prototype.test() (opens new window) 的用法。用法里重要的一点就是:

如果正则表达式设置了全局标志,test() 的执行会改变正则表达式 lastIndex 属性。连续的执行 test()方法,后续的执行将会从 lastIndex 处开始匹配字符串,(exec() 同样改变正则本身的 lastIndex 属性值)。

因此这道题的输出应该是:true、false、true、false。

# 代码题

🔒 继续考考基础。

var yideng = function yideng() {
  yideng = 1;
  console.log(typeof yideng);
};
yideng(); // function
yideng = 1;
console.log(typeof yideng); // number
1
2
3
4
5
6
7

答案解析:

函数里边重名的 yideng 不会覆盖掉函数,但是外边的 yideng 就覆盖掉了函数。

加了严格模式就能看到函数里边不能修改重名函数了。会报错:Uncaught TypeError: Assignment to constant variable.

[, ,].length; // 2
1

# window.length

🔒 【仔细思考】写出如下代码的执行结果,并解释为什么。

var length = 10;
function fn() {
  console.log(this.length);
}
var yideng = {
  length: 5,
  method: function(fn) {
    fn(); // 10
    arguments[0](); // 2
  }
};
yideng.method(fn, 1); // 10 2
1
2
3
4
5
6
7
8
9
10
11
12

答案解析:

🔔 window 的 length 取决于页面中 iframe 的数量,window.length (opens new window)

因此,如果把 var length = 10 这句注释掉,那么输出的结果就应该是 0 2 了,而不是 undefined 2。

之所以会输出 2 是因为 arguments[0]() 这句执行后,fn 里面的 this 指向的是 arguments,所以 this.length 此时就是实参的数量,为 2。

# 括号相加和值比较的各种情况

🔒 请写出如下代码输出值,并解释为什么。

console.log({} + '');     // "[object Object]"
console.log({} + []);     // "[object Object]"
{} + [];                  // 0
[] + {};                  // "[object Object]"
{} + {};                  // NaN
console.log([] == false); // true
console.log({} == false); // false
{} == false;              // Uncaught SyntaxError: Unexpected token '=='
1
2
3
4
5
6
7
8
if ([]) {
  console.log([] == false);                    // true
}
('b' + 'a' + + 'a' + 'a').toLocaleLowerCase(); // "banana"
0 == '0';                                      // true,'0' 会先被转成 number,Number('0')
'0' == 0;                                      // true
Boolean('0') == Boolean(0);                    // false
NaN == 0;                                      // false
NaN <= 0;                                      // false
NaN >= 0;                                      // false
NaN == NaN;                                    // false
null == 0;                                     // false
null >= 0;                                     // true
null <= 0;                                     // true
null == null;                                  // true
null == undefined;                             // true
{} == {};                                      // Uncaught SyntaxError: Unexpected token '=='
[] == [];                                      // false
[] == ![];                                     // true,! 的优先级比 == 高,所以右边的先转成了 false
[] == 0;                                       // true
[] == false;                                   // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

答案解析:

  • valueOf 方法用来获取原始值。

  • toString 方法用来返回字符串。

  • 自定义对象的 valueOf 和 toString 方法,来演示对象相加时是怎么加的。

let obj = {
  valueOf: function() {
    console.log("valueOf");
    return 1;
  },
  toString: function() {
    console.log("toString");
    return {};
  }
};

console.log(obj + []);
// valueOf
// 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14

可以看到,对象相加的时候进入了 valueOf。如果把 valueOf 的返回值注释掉,也还是进入 valueOf。

// valueOf
// undefined
1
2
  • console.log({} + []); 之所以会输出那样的结果是因为括号的原因,形成了代码块,{} 的原始值是 [object Object],[] 的原始值是空,所以加起来的结果就是 [object Object]。如果把 console.log 去掉,只是 ({} + []),结果也是 [object Object]。如果把 [] 换成 '',({} + '')的结果也是 [object Object]。object 代表对象类型,Object 代表真正的对象。

  • ('b' + 'a' + + 'a' + 'a').toLocaleLowerCase(); 这一段的核心其实在于 ( + 'a') 的值是 NaN。这是因为任何对象或字符串跟一个空的值加,结果都会强制转换成 number 类型。比如 ( + {}) 的值也是 NaN。但是 ( + []) 的值是 0

  • {} + {}; 的值是 NaN。

  • 记住一点,如果 {} 的前面没有东西,那么它就可以忽略掉。比如 {} + [];( + []) 的值是一样的,都是 0。如果 {} 的前面有括号或者是有另一个 {} 相加,那么就形成了代码块,比如 ({} + []);{} + {}({} + {});[] + {}; 的值都是 "[object Object]"。

  • { const a = 20; } + {}的值是 NaN,如果前一个对象有值,就会强制转换成 number 类型。

  • Boolean('0') == Boolean(0); 的结果是 false,是因为 Boolean('0') 的值为 true,而 Boolean(0) 的值为 false。

  • NaN 是数字,所以 typeof NaN 的值是 number。

  • null 不是对象,但是 typeof null 的值是 object,这是 JavaScript 的一个 bug,历史遗留问题。null 只等于 undefined,null 使用关系运算符(+,>,< 而不是 ==)的时候会转为 0。

  • 对象的比较和运算会使用 ToPrimitive 运算转换左与右运算元为原始数据类型值(primitive)。

  • ToPrimitive 运算:valueOf 能得到原始数据类型的值,则返回这个值。-> toString 能得到原始数据类型的值,则返回这个值。-> 报错 TypeError。

  • [] == 0 的比较流程是:[].valueOf().toStrng() == 0 转成了 '' == 0。

# window.length

🔒 请写出如下输出值,并完成附加题的作答。

function fn() {
  console.log(this.length);
}
var obj = {
  length: 5,
  method: function() {
    "use strict";
    fn();
    arguments[0]();
  }
};
const result = obj.method.bind(null);
result(fn, 1); // 1 2
1
2
3
4
5
6
7
8
9
10
11
12
13

答案解析

这道题跟 Q13 是类似的。看那道题的解析就可以了。

# 变量提升和词法作用域

🔒 附加题

function bar() {
  console.log(myName);
}
function foo() {
  var myName = "林楠";
  bar();
}
var myName = "深圳大学";
foo(); // 深圳大学
1
2
3
4
5
6
7
8
9

答案解析

  • 这道题考察的就是变量提升和词法作用域。var myName = '深圳大学'; 这句代码会提升,bar 函数在定义的时候它的词法作用域就是确定的了,所以它是不会去找 foo 函数里定义的 myName。记住,作用域链是在定义的时候确定的,不是在执行的时候确定的。

  • 如果我们把 var myName = '深圳大学'; 这句代码注释掉,那么会报错:Uncaught ReferenceError: myName is not defined。

  • 如果 bar 函数里边改成 console.log(this.myName);,那这道题就需要从另一个角度来思考了。虽然输出结果还是 “深圳大学“,但是此时是因为 bar 函数的 this 默认绑定到了全局对象上,所以会找到外边定义的 myName。如果把 var myName = '深圳大学'; 注释掉,就会输出 undefined。

# GC & 内存泄漏

🔒 请问变量 a 会被 GC 回收么,为什么?

var b = 30;
function test() {
  var a = "yideng";
  return function() {
    eval("");
  };
}
test()();
1
2
3
4
5
6
7
8

答案解析

  • 在外部用 var 声明的变量会被添加到全局对象 Global 中,比如 var b = 30;那么它就永远不会被回收。

jstest

  • 如果是用 let 或 const 声明的变量,就不会挂载到全局对象上,而是放在 Script 对象里面。

jstest

  • var a = 'yideng'; 下面再加一句 debugger,可以看到变量 a 被放到了 Local 对象里面。

jstest

此时点击下一步,发现变量 a 没了。说明变量 a 并没有被引用,所以被内存回收了。

jstest

  • 如果在内层函数中加 return a;,一开始 a 还是在 Local 对象里面,但是当我点击下一步的时候,会发现它被放到了 Closure(test) 对象里面,这个就是闭包。

jstest

一旦变量被闭包所引用,内存就无法回收它了。所以这也说明了一点,并不是看到闭包就说变量一定不会被回收,而是还要看看这个变量是否有被闭包引用,如果没被引用,还是照样会被回收。

  • 如何查看一个变量是否还在内存中

打开 Memory 面板。

jstest

然后点击左上角的圆形按钮进行录制,就可以看到当前页面的所有变量,可以在上方搜索框中搜索类名。如果有发生内存泄漏,相应的部分会变黄色。Memory 一般是配合 Performance 一起使用的。

jstest

  • 下面这段代码在内存中可以看到 Yideng 有两个引用。
function Yideng(name) {
  this.name = name;
}
let student1 = new Yideng();
let student2 = new Yideng();
1
2
3
4
5

jstest

当我们把 student1 置为 null 时,重新生成快照,就会发现对 Yideng 的引用就只剩下一个了,说明 student1 已经被回收了。快照是可以保存的。不过有时当我们把一个变量置为 null 时,v8 的 GC 不是马上就会把变量回收的,而是会有一定的延迟。

jstest

  • 下面这段代码中,尽管 p1 置为 null,但是这只是把 YidengFactory 生成的实例回收了。因为闭包,内部的 student 还是存在着对外部 Yideng 的引用。除非我们手动在内部加上 student = null; 才会被回收,这是最关键的。
function Yideng(name) {
  this.name = name;
}
// 闭包会存在堆区,方便复用
let YidengFactory = function(name) {
  let student = new Yideng(name);
  return function() {
    console.log(student);
  };
};
let p1 = YidengFactory("shenzhen");
p1();
p1 = null;
1
2
3
4
5
6
7
8
9
10
11
12
13

jstest

注意到此时 Yideng 虽然还在,但是它的 Distance 已经变成了 “—”。相当于它被单独分离开了。

  • eval 会把变量 a 塞到全局的词法作用域中,这里还会产生闭包,因为引擎不知道 eval 里面会执行什么。所以这道题里的变量 a 不会被 GC 回收。

jstest

jstest

这个问题的解决方法就是把 eval 改成:

window.eval("");
1

jstest

  • webpack 里面用的就全是 eval。webpack 之所以这么做,是为了生成代码速度快。

  • with 也有坑。比如下面这段代码直接在全局创建了一个变量 b。但是 with 现在还是有人在用的,比如 vue 的源码中就用到了。

var obj = { a: 30 };
with (obj) {
  b = 30;
}
console.log(b);
1
2
3
4
5

# 原型链

🔒 请写出以下代码的输出值,并解释原因。

Object.prototype.a = 'a';
Function.prototype.a = 'a1';
function Person() {};
var yideng = new Person();
console.log(Person.a);  // a1
console.log(yideng.a);  // a
console.log(1..a);      // a
console.log(1.a);       // 报错
console.log(yideng.__proto__.__proto__.constructor.constructor.constructor); // ƒ Function() { [native code] }

console.log((1).a);     // a
const x = new Number(1);
console.log(x.a);       // a
1
2
3
4
5
6
7
8
9
10
11
12
13

Object.prototype 和 Function.prototype 打印的内容差距很大的原因是什么?

答案解析

  • 关于原型链有下面这张典型的图。

jstest

  • 对于第一个结果,寻值的过程是这样的:function Foo() -> __proto__ -> Function.prototype,因为 Function.prototype 上有个 a 的值是 a1,所以最后的结果就是 a1。

  • 对于第二个结果,寻值的过程是这样的:new Foo() -> __proto__ -> Foo.prototype -> __proto__ -> Object.prototype,因为 Object.prototype 上有个 a 的值是 a,所以最后的结果就是 a。

  • 对于第三个结果,是因为 1. 是一个对象,跟 (1) 是一样的,所以最终找到了 Object.prototype 上的 a。但是 typeof 1. 的值是 number。

  • 对于第四个结果,因为 1 不是对象,所以报错了。

  • 经常在很多库里可能会看到类似下面的代码,这是库为了缩小代码体积而使用的一种简便写法。而且括号里面的东西,在加载时会提前执行。

// 总是会返回后一个值
(20, 30)(
  // 30
  20,
  null
)(
  // null
  30,
  20
); // 20
1
2
3
4
5
6
7
8
9
10

假如有个 obj.a 的值是 30,为了让它拿到值之后可以做下面的事,那么可能就会这么写:

(obj.a).x
// 相当于
(30).x
// 但是不能是下面这样的,因为 30 不是一个对象
30.x
1
2
3
4
5
  • 下面这段代码的输出结果也值得注意下。
Object.prototype.name = "shenzhen";
function test() {}
test.name; // test
(1).name; // shezhen
1
2
3
4

输出结果是 test,这是因为函数的 name 属性是不能改的。

  • 对于第五个结果,可以对照那张原型链的图,一层一层打印下。
console.log(yideng.__proto__);
/*
  {constructor: ƒ}
  constructor: ƒ Person()
  __proto__: Object
*/

console.log(yideng.__proto__.__proto__);
// {a: "a", constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, …}

console.log(yideng.__proto__.__proto__.constructor);
// ƒ Object() { [native code] }

console.log(yideng.__proto__.__proto__.constructor.constructor);
// ƒ Function() { [native code] }

console.log(yideng.__proto__.__proto__.constructor.constructor.constructor);
// ƒ Function() { [native code] }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# Symbol

🔒 请写出如下代码执行结果。

var a = {},
  b = { key: "b" },
  c = { key: "c" };
a[b] = 123;
a[c] = 456;
console.log(a[b]); // 456
console.log(Symbol(b) == Symbol(b)); // false
console.log(Symbol.for(b) == Symbol.for(b)); // true

console.log(a);
/*
  {[object Object]: 456}
  [object Object]: 456
  __proto__: Object
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

答案解析

  • 之所以输出 456,是因为键值是对象时,现代浏览器是把它转成了 [object Object],所以 a[b] 和 a[c] 其实都是 a[[object Object]],因此最后输出的就是 456。

  • Symbol 是唯一值,但是 Symbol.for 不是。

# ES6 元编程

🔒 请写出你了解的 ES6 的元编程。

答案解析

var shenzhen = {
  [Symbol.toPrimitive]: (
    (i) => () => ++i
  )(0)
}
if (shenzhen == 1 && shenzhen == 2 && shenzhen == 3) {
  console.log('深圳'); // 深圳
}
1
2
3
4
5
6
7
8
// 临时向 Object.prototype 添加一个迭代器
Object.prototype[Symbol.iterator] = function* () {
  for (let key of Object.keys(this)) {
    yield this[key];
  }
};

// 这行代码保持不变
const [a, b] = { a: 1, b: 2 };

// 在使用完迭代器之后,最好删除它,避免全局污染
delete Object.prototype[Symbol.iterator];

console.log(a, b); // 输出 1 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 浏览器留了一些底层属性可以给你用,这也属于一种元编程,比如:
// 开启尾递归调用优化
function test(i) {
  return test(i--, i);
  TCO_ENABLED = true;
}
test(5);
1
2
3
4
5
6

# async 原理、协程、线程

🔒 请按照下方要求作答,并解释原理。请解释下 babel 编译后的 async 原理。

let a = 0;
let yideng = async () => {
  a = a + (await 10);
  console.log(a);
};
yideng(); // 10
console.log(++a); // 1
// 先输出 1,再输出 10
1
2
3
4
5
6
7
8

答案解析

  • async 本身不是异步的,await 才是,await 后面的代码会等待 promise.then 执行后才执行。

  • 先写一个简单的 generator。

function* genDemo() {
  console.log("第一段执行逻辑");
  yield "Generator2 ";
  console.log("第二段执行逻辑");
  yield "Generator2 ";
  console.log("第三段执行逻辑");
  yield "Generator2 ";
  console.log("执行完毕");
  return "Generator2";
}
console.log("main 0");
let gen = genDemo();
console.log(gen.next().value);
console.log("main 1");

console.log("--------------");

console.log(gen.next().value);
console.log("main 2");
console.log(gen.next().value);
console.log("main 3");
console.log(gen.next().value);
console.log("main 4");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

通过这段代码的结果,我们得明白以下几点:

(1)代码不是一次性执行完的,而是和全局代码交替执行。可以暂停,也可以恢复。

(2)协程比线程更轻量,它跑在线程上,并且一个线程可以有多个协程。

(3)线程上同时只执行一个协程。如果协程 A 在运行,此时想启动协程 B,那么就需要把控制权从 A 移交给 B,A 先暂停。如果协程 A 打开 协程 B,那么 A 就是 B 的父协程。

(4)之所以用协程进行管理,是因为协程不受操作系统管理,是由程序控制的。当我们调用 gen.next() 时,引擎会保存父协程的调用栈信息。这也叫做 “锁变量”。

以上这几点也是 async ... await 的执行原理。

  • 下面是跟协程相关的几张图。

(1)父协程

jstest

(2)协程执行流程图

jstest

(3)async / await 执行过程

jstest

  • async 函数的实现原理 (opens new window),面试中也经常会让手写 async/await,核心就是实现 spawn 函数。

  • 这道题中,a = a + await 10; 这里的 a 其实是被保存起来了(锁变量),所以相当于是 a = 0 + 10,结果是输出 10。如果把这道题稍微改一下由不一样了。

let a = 0;
let yideng = async () => {
  a = (await 10) + a;
  console.log(a);
};
yideng(); // 11
console.log(++a); // 1
// 先输出 1,再输出 11
1
2
3
4
5
6
7
8

这是因为 await 之前的变量才会被锁定。

加强一下

async function async1() {
  console.log(1);
  await async2();
  console.log(3);
}
async function async2() {
  console.log(2);
}
async1();
console.log(4);
// 这题简单,依次输出:1 2 4 3
1
2
3
4
5
6
7
8
9
10
11

# 爆栈和死循环

🔒 请问点击 <button id="test"></button> 会有反应么?为什么?能解决么?

$("#test").click(function(argument) {
  console.log(1);
});
setTimeout(function() {
  console.log(2);
}, 0);
while (true) {
  console.log(Math.random());
}
1
2
3
4
5
6
7
8
9

答案解析

  • 爆栈是指内存空间不够了,用完了;死循环是指占着主线程不放,内存空间还是有的。

  • 这题的解决方法就是用多线程。在 web worker 没出现之前,很多人都是用 Concurrent.Thread.js (opens new window) 来模拟多线程的。

# Promise

🔒 请先书写如下代码执行结果,并用 ES5 实现 ES6 PromiseA+ 规范的代码,同时你能解释下如何使用 Promise 完成事务的操作么。

const pro = new Promise((resolve, reject) => {
  const innerpro = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(1);
    });
    console.log(2);
    resolve(3);
  });
  innerpro.then((res) => console.log(res));
  resolve(4);
  console.log("yideng");
});
pro.then((res) => console.log(res));
console.log("end");
// 依次输出:2 yideng end 3 4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

答案解析

  • 创建 promise 的时候是同步的,promise.then 才是异步的。

  • promise 的状态一旦改变,就结束了。所以 setTimeout 里面的 resolve 是没用的。

# js 模拟指针移动

🔒 请写出如下代码的输出值,并解释为什么。

var s = [];
var arr = s;
for (var i = 0; i < 3; i++) {
  var pusher = {
      value: "item" + i
    },
    tmp;
  if (i !== 2) {
    tmp = [];
    pusher.children = tmp;
  }
  arr.push(pusher);
  arr = tmp;
}
console.log(s[0]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

答案解析

  • 这道题其实是用 js 模拟指针移动,最后的输出结果是一棵树。

# 函数式编程

🔒 请描述你理解的函数式编程,并书写如下代码的结果。那么你能使用 Zone + RX 写出一段 FRP 的代码么。

var Container = function(x) {
  this._value = x;
};
Container.of = (x) => new Container(x);
Container.prototype.map = function(f) {
  return Container.of(f(this._value));
};
Container.of(3)
  .map((x) => x + 1)
  .map((x) => "Result is " + x);
1
2
3
4
5
6
7
8
9
10
上次更新时间: 2023年12月27日 21:57:42