Jack N @ GitHub

Full stack engineer, focus on: Angular/React, node.js/.Net

0%

2021 javascript 面试题汇总

2021 javascript 面试题汇总

1. this 都可以是哪些内容

对 C#、Java 等语言,this 就是当前对象,但是 javascript 不是,简单来说:

  • 全局 this 是 window;
  • 函数 this 是调用者;
  • 构造函数的 this 是 new 之后的新对象,
  • call 和 apply bind 的 this 第一个参数

2. 原型链, prototype

  • 函数对象都包含 prototype 属性(函数的原型对象),其作用就是让该函数所实例化的对象们都可以找到公用的属性和方法;
  • constructor 属性的含义就是指向该对象的构造函数
  • __proto__和 constructor 属性是对象所独有的;prototype 属性是函数所独有的,因为函数也是一种对象,所以函数也拥有__proto__和 constructor 属性。
  • JavaScript 的每个对象都继承另一个父级对象,父级对象称为原型 (prototype) 对象。
  • 每一个实例对象都有一个私有属性proto指向其构造函数的原型对象 prototype;该原型对象也会作为实例对象有一个私有属性proto,层层向上直到一个对象的原型对象值为 null。
  • 当访问一个对象的属性或方法时,js 引擎会先查找该对象本身是否包含,如果没有,会去该对象的proto属性所指向的原型对象上找,如果没有,会继续向上一层找,直到某个对象的proto值为 null,这就是原型链。
  • 每个构造函数都有一个 prototype 属性,指向另外一个对象,说明整个对象所有的属性和方法都会被构造函数所拥有。

3. new 操作符都做了什么

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型之一。

new Object()举例:

  1. 创建一个新对象
  2. 把新对象的原型(__proto__)指向构造函数的 prototype
  3. 把构造函数里的 this 指向新对象
  4. 返回这个新对象

4. Javascript 的内存回收机制

Javascript 内嵌了垃圾收集器,用来跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。垃圾收集器会按照固定的时间间隔,或代码执行中预定的收集时间,周期性地执行这一操作。
垃圾收集器必须跟踪哪个变量有用哪个变量无用,对于不再有用的变量打上标记,以备将来收回其所占用的内存。用于标识无用变量的策略通常有标记清除和引用计数两种。不同浏览器采用的策略不完全一致。

5. 内存泄漏的主要原因

  • 缓存
  • 队列消费不及时
  • 作用域未释放

6. Javascript 性能优化

  1. 针对 js 方面的前端性能优化,可以 1)使用 ansyc/defer 加载;2)加载时,放到</body>之前;3) 合并 JS 文件,并进行最小化处理;4)缓存;5)使用 CDN 网络;6)js 的 HTTP 压缩
  2. 删除未使用的功能性代码以及与之相关的代码, 多余的依赖库,被滥用的 npm 包。
  3. 减少作用域链上的查找次数(减少循环中的活动)。比如 for 循环,把 Array.length 赋值给一个变量,而不是每次比较都直接使用 length 属性;对页面元素进行操作时,取出复制给一个变量,每次操作都对这个变量进行操作,而不是每次操作都再重复取一下这个元素;
  4. 闭包导致的内存泄露。闭包可以保证函数内的变量安全,可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中,不会被自动清除。我们需要手动销毁内存中的变量。(赋值为 null);
  5. 尽量少用全局变量,尽量使用局部变量。
  6. 减少不必要的变量
  7. 类型转换:把数字转换成字符串使用 number + “” 。 性能对比: (“” + ) > String() > .toString() > new String()
  8. 对字符串进行循环操作,譬如替换、查找,应使用正则表达式。因为本身 JavaScript 的循环速度就比较慢,而正则表达式的操作是用 C 写成的语言的 API,性能很好。
  9. 浮点数转换成整型使用 Math.floor()或者 Math.round()。parseInt()是用于将字符串转换成数字,Math 是内部对象,所以 Math.floor()其实并没有多少查询方法和调用的时间,速度是最快的。
  10. 使用 classname 代替大量的内联样式修改。
  11. 循环遍历,尽量少用 for in, 虽然代码易读,但性能很差。 尤其是遍历属性数量未知的情况下,少用。

7. ES6/ES7/ES8/… 新增的功能

7.1. ES6 (2015)

  • 模块化
  • 箭头函数
  • 函数参数默认值
  • 模板字符串
  • 解构赋值
  • 延展操作符
  • 对象属性简写
  • Promise
  • Let与Const

7.2. ES7(2016)

  • 数组includes()方法,用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回true,否则返回false。
  • **指数运算符: a ** b指数运算符,它与 Math.pow(a, b)相同。

    7.3. ES8(2017)

  • async/await
  • Object.values()
  • Object.entries()
  • String padding: padStart()和padEnd(),填充字符串达到当前长度
  • 函数参数列表结尾允许逗号
  • Object.getOwnPropertyDescriptors()
  • ShareArrayBuffer和Atomics对象,用于从共享内存位置读取和写入

    7.4. ES9(2018)

  • 异步迭代
  • Promise.finally()
  • Rest/Spread 属性: ES2015引入了Rest参数和扩展运算符。三个点(…)仅用于数组。Rest参数语法允许我们将一个不定数量的参数表示为一个数组。
  • 正则表达式命名捕获组
  • 正则表达式反向断言

7.5. ES10新特性(2019)

  • 行分隔符(U + 2028)和段分隔符(U + 2029)符号现在允许在字符串文字中,与JSON匹配
  • 更加友好的 JSON.stringify
  • 新增了Array的flat()方法和flatMap()方法
  • 新增了String的trimStart()方法和trimEnd()方法
  • Object.fromEntries()
  • Symbol.prototype.description
  • String.prototype.matchAll
  • Function.prototype.toString()现在返回精确字符,包括空格和注释
  • 简化try {} catch {},修改 catch 绑定
  • 新的基本数据类型BigInt
  • globalThis
  • import()
  • Legacy RegEx
  • 私有的实例方法和访问器

7.6. ES11(2020)

  • Nullish coalescing Operator(空值处理)
  • import() 按需导入

7.7. ES12(2021)

  • string增加replaceAll
  • Promise.any, 收一个Promise可迭代对象,只要其中的一个 promise 成功,就返回那个已经成功的 promise
  • WeakRefs, 使用WeakRefs的Class类创建对对象的弱引用(对对象的弱引用是指当该对象应该被GC回收时不会阻止GC的回收行为)
  • 数字分隔符
  • 逻辑运算符和赋值表达式

8. === VS ==

1
2
3
4
5
100 == "100"; // true
0 == ""; // true
0 == false; // true
false == ""; // true
null == undefined; // true

很有意思吧, 看看这两个,是等效的:

1
2
obj == null;
obj === null || obj === undefined;

9. 闭包

在 JavaScript 中,实现外部作用域访问内部作用域中变量的方法叫做闭包(closure)。这得益于高阶函数的特性:函数可以作为参数或者者返回值;

当一个内部函数被调用,就会形成闭包,闭包就是能够读取其他函数内部变量的函数,就是一个函数去访问了另外一个函数的中的变量的函数。

闭包作用:局部变量无法共享和长久的保存,而全局变量可能造成变量污染,所以我们希望有一种机制既可以长久的保存变量又不会造成全局污染。延伸变量的作用范围。

闭包特点:占用更多内存;不容易被释放

闭包用法:变量既想反复使用,又想避免全局污染如何使用?

  1. 定义外层函数,封装被保护的局部变量。
  2. 定义内层函数,执行对外部函数变量的操作。
  3. 外层函数返回内层函数的对象,并且外层函数被调用,结果保存在一个全局的变量中。

e.g.

1
2
3
4
5
6
7
function outerMethod() {
var a = "some value";
var innerMethod = function () {
console.log(a + "!");
};
innerMethod();
}

10. js 里的作用域是什么样子的?

大多数语言里边都是块作作用域,以{}进行限定,js 里边不是.js 里边叫函数作用域,就是一个变量在全函数里有效.比如有个变量 p1 在函数最后一行定义,第一行也有效,但是值是 undefined.

11. let 和 var 的区别

  • var 声明变量可以重复声明,而 let 不可以重复声明,属于 TDZ 暂时性死区问题
  • 作用域不同,var 是函数作用域,而 let 是块级作用域;
  • var 可以在声明的上面访问变量,而 let 不存在变量提升;

12. 对 async 和 await 的理解

  1. async…await 是基于 promise 的 generator 语法糖,是用来解决异步的,它用来等待 promise 的执行结果,常规函数使用 await 没有效果;
  2. async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数
  3. 当函数执行的时候,一旦遇到 await 就会先返回,等到异步操作完成,再接着执行函数体内后面的语句;

13. promised 的三种状态

promise 有三种状态:pending/reslove/reject 。pending 就是未决,resolve 可以理解为成功,reject 可以理解为拒绝。

14. async/await 的优点

1)方便级联调用: 即调用依次发生的场景;

2)同步代码编写方式: Promise 使用 then 函数进行链式调用,一直点点点,是一种从左向右的横向写法;async/await 从上到下,顺序执行,就像写同步代码一样,更符合代码编写习惯;

3)多个参数传递: Promise 的 then 函数只能传递一个参数,虽然可以通过包装成对象来传递多个参数,但是会导致传递冗余信息,频繁的解析又重新组合参数,比较麻烦;async/await 没有这个限制,可以当做普通的局部变量来处理,用 let 或者 const 定义的块级变量想怎么用就怎么用,想定义几个就定义几个,完全没有限制,也没有冗余工作;

4)同步代码和异步代码可以一起编写: 使用 Promise 的时候最好将同步代码和异步代码放在不同的 then 节点中,这样结构更加清晰;async/await 整个书写习惯都是同步的,不需要纠结同步和异步的区别,当然,异步过程需要包装成一个 Promise 对象放在 await 关键字后面;

5)基于协程: Promise 是根据函数式编程的范式,对异步过程进行了一层封装,async/await 基于协程的机制,是真正的“保存上下文,控制权切换……控制权恢复,取回上下文”这种机制,是对异步过程更精确的一种描述;

6)async/await 是对 Promise 的优化: async/await 是基于 Promise 的,是进一步的一种优化,不过在写代码时,Promise 本身的 API 出现得很少,很接近同步代码的写法;

15. async/await 的写法

1
2
3
4
5
6
7
8
9
async function testAsync() {
return "hello async";
}

// 调用时
async function test() {
var value = await testAsync();
console.log(value);
}

16. apply、call 和 bind 的区别

这三者都是用来改变函数的 this 对象的指向的(也就是说要改变函数运行时的 context 即上下文),第一个参数都是 this 要指向的对象。

不同之处:

  1. call 接受连续参数,apply 接受数组参数。
  2. bind 功能和 call apply 差不多,区别在于 bind 返回的是一个改变 this 指向的新函数,这个函数不是立即执行函数(call apply 两个立即执行!)

17. 深拷贝的原理

操作的是值,取出对象的值,放到一个新的{},修改时不会影响到原数据;要完成对象的深拷贝需要使用递归遍历所有对象的属性进行赋值,也可以使用 JSON.stringfy 和 JSON.parse 操作。

18. set 和 Map 的区别

  1. Set 是一种类似数组的集合类型,它与数组不同的是,不允许存在重复数据;常用操作方法有:add,delete,has,clear 等;遍历使用 forEach;
  2. Map 是一种类似对象的集合类型,它与对象不同的是,key 可以接受对象类型,常用的操作方法有:set,get,has,delete 等;遍历使用 forEach

19. JS 实现双向绑定 (vue/angular 双向绑定的原理)

vue.js 采用数据劫持(Proxy 模式)结合发布者-订阅者模式(PubSub 模式)的方式,通过 Object.defineProperty()来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
Object.defineProperty( )内还包含一对儿 getter 和 setter 函数,它们被称作这个对象的访问器属性,这两个函数都不是必须的,只是在读取访问对象属性时,会调用 getter 函数,这个函数负责返回有效值,在写入对象属性时,会调用 setter 函数并传入新值,这个函数负责决定如何处理数据。

Angular, 采用脏检查的方式。基于 zone.js,重写 setTimeout, httpRequest 等方法,触发脏检查, 更新 dom 节点。

20. var 和 let 的区别

  1. 作用域不同, var 是函数作用域,let 是块作用域。
  • 在函数中声明了 var,整个函数内都是有效的,比如说在 for 循环内定义的一个 var 变量,实际上其在 for 循环以外也是可以访问的
  • 而 let 由于是块作用域,所以如果在块作用域内定义的变量,比如说在 for 循环内,在其外面是不可被访问的,所以 for 循环推荐用 let
  1. let 不能在定义之前访问该变量,但是 var 可以。 (var 可以提升变量作用域)
  2. let不能被重新定义,但是var是可以的。(重新定义var,执行时会忽略)

    21. Javascript 如何实现继承?

1、构造继承
2、原型继承: Child.prototype = new Parent();
3、实例继承
4、拷贝继承

22. 变量提升

js 引擎首先在读取 js 代码时默认执行 2 个步骤:

  1. 解释(通篇扫描所有 js 代码,然后把所有声明(变量申明、函数声明)提升到对应作用域顶端)
  2. 执行(执行逻辑操作)
1
2
3
4
5
6
7
8
// 例1.
console.log(b); // 输出 ReferenceError: b is not defined
b = 2;

// 例2.
console.log(b); // 输出 undefined
var b = 2;
console.log(b); // 输出 2

23. 函数提升

首先要知道函数定义有两种方式的,一种是函数定义表达式,一种是函数声明语句。

1
2
3
4
5
6
7
// 函数定义表达式
var f = function (){
};

// 函数声明语句
function f(){
}
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
// 提升测试
// 例1.
f();
function f() {
console.log("test");
}
//输出 test

// 例2.
f();
var f = function () {
console.log("test");
};

//输出 Uncaught TypeError: f is not a function

// 例3
console.log(f);
var f = 10;
console.log(f);
function f() {
console.log("fff");
}
// 输出
// [Function: f]
// 10
// 10

// 例4
var a = 1;
function foo() {
a = 10;
console.log(a); // 输出:10
return;
function a() {} // 看似无用, 实际上在foo的作用域内重新声明了'a',和外面的a不一样了
}
foo();
console.log(a); // 输出:1
//输出:
// 10
// 1

重点:

  1. 函数提升针对是函数声明语句而言,其次变量提升只提升变量名而函数提升会提升整个函数体
  2. 优先级,函数提升会在变量提升的上面

24. 作用域

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
46
47
48
// 例1 块级作用域, 不适合变量提升
// es6允许块级作用域的任意嵌套。外层作用域无法读取内层作用域的变量。
// 内层作用域可以定义外层作用域的同名变量。

var v = "hello";
if (true) {
console.log(v);
var v = "world";
}
//输出 hello

var v = "hello";
if (true) {
console.log(v);
var v = "world";
var z = "!";
}
console.log(v, z);
//输出 :
// hello
// world !

// 例2 闭包,变量提升
var v = "hello";
(function () {
console.log(v);
var v = "world";
})();
//输出 undefined

// 例 3, 块级作用域内let和const命令所声明的变量,只在命令所在的代码块内有效。
if (true) {
let a = 10;
var b = 1;
}
console.log(a); //ReferenceError: a is not defined.
console.log(b); // 1

// 例4 let, var与作用域、变量提升
if (true) {
console.log(a); //ReferenceError: a is not defined.
console.log(b); // 1

let a = 10;
var b = 1;
}
console.log(a); //ReferenceError: a is not defined.
console.log(b); // 1

25. 实现一个随机排序的程序

1
2
3
4
5
6
7
8
9
10
11
function randomSort() {
return Math.random() - 0.5;
}
let list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
list.sort(randomSort);
console.log("after random sort: ", list.join(","));

// ES6 箭头函数写法
let list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
list.sort(() => Math.random() - 0.5);
console.log("after random sort: ", list.join(","));

26. 实现一个函数,传入一个字符串,得到使用次数最多的字符,以及数量

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
function getMostUsedChar(sourceString) {
let length = sourceString.length;
let counter = {};
for (var i = 0; i < length; i++) {
let currentChar = sourceString[i];
if (counter[currentChar]) {
counter[currentChar]++;
} else {
counter[currentChar] = 1;
}
}

let maxCount = 0;
let maxChar = "";
for (const key in counter) {
if (counter[key] >= maxCount) {
maxChar = key;
maxCount = counter[key];
}
}

console.log(`most used char: ${maxChar}, count: ${maxCount}`);
}

getMostUsedChar("fjiwejfal33232jkj3fjfie");

参考: