# 1. 根据下面 ES6 构造函数的书写方式,要求写出 ES5 的
class Example {
constructor(name) {
this.name = name;
}
init() {
const fun = () => {
console.log(this.name);
};
fun();
}
}
const e = new Example("Hello");
e.init();
2
3
4
5
6
7
8
9
10
11
12
13
参考答案:
function Example(name) { "use strict"; if (!new.target) { throw new TypeError("Class constructor cannot be invoked without new"); } this.name = name; } Object.defineProperty(Example.prototype, "init", { enumerable: false, value: function () { "use strict"; if (new.target) { throw new TypeError("init is not a constructor"); } var fun = function () { console.log(this.name); }; fun.call(this); }, });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
解析:
此题的关键在于是否清楚 ES6 的 class 和普通构造函数的区别,记住它们有以下区别,就不会有遗漏:
ES6 中的 class 必须通过 new 来调用,不能当做普通函数调用,否则报错
因此,在答案中,加入了 new.target 来判断调用方式
ES6 的 class 中的所有代码均处于严格模式之下
因此,在答案中,无论是构造函数本身,还是原型方法,都使用了严格模式
ES6 中的原型方法是不可被枚举的
因此,在答案中,定义原型方法使用了属性描述符,让其不可枚举
原型上的方法不允许通过 new 来调用
因此,在答案中,原型方法中加入了 new.target 来判断调用方式
# 2. 数组去重有哪些方法?(美团 19 年)
参考答案:
// 数字或字符串数组去重,效率高 function unique(arr) { var result = {}; // 利用对象属性名的唯一性来保证不重复 for (var i = 0; i < arr.length; i++) { if (!result[arr[i]]) { result[arr[i]] = true; } } return Object.keys(result); // 获取对象所有属性名的数组 } // 任意数组去重,适配范围广,效率低 function unique(arr) { var result = []; // 结果数组 for (var i = 0; i < arr.length; i++) { if (!result.includes(arr[i])) { result.push(arr[i]); } } return result; } // 利用ES6的Set去重,适配范围广,效率一般,书写简单 function unique(arr) { return [...new Set(arr)]; }
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
# 3. 描述下列代码的执行结果
foo(typeof a);
function foo(p) {
console.log(this);
console.log(p);
console.log(typeof b);
let b = 0;
}
2
3
4
5
6
7
参考答案:
报错,报错的位置在
console.log(typeof b);
报错原因:ReferenceError: Cannot access 'b' before initialization
解析:
这道题考查的是 ES6 新增的声明变量关键字 let 以及暂时性死区的知识。let 和以前的 var 关键字不一样,无法在 let 声明变量之前访问到该变量,所以在 typeof b 的地方就会报错。
# 4. 描述下列代码的执行结果
class Foo {
constructor(arr) {
this.arr = arr;
}
bar(n) {
return this.arr.slice(0, n);
}
}
var f = new Foo([0, 1, 2, 3]);
console.log(f.bar(1));
console.log(f.bar(2).splice(1, 1));
console.log(f.arr);
2
3
4
5
6
7
8
9
10
11
12
参考答案:
[ 0 ] > [ 1 ] > [ 0, 1, 2, 3 ]
解析:
主要考察的是数组相关的知识。 f 对象上面有一个属性 arr,arr 的值在初始化的时候会被初始化为 [0, 1, 2, 3],之后就完全是考察数组以及数组方法的使用了。
# 5. 描述下列代码的执行结果
01 function f(count) {
02 console.log(`foo${count}`);
03 setTimeout(() => { console.log(`bar${count}`); });
04 }
05 f(1);
06 f(2);
07 setTimeout(() => { f(3); });
2
3
4
5
6
7
参考答案:
foo1 foo2 bar1 bar2 foo3 bar3
解析:
这个完全是考察的异步的知识。调用 f(1) 的时候,会执行同步代码,打印出 foo1,然后 03 行的 setTimeout 被放入到异步执行队列,接下来调用 f(2) 的时候,打印出 foo2,后面 03 行的 setTimeout 又被放入到异步执行队列。然后执行 07 行的语句,被放入到异步执行队列。至此,所有同步代码就都执行完毕了。
接下来开始执行异步代码,那么大家时间没写,就都是相同的,所以谁先被放入到异步队列,谁就先执行,所以先打印出 bar1、然后是 bar2,接下来执行之前 07 行放入到异步队列里面的 setTimeout,先执行 f 函数里面的同步代码,打印出 foo3,然后是最后一个异步,打印出 bar3
# 6. 描述下列代码的执行结果
var a = 2;
var b = 5;
console.log(a === 2 || (1 && b === 3) || 4);
2
3
参考答案:
true
考察的是逻辑运算符。在 || 里面,只要有一个为真,后面的直接短路,都不用去计算。所以 a === 2 得到 true 之后直接短路了,返回 true。
# 7. 描述下列代码的执行结果
export class ButtonWrapper {
constructor(domBtnEl, hash) {
this.domBtnEl = domBtnEl;
this.hash = hash;
this.bindEvent();
}
bindEvent() {
this.domBtnEl.addEventListener("click", this.clickEvent, false);
}
detachEvent() {
this.domBtnEl.removeEventListener("click", this.clickEvent);
}
clickEvent() {
console.log(`The hash of the button is: ${this.hash}`);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
参考答案:
上面的代码导出了一个 ButtonWrapper 类,该类在被实例化的时候,实例化对象上面有两个属性,分别是 domBtnEl 和 hash,domBtnEl 是一个 DOM 节点,之后为这个 domBtnEl 绑定了点击事件,点击后打印出 The hash of the button is: hash 那句话。detachEvent 是移除点击事件,当调用实例化对象的 detachEvent 方法时,点击事件就会被移除。
# 8. 箭头函数有哪些特点
参考答案:
- 更简洁的语法,例如
- 只有一个形参就不需要用括号括起来
- 如果函数体只有一行,就不需要放到一个块中
- 如果 return 语句是函数体内唯一的语句,就不需要 return 关键字
- 箭头函数没有自己的 this,arguments,super
- 箭头函数 this 只会从自己的作用域链的上一层继承 this。
# 9. 说一说类的继承
参考答案:
继承是面向对象编程中的三大特性之一。
JavaScript 中的继承经过不断的发展,从最初的对象冒充慢慢发展到了今天的圣杯模式继承。
其中最需要掌握的就是伪经典继承和圣杯模式的继承。
很长一段时间,JS 继承使用的都是组合继承。这种继承也被称之为伪经典继承,该继承方式综合了原型链和盗用构造函数的方式,将两者的优点集中了起来。
组合继承弥补了之前原型链和盗用构造函数这两种方式各自的不足,是 JavaScript 中使用最多的继承方式。
组合继承最大的问题就是效率问题。最主要就是父类的构造函数始终会被调用两次:一次是在创建子类原型时调用,另一次是在子类构造函数中调用。
本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。
圣杯模式的继承解决了这一问题,其基本思路就是不通过调用父类构造函数来给子类原型赋值,而是取得父类原型的一个副本,然后将返回的新对象赋值给子类原型。
解析:该题主要考察就是对 js 中的继承是否了解,以及常见的继承的形式有哪些。最常用的继承就是组合继承(伪经典继承)和圣杯模式继承。下面附上 js 中这两种继承模式的详细解析。
下面是一个组合继承的例子:
// 基类 var Person = function (name, age) { this.name = name; this.age = age; }; Person.prototype.test = "this is a test"; Person.prototype.testFunc = function () { console.log("this is a testFunc"); }; // 子类 var Student = function (name, age, gender, score) { Person.apply(this, [name, age]); // 盗用构造函数 this.gender = gender; this.score = score; }; Student.prototype = new Person(); // 改变 Student 构造函数的原型对象 Student.prototype.testStuFunc = function () { console.log("this is a testStuFunc"); }; // 测试 var zhangsan = new Student("张三", 18, "男", 100); console.log(zhangsan.name); // 张三 console.log(zhangsan.age); // 18 console.log(zhangsan.gender); // 男 console.log(zhangsan.score); // 100 console.log(zhangsan.test); // this is a test zhangsan.testFunc(); // this is a testFunc zhangsan.testStuFunc(); // this is a testStuFunc
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在上面的例子中,我们使用了组合继承的方式来实现继承,可以看到无论是基类上面的属性和方法,还是子类自己的属性和方法,都得到了很好的实现。
但是在组合继承中存在效率问题,比如在上面的代码中,我们其实调用了两次 Person,产生了两组 name 和 age 属性,一组在原型上,一组在实例上。
也就是说,我们在执行 Student.prototype = new Person( ) 的时候,我们是想要 Person 原型上面的方法,属性是不需要的,因为属性之后可以通过 Person.apply(this, [name, age]) 拿到,但是当你 new Person( ) 的时候,会实例化一个 Person 对象出来,这个对象上面,属性和方法都有。
圣杯模式的继承解决了这一问题,其基本思路就是不通过调用父类构造函数来给子类原型赋值,而是取得父类原型的一个副本,然后将返回的新对象赋值给子类原型。
下面是一个圣杯模式的示例:
// target 是子类,origin 是基类 // target ---> Student, origin ---> Person function inherit(target, origin) { function F() {} // 没有任何多余的属性 // origin.prototype === Person.prototype, origin.prototype.constructor === Person 构造函数 F.prototype = origin.prototype; // 假设 new F() 出来的对象叫小 f // 那么这个 f 的原型对象 === F.prototype === Person.prototype // 那么 f.constructor === Person.prototype.constructor === Person 的构造函数 target.prototype = new F(); // 而 f 这个对象又是 target 对象的原型对象 // 这意味着 target.prototype.constructor === f.constructor // 所以 target 的 constructor 会指向 Person 构造函数 // 我们要让子类的 constructor 重新指向自己 // 若不修改则会发现 constructor 指向的是父类的构造函数 target.prototype.constructor = target; } // 基类 var Person = function (name, age) { this.name = name; this.age = age; }; Person.prototype.test = "this is a test"; Person.prototype.testFunc = function () { console.log("this is a testFunc"); }; // 子类 var Student = function (name, age, gender, score) { Person.apply(this, [name, age]); this.gender = gender; this.score = score; }; inherit(Student, Person); // 使用圣杯模式实现继承 // 在子类上面添加方法 Student.prototype.testStuFunc = function () { console.log("this is a testStuFunc"); }; // 测试 var zhangsan = new Student("张三", 18, "男", 100); console.log(zhangsan.name); // 张三 console.log(zhangsan.age); // 18 console.log(zhangsan.gender); // 男 console.log(zhangsan.score); // 100 console.log(zhangsan.test); // this is a test zhangsan.testFunc(); // this is a testFunc zhangsan.testStuFunc(); // this is a testStuFunc
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
49
50
51
52
53
54在上面的代码中,我们在 inherit 方法中创建了一个中间层,之后让 F 的原型和父类的原型指向同一地址,再让子类的原型指向这个 F 的实例化对象来实现了继承。
这样我们的继承,属性就不会像之前那样实例对象上一份,原型对象上一份,拥有两份。圣杯模式继承是目前 js 继承的最优解。
最后我再画个图帮助大家理解,如下图:
组合模式(伪经典模式)下的继承示意图:
![]()
圣杯模式下的继承示意图:
# 10. new 操作符都做了哪些事?
参考答案:
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。
new 关键字会进行如下的操作:
步骤 1:创建一个空的简单 JavaScript 对象,即 { } ;
步骤 2:链接该对象到另一个对象(即设置该对象的原型对象);
步骤 3:将步骤 1 新创建的对象作为 this 的上下文;
步骤 4:如果该函数没有返回对象,则返回 this。
# 11. call、apply、bind 的区别 ?
参考答案:
call 和 apply 的功能相同,区别在于传参的方式不一样:
- fn.call(obj, arg1, arg2, ...) 调用一个函数, 具有一个指定的 this 值和分别地提供的参数(参数的列表)。
- fn.apply(obj, [argsArray]) 调用一个函数,具有一个指定的 this 值,以及作为一个数组(或类数组对象)提供的参数。
bind 和 call/apply 有一个很重要的区别,一个函数被 call/apply 的时候,会直接调用,但是 bind 会创建一个新函数。当这个新函数被调用时,bind( ) 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。
# 12. 事件循环机制(宏任务、微任务)
参考答案:
在 js 中任务会分为同步任务和异步任务。
如果是同步任务,则会在主线程(也就是 js 引擎线程)上进行执行,形成一个执行栈。但是一旦遇到异步任务,则会将这些异步任务交给异步模块去处理,然后主线程继续执行后面的同步代码。
当异步任务有了运行结果以后,就会在任务队列里面放置一个事件,这个任务队列由事件触发线程来进行管理。
一旦执行栈中所有的同步任务执行完毕,就代表着当前的主线程(js 引擎线程)空闲了,系统就会读取任务队列,将可以运行的异步任务添加到执行栈中,开始执行。
在 js 中,任务队列中的任务又可以被分为 2 种类型:宏任务(macrotask)与微任务(microtask)
宏任务可以理解为每次执行栈所执行的代码就是一个宏任务,包括每次从事件队列中获取一个事件回调并放到执行栈中所执行的任务。
微任务可以理解为当前宏任务执行结束后立即执行的任务。
# 13. 你了解 node 中的事件循环机制吗?node11 版本以后有什么改变
参考答案:
Node.js 在主线程里维护了一个事件队列,当接到请求后,就将该请求作为一个事件放入这个队列中,然后继续接收其他请求。当主线程空闲时(没有请求接入时),就开始循环事件队列,检查队列中是否有要处理的事件,这时要分两种情况:如果是非 I/O 任务,就亲自处理,并通过回调函数返回到上层调用;如果是 I/O 任务,就从线程池中拿出一个线程来处理这个事件,并指定回调函数,然后继续循环队列中的其他事件。
当线程中的 I/O 任务完成以后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,等待事件循环,当主线程再次循环到该事件时,就直接处理并返回给上层调用。 这个过程就叫 事件循环 (Event Loop)。
无论是 Linux 平台还是 Windows 平台,Node.js 内部都是通过线程池来完成异步 I/O 操作的,而 LIBUV 针对不同平台的差异性实现了统一调用。因此,Node.js 的单线程仅仅是指 JavaScript 运行在单线程中,而并非 Node.js 是单线程。
Node.JS 的事件循环分为 6 个阶段:
- timers 阶段:这个阶段执行 timer( setTimeout、setInterval )的回调
- I/O callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调
- idle、prepare 阶段:仅 Node.js 内部使用
- poll 阶段:获取新的 I/O 事件, 适当的条件下 Node.js 将阻塞在这里
- check 阶段:执行 setImmediate( ) 的回调
- close callbacks 阶段:执行 socket 的 close 事件回调
事件循环的执行顺序为:
外部输入数据 –-> 轮询阶段( poll )-–> 检查阶段( check )-–> 关闭事件回调阶段( close callback )–-> 定时器检测阶段( timer )–-> I/O 事件回调阶段( I/O callbacks )-–>闲置阶段( idle、prepare )–->轮询阶段(按照该顺序反复运行)...
浏览器和 Node.js 环境下,微任务任务队列的执行时机不同
- Node.js 端,微任务在事件循环的各个阶段之间执行
- 浏览器端,微任务在事件循环的宏任务执行完之后执行
Node.js v11.0.0 版本于 2018 年 10 月,主要有以下变化:
- V8 引擎更新至版本 7.0
- http、https 和 tls 模块默认使用 WHESWG URL 解析器。
- 隐藏子进程的控制台窗口默认改为了 true。
- FreeBSD 10不再支持。
- 增加了多线程 Worker Threads
# 14. 什么是函数柯里化?
参考答案:
柯里化(currying)又称部分求值。一个柯里化的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
举个例子,就是把原本:
function(arg1,arg2) 变成 function(arg1)(arg2) > function(arg1,arg2,arg3) 变成 function(arg1)(arg2)(arg3) > function(arg1,arg2,arg3,arg4) 变成 function(arg1)(arg2)(arg3)(arg4)
总而言之,就是将:
function(arg1,arg2,…,argn) 变成 function(arg1)(arg2)…(argn)
# 15. promise.all 方法的使用场景?数组中必须每一项都是 promise 对象吗?不是 promise 对象会如何处理 ?
参考答案:
promise.all(promiseArray) 方法是 promise 对象上的静态方法,该方法的作用是将多个 promise 对象实例包装,生成并返回一个新的 promise 实例。
此方法在集合多个 promise 的返回结果时很有用。
返回值将会按照参数内的 promise 顺序排列,而不是由调用 promise 的完成顺序决定。
promise.all 的特点
接收一个
Promise
实例的数组或具有Iterator接口的对象如果元素不是
Promise
对象,则使用Promise.resolve
转成Promise
对象如果全部成功,状态变为
resolved
,返回值将组成一个数组传给回调只有有一个失败,状态就变为
rejected
,返回值将直接传递给回调 **all( )**的返回值,也是新的promise
对象
# 16. this 的指向哪几种 ?
参考答案:
总结起来,this 的指向规律有如下几条:
- 在函数体中,非显式或隐式地简单调用函数时,在严格模式下,函数内的 this 会被绑定到 undefined 上,在非严格模式下则会被绑定到全局对象 window/global 上。
- 一般使用 new 方法调用构造函数时,构造函数内的 this 会被绑定到新创建的对象上。
- 一般通过 call/apply/bind 方法显式调用函数时,函数体内的 this 会被绑定到指定参数的对象上。
- 一般通过上下文对象调用函数时,函数体内的 this 会被绑定到该对象上。
- 在箭头函数中,this 的指向是由外层(函数或全局)作用域来决定的。
# 17. JS 中继承实现的几种方式
参考答案:
JS 的继承随着语言的发展,从最早的对象冒充到现在的圣杯模式,涌现出了很多不同的继承方式。每一种新的继承方式都是对前一种继承方式不足的一种补充。
- 原型链继承
- 重点:让新实例的原型等于父类的实例。
- 特点:实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性。(新实例不会继承父类实例的属性!)
- 缺点:
- 1、新实例无法向父类构造函数传参。
- 2、继承单一。
- 3、所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改!)
- 借用构造函数继承
- 重点:用 call( ) 和 apply( ) 将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))
- 特点: - 1、只继承了父类构造函数的属性,没有继承父类原型的属性。
- 2、解决了原型链继承缺点 1、2、3。
- 3、可以继承多个构造函数属性(call 多个)。
- 4、在子实例中可向父实例传参。
- 缺点: - 1、只能继承父类构造函数的属性。
- 2、无法实现构造函数的复用。(每次用每次都要重新调用)
- 3、每个新实例都有父类构造函数的副本,臃肿。
- 组合模式(又被称之为伪经典模式)
- 重点:结合了两种模式的优点,传参和复用
- 特点: - 1、可以继承父类原型上的属性,可以传参,可复用。 - 2、每个新实例引入的构造函数属性是私有的。
- 缺点:调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。
- 寄生组合式继承(圣杯模式)
- 重点:修复了组合继承的问题
# 18. 什么是事件监听
参考答案:
首先需要区别清楚事件监听和事件监听器。
在绑定事件的时候,我们需要对应的书写一个事件处理程序,来应对事件发生时的具体行为。
这个事件处理程序我们也称之为事件监听器。
当事件绑定好后,程序就会对事件进行监听,当用户触发事件时,就会执行对应的事件处理程序。
关于事件监听,W3C 规范中定义了 3 个事件阶段,依次是捕获阶段、目标阶段、冒泡阶段。
捕获阶段:在事件对象到达事件目标之前,事件对象必须从 window 经过目标的祖先节点传播到事件目标。 这个阶段被我们称之为捕获阶段。在这个阶段注册的事件监听器在事件到达其目标前必须先处理事件。
目标 阶段:事件对象到达其事件目标。 这个阶段被我们称为目标阶段。一旦事件对象到达事件目标,该阶段的事件监听器就要对它进行处理。如果一个事件对象类型被标志为不能冒泡。那么对应的事件对象在到达此阶段时就会终止传播。
冒泡 阶段:事件对象以一个与捕获阶段相反的方向从事件目标传播经过其祖先节点传播到 window。这个阶段被称之为冒泡阶段。在此阶段注册的事件监听器会对相应的冒泡事件进行处理。
# 19. 什么是 js 的闭包?有什么作用?
参考答案:
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
闭包的用处:
- 匿名自执行函数
- 结果缓存
- 封装
- 实现类和继承
# 20. 事件委托以及冒泡原理
参考答案:
事件委托,又被称之为事件代理。在 JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面整体的运行性能。导致这一问题的原因是多方面的。
首先,每个函数都是对象,都会占用内存。内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 DOM 访问次数,会延迟整个页面的交互就绪时间。
对事件处理程序过多问题的解决方案就是事件委托。
事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,click 事件会一直冒泡到 document 层次。也就是说,我们可以为整个页面指定一个 onclick 事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。
事件冒泡(event bubbling),是指事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。
# 21. let const var 的区别?什么是块级作用域?如何用?
参考答案:
- var 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问,有变量提升。
- let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问,无变量提升,不可以重复声明。
- const 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改,无变量提升,不可以重复声明。
最初在 JS 中作用域有:全局作用域、函数作用域。没有块作用域的概念。
ES6 中新增了块级作用域。块作用域由 { } 包括,if 语句和 for 语句里面的 { } 也属于块作用域。
在以前没有块作用域的时候,在 if 或者 for 循环中声明的变量会泄露成全局变量,其次就是 { } 中的内层变量可能会覆盖外层变量。块级作用域的出现解决了这些问题。
# 22. ES5 的方法实现块级作用域(立即执行函数) ES6 呢?
参考答案:
ES6 原生支持块级作用域。块作用域由 { } 包括,if 语句和 for 语句里面的 { } 也属于块作用域。
使用 let 声明的变量或者使用 const 声明的常量,只能在块作用域里访问,不能跨块访问。
# 23. ES6 箭头函数的特性
参考答案:
- 更简洁的语法,例如
- 只有一个形参就不需要用括号括起来
- 如果函数体只有一行,就不需要放到一个块中
- 如果 return 语句是函数体内唯一的语句,就不需要 return 关键字
- 箭头函数没有自己的 this,arguments,super
- 箭头函数 this 只会从自己的作用域链的上一层继承 this。
# 24. 箭头函数与普通函数的区别 ?
参考答案:
外形不同。箭头函数使用箭头定义,普通函数中没有
普通函数可以有匿名函数,也可以有具体名函数,但是箭头函数都是匿名函数。
**箭头函数不能用于构造函数,不能使用 new,**普通函数可以用于构造函数,以此创建对象实例。
**箭头函数中 this 的指向不同,**在普通函数中,this 总是指向调用它的对象,如果用作构造函数,this 指向创建的对象实例。 箭头函数本身不创建 this,也可以说箭头函数本身没有 this,但是它在声明时可以捕获其所在上下文的 this 供自己使用。
每一个普通函数调用后都具有一个 arguments 对象,用来存储实际传递的参数。
但是箭头函数并没有此对象。取而代之用 rest 参数来解决。
箭头函数不能用于 Generator 函数,不能使用 yeild 关键字。
箭头函数不具有 prototype 原型对象。而普通函数具有 prototype 原型对象。
箭头函数不具有 super。
箭头函数不具有 new.target。
# 25. JS 的基本数据类型有哪些?基本数据类型和引用数据类型的区别
参考答案:
在 JavaScript 中,数据类型整体上来讲可以分为两大类:基本类型和引用数据类型
基本数据类型,一共有 8 种:
string,symbol,number,boolean,undefined,null,bigInt
1其中 symbol、bigInt 类型是在 ES6 及后续版本里面新添加的基本数据类型。
引用数据类型,就只有 1 种:
object;
1基本数据类型的值又被称之为原始值或简单值,而引用数据类型的值又被称之为复杂值或引用值。
两者的区别在于:
原始值是表示 JavaScript 中可用的数据或信息的最底层形式或最简单形式。简单类型的值被称为原始值,是因为它们是不可细化的。
也就是说,数字是数字,字符是字符,布尔值是 true 或 false,null 和 undefined 就是 null 和 undefined。这些值本身很简单,不能够再进行拆分。由于原始值的数据大小是固定的,所以原始值的数据是存储于内存中的栈区里面的。
在 JavaScript 中,对象就是一个引用值。因为对象可以向下拆分,拆分成多个简单值或者复杂值。引用值在内存中的大小是未知的,因为引用值可以包含任何值,而不是一个特定的已知值,所以引用值的数据都是存储于堆区里面。
最后总结一下两者的区别:
访问方式
- 原始值:访问到的是值
- 引用值:访问到的是引用地址
比较方式
- 原始值:比较的是值
- 引用值:比较的是地址
动态属性
- 原始值:无法添加动态属性
- 引用值:可以添加动态属性
变量赋值
- 原始值:赋值的是值
- 引用值:赋值的是地址
# 26. NaN 是什么的缩写
参考答案:
NaN 的全称为 Not a Number,表示非数,或者说不是一个数。虽然 NaN 表示非数,但是它却属于 number 类型。
NaN 有两个特点:
- 任何涉及 NaN 的操作都会返回 NaN
- NaN 和任何值都不相等,包括它自己本身
# 27. JS 的作用域类型
参考答案:
在 JavaScript 里面,作用域一共有 4 种:全局作用域,局部作用域、函数作用域以及 eval 作用域。
**全局作用域:**这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。
**局部作用域:**当使用 let 或者 const 声明变量时,这些变量在一对花括号中存在局部作用域,只能够在花括号内部进行访问使用。
**函数作用域:**当进入到一个函数的时候,就会产生一个函数作用域。函数作用域里面所声明的变量只在函数中提供访问使用。
**eval 作用域:**当调用 eval( ) 函数的时候,就会产生一个 eval 作用域。
# 28. undefined==null 返回的结果是什么?undefined 与 null 的区别在哪?
参考答案:
返回 true。
这两个值都表示“无”的意思。
通常情况下, 当我们试图访问某个不存在的或者没有赋值的变量时,就会得到一个 undefined 值。Javascript 会自动将声明是没有进行初始化的变量设为 undifined。
而 null 值表示空,null 不能通过 Javascript 来自动赋值,也就是说必须要我们自己手动来给某个变量赋值为 null。
解析:
那么为什么 JavaScript 要设置两个表示"无"的值呢?这其实是历史原因。
1995 年 JavaScript 诞生时,最初像 Java 一样,只设置了 null 作为表示"无"的值。根据 C 语言的传统,null 被设计成可以自动转为0。
但是,JavaScript 的设计者,觉得这样做还不够,主要有以下两个原因。
- null 像在 Java 里一样,被当成一个对象。但是,JavaScript 的数据类型分成原始类型(primitive)和合成类型(complex)两大类,作者觉得表示"无"的值最好不是对象。
- JavaScript 的最初版本没有包括错误处理机制,发生数据类型不匹配时,往往是自动转换类型或者默默地失败。作者觉得,如果 null 自动转为 0,很不容易发现错误。
因此,作者又设计了一个 undefined。
这里注意:先有 null 后有 undefined 出来,undefined 是为了填补之前的坑。
JavaScript 的最初版本是这样区分的:
null 是一个表示"无"的对象(空对象指针),转为数值时为 0;
典型用法是:
作为函数的参数,表示该函数的参数不是对象。
作为对象原型链的终点。
undefined 是一个表示"无"的原始值,转为数值时为 NaN。
典型用法是:
- 变量被声明了,但没有赋值时,就等于 undefined。
- 调用函数时,应该提供的参数没有提供,该参数等于 undefined。
- 对象没有赋值的属性,该属性的值为 undefined。
- 函数没有返回值时,默认返回 undefined。
# 29. 写一个函数判断变量类型
参考答案:
function getType(data) { let type = typeof data; if (type !== "object") { return type; } return Object.prototype.toString.call(data).replace(/^\[object (\S+)\]$/, "$1"); } function Person() {} console.log(getType(1)); // number console.log(getType(true)); // boolean console.log(getType([1, 2, 3])); // Array console.log(getType(/abc/)); // RegExp console.log(getType(new Date())); // Date console.log(getType(new Person())); // Object console.log(getType({})); // Object
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 30. js 的异步处理函数
参考答案:
在最早期的时候,JavaScript 中要实现异步操作,使用的就是 Callback 回调函数。
但是回调函数会产生回调地狱(Callback Hell)
之后 ES6 推出了 Promise 解决方案来解决回调地狱的问题。不过,虽然 Promise 作为 ES6 中提供的一种新的异步编程解决方案,但是它也有问题。比如,代码并没有因为新方法的出现而减少,反而变得更加复杂,同时理解难度也加大。
之后,就出现了基于 Generator 的异步解决方案。不过,这种方式需要编写外部的执行器,而执行器的代码写起来一点也不简单。当然也可以使用一些插件,比如 co 模块来简化执行器的编写。
ES7 提出的 async 函数,终于让 JavaScript 对于异步操作有了终极解决方案。
实际上,async 只是生成器的一种语法糖而已,简化了外部执行器的代码,同时利用 await 替代 yield,async 替代生成器的
*
号。
# 31. defer 与 async 的区别
参考答案:
按照惯例,所有 script 元素都应该放在页面的 head 元素中。这种做法的目的就是把所有外部文件(CSS 文件和 JavaScript 文件)的引用都放在相同的地方。可是,在文档的 head 元素中包含所有 JavaScript 文件,意味着必须等到全部 JavaScript 代码都被下载、解析和执行完成以后,才能开始呈现页面的内容(浏览器在遇到 body 标签时才开始呈现内容)。
对于那些需要很多 JavaScript 代码的页面来说,这无疑会导致浏览器在呈现页面时出现明显的延迟,而延迟期间的浏览器窗口中将是一片空白。为了避免这个问题,现在 Web 应用程序一般都全部 JavaScript 引用放在 body 元素中页面的内容后面。这样一来,在解析包含的 JavaScript 代码之前,页面的内容将完全呈现在浏览器中。而用户也会因为浏览器窗口显示空白页面的时间缩短而感到打开页面的速度加快了。
有了 defer 和 async 后,这种局面得到了改善。
defer (延迟脚本)
延迟脚本:defer 属性只适用于外部脚本文件。
如果给 script 标签定义了defer 属性,这个属性的作用是表明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,如果 script 元素中设置了 defer 属性,相当于告诉浏览器立即下载,但延迟执行。
async(异步脚本)
异步脚本:async 属性也只适用于外部脚本文件,并告诉浏览器立即下载文件。
但与 defer 不同的是:标记为 async 的脚本并不保证按照指定它们的先后顺序执行。
所以总结起来,两者之间最大的差异就是在于脚本下载完之后何时执行,显然 defer 是最接近我们对于应用脚本加载和执行的要求的。
defer 是立即下载但延迟执行,加载后续文档元素的过程将和脚本的加载并行进行(异步),但是脚本的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。async 是立即下载并执行,加载和渲染后续文档元素的过程将和 js 脚本的加载与执行并行进行(异步)。
# 32. 浏览器事件循环和任务队列
参考答案:
JavaScript 的异步机制由事件循环和任务队列构成。
JavaScript 本身是单线程语言,所谓异步依赖于浏览器或者操作系统等完成。JavaScript 主线程拥有一个执行栈以及一个任务队列,主线程会依次执行代码,当遇到函数时,会先将函数入栈,函数运行完毕后再将该函数出栈,直到所有代码执行完毕。
遇到异步操作(例如:setTimeout、Ajax)时,异步操作会由浏览器(OS)执行,浏览器会在这些任务完成后,将事先定义的回调函数推入主线程的任务队列(task queue)中,当主线程的执行栈清空之后会读取任务队列中的回调函数,当任务队列被读取完毕之后,主线程接着执行,从而进入一个无限的循环,这就是事件循环。
# 33. 原型与原型链 (美团 19 年)
参考答案:
- 每个对象都有一个
__proto__
属性,该属性指向自己的原型对象- 每个构造函数都有一个
prototype
属性,该属性指向实例对象的原型对象- 原型对象里的
constructor
指向构造函数本身如下图:
![]()
每个对象都有自己的原型对象,而原型对象本身,也有自己的原型对象,从而形成了一条原型链条。
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
# 34. 作用域与作用域链 (美团 19 年)
参考答案:
作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供了块级作用域。
作用域链指的是作用域与作用域之间形成的链条。当我们查找一个当前作用域没有定义的变量(自由变量)的时候,就会向上一级作用域寻找,如果上一级也没有,就再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链 。
# 35. 闭包及应用场景以及闭包缺点 (美团 19 年)
参考答案:
闭包的应用场景:
- 匿名自执行函数
- 结果缓存
- 封装
- 实现类和继承
闭包的缺点:
因为闭包的作用域链会引用包含它的函数的活动对象,导致这些活动对象不会被销毁,因此会占用更多的内存。
# 36. 继承方式 (美团 19 年)
参考答案:
参阅前面第 9 题以及第 18 题答案。
# 37. 原始值与引用值 (美团 19 年)
参考答案:
原始值是表示 JavaScript 中可用的数据或信息的最底层形式或最简单形式。简单类型的值被称为原始值,是因为它们是不可细化的。
也就是说,数字是数字,字符是字符,布尔值是 true 或 false,null 和 undefined 就是 null 和 undefined。这些值本身很简单,不能够再进行拆分。由于原始值的数据大小是固定的,所以原始值的数据是存储于内存中的栈区里面的。
在 JavaScript 中,对象就是一个引用值。因为对象可以向下拆分,拆分成多个简单值或者复杂值。引用值在内存中的大小是未知的,因为引用值可以包含任何值,而不是一个特定的已知值,所以引用值的数据都是存储于堆区里面。
最后总结一下两者的区别:
访问方式
- 原始值:访问到的是值
- 引用值:访问到的是引用地址
比较方式
- 原始值:比较的是值
- 引用值:比较的是地址
动态属性
- 原始值:无法添加动态属性
- 引用值:可以添加动态属性
变量赋值
- 原始值:赋值的是值
- 引用值:赋值的是地址
# 38. 描述下列代码的执行结果
const first = () =>
new Promise((resolve, reject) => {
console.log(3);
let p = new Promise((resolve, reject) => {
console.log(7);
setTimeout(() => {
console.log(1);
}, 0);
setTimeout(() => {
console.log(2);
resolve(3);
}, 0);
resolve(4);
});
resolve(2);
p.then((arg) => {
console.log(arg, 5); // 1 bb
});
setTimeout(() => {
console.log(6);
}, 0);
});
first().then((arg) => {
console.log(arg, 7); // 2 aa
setTimeout(() => {
console.log(8);
}, 0);
});
setTimeout(() => {
console.log(9);
}, 0);
console.log(10);
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
参考答案:
3 7 10 4 5 2 7 1 2 6 9 8
# 39. 如何判断数组或对象(美团 19 年)
参考答案:
- 通过 instanceof 进行判断
var arr = [1, 2, 3, 1]; console.log(arr instanceof Array); // true
1
2
- 通过对象的 constructor 属性
var arr = [1, 2, 3, 1]; console.log(arr.constructor === Array); // true
1
2
- Object.prototype.toString.call(arr)
console.log(Object.prototype.toString.call({ name: "jerry" })); //[object Object] console.log(Object.prototype.toString.call([])); //[object Array]
1
2
- 可以通过 ES6 新提供的方法 Array.isArray( )
Array.isArray([]); //true
1
# 40. 对象深拷贝与浅拷贝,单独问了 Object.assign(美团 19 年)
参考答案:
浅拷贝:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做浅拷贝(浅复制)
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
深拷贝:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。
Object.assign 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign 方法进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。
# 42. 说说 instanceof 原理,并回答下面的题目(美团 19 年)
function A() {}
function B() {}
A.prototype = new B();
let a = new A();
console.log(a instanceof B); // true of false ?
2
3
4
5
参考答案:
答案为 true。
instanceof 原理:
instanceof 用于检测一个对象是否为某个构造函数的实例。
例如:A instanceof B > instanceof 用于检测对象 A 是不是 B 的实例,而检测是基于原型链进行查找的,也就是说 B 的 prototype 有没有在对象 A 的__proto__ 原型链上,如果有就返回 true,否则返回 false
#
# 43. 内存泄漏(美团 19 年)
参考答案:
内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
Javascript 是一种高级语言,它不像 C 语言那样要手动申请内存,然后手动释放,Javascript 在声明变量的时候自动会分配内存,普通的类型比如 number,一般放在栈内存里,对象放在堆内存里,声明一个变量,就分配一些内存,然后定时进行垃圾回收。垃圾回收的任务由 JavaScript 引擎中的垃圾回收器来完成,它监视所有对象,并删除那些不可访问的对象。
基本的垃圾回收算法称为**“标记-清除”**,定期执行以下“垃圾回收”步骤:
- 垃圾回收器获取根并**“标记”**(记住)它们。
- 然后它访问并“标记”所有来自它们的引用。
- 然后它访问标记的对象并标记它们的引用。所有被访问的对象都被记住,以便以后不再访问同一个对象两次。
- 以此类推,直到有未访问的引用(可以从根访问)为止。
- 除标记的对象外,所有对象都被删除。
# 44. ES6 新增哪些东西?让你自己说(美团 19 年)
参考答案:
ES6 新增内容众多,这里列举出一些关键的以及平时常用的新增内容:
- 箭头函数
- 字符串模板
- 支持模块化(import、export)
- 类(class、constructor、extends)
- let、const 关键字
- 新增一些数组、字符串等内置构造函数方法,例如 Array.from、Array.of 、Math.sign、Math.trunc 等
- 新增一些语法,例如扩展操作符、解构、函数默认参数等
- 新增一种基本数据类型 Symbol
- 新增元编程相关,例如 proxy、Reflect
- Set 和 Map 数据结构
- Promise
- Generator 生成器
# 45. weakmap、weakset(美团 19 年)
参考答案:
WeakSet 对象是一些对象值的集合, 并且其中的每个对象值都只能出现一次。在 WeakSet 的集合中是唯一的
它和 Set 对象的区别有两点:
- 与 Set 相比,WeakSet 只能是对象的集合,而不能是任何类型的任意值。
- WeakSet 持弱引用:集合中对象的引用为弱引用。 如果没有其他的对 WeakSet 中对象的引用,那么这些对象会被当成垃圾回收掉。 这也意味着 WeakSet 中没有存储当前对象的列表。 正因为这样,WeakSet 是不可枚举的。
WeakMap 对象也是键值对的集合。它的键必须是对象类型,值可以是任意类型。它的键被弱保持,也就是说,当其键所指对象没有其他地方引用的时候,它会被 GC 回收掉。WeakMap 提供的接口与 Map 相同。
与 Map 对象不同的是,WeakMap 的键是不可枚举的。不提供列出其键的方法。列表是否存在取决于垃圾回收器的状态,是不可预知的。
# 46. 为什么 ES6 会新增 Promise(美团 19 年)
参考答案:
在 ES6 以前,解决异步的方法是回调函数。但是回调函数有一个最大的问题就是回调地狱(callback hell),当我们的回调函数嵌套的层数过多时,就会导致代码横向发展。
Promise 的出现就是为了解决回调地狱的问题。
# 47. ES5 实现继承?(虾皮)
参考答案:
- 借用构造函数实现继承
function Parent1() { this.name = "parent1"; } function Child1() { Parent1.call(this); this.type = "child1"; }
1
2
3
4
5
6
7缺点:Child1 无法继承 Parent1 的原型对象,并没有真正的实现继承 (部分继承)。
- 借用原型链实现继承
function Parent2() { this.name = "parent2"; this.play = [1, 2, 3]; } function Child2() { this.type = "child2"; } Child2.prototype = new Parent2();
1
2
3
4
5
6
7
8缺点:原型对象的属性是共享的。
- 组合式继承
function Parent3() { this.name = "parent3"; this.play = [1, 2, 3]; } function Child3() { Parent3.call(this); this.type = "child3"; } Child3.prototype = Object.create(Parent3.prototype); Child3.prototype.constructor = Child3;
1
2
3
4
5
6
7
8
9
10
# 48. 科里化?(搜狗)
参考答案:
柯里化,英语全称 Currying,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
举个例子,就是把原本:
function(arg1,arg2) 变成 function(arg1)(arg2) > function(arg1,arg2,arg3) 变成 function(arg1)(arg2)(arg3) > function(arg1,arg2,arg3,arg4) 变成 function(arg1)(arg2)(arg3)(arg4)
总而言之,就是将:
function(arg1,arg2,…,argn) 变成 function(arg1)(arg2)…(argn)
# 49. 防抖和节流?(虾皮)
参考答案:
我们在平时开发的时候,会有很多场景会频繁触发事件,比如说搜索框实时发请求,onmousemove、resize、onscroll 等,有些时候,我们并不能或者不想频繁触发事件,这时候就应该用到函数防抖和函数节流。
函数防抖(debounce),指的是短时间内多次触发同一事件,只执行最后一次,或者只执行最开始的一次,中间的不执行。
函数节流(throttle),指连续触发事件但是在 n 秒中只执行一次函数。即 2n 秒内执行 2 次... 。节流如字面意思,会稀释函数的执行频率。
# 50. 闭包?(好未来---探讨了 40 分钟)
参考答案:
请参阅前面第 20 题以及第 36 题答案。
# 51. 原型和原型链?(字节)
参考答案:
请参阅前面第 34 题答案。
# 52. 排序算法---(时间复杂度、空间复杂度)
参考答案:
算法(Algorithm)是指用来操作数据、解决程序问题的一组方法。对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。
主要还是从算法所占用的「时间」和「空间」两个维度去考量。
- 时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。
- 空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述。
因此,评价一个算法的效率主要是看它的时间复杂度和空间复杂度情况。然而,有的时候时间和空间却又是「鱼和熊掌」,不可兼得的,那么我们就需要从中去取一个平衡点。
排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程。
排序的分类分为内部排序和外部排序法。
- 内部排序:指将需要处理的所有数据都加载到**内部存储器(内存)**中进行排序。
- 外部排序:数据量过大,无法全部加载到内存中,需要借助**外部存储(文件等)**进行排序。
# 53. 浏览器事件循环和 node 事件循环(搜狗)
参考答案:
- 浏览器中的 Event Loop
事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。宏任务队列可以有多个,微任务队列只有一个。
- 常见的 macro-task 比如:setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作、UI 渲染等。
- 常见的 micro-task 比如: process.nextTick、new Promise( ).then(回调)、MutationObserver(html5 新特性) 等。
当某个宏任务执行完后,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务,如果没有,会读取宏任务队列中排在最前的任务,执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,依次类推。
- Node 中的事件循环
Node 中的 Event Loop 和浏览器中的是完全不相同的东西。Node.js 采用 V8 作为 js 的解析引擎,而 I/O 处理方面使用了自己设计的 libuv,libuv 是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的 API,事件循环机制也是它里面的实现。
Node.JS 的事件循环分为 6 个阶段:
- timers 阶段:这个阶段执行 timer( setTimeout、setInterval )的回调
- I/O callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调
- idle、prepare 阶段:仅 Node.js 内部使用
- poll 阶段:获取新的 I/O 事件, 适当的条件下 Node.js 将阻塞在这里
- check 阶段:执行 setImmediate( ) 的回调
- close callbacks 阶段:执行 socket 的 close 事件回调
Node.js 的运行机制如下:
- V8 引擎解析 JavaScript 脚本。
- 解析后的代码,调用 Node API。
- libuv 库负责 Node API 的执行。它将不同的任务分配给不同的线程,形成一个 Event Loop(事件循环),以异步的方式将任务的执行结果返回给 V8 引擎。
- V8 引擎再将结果返回给用户。
# 54. 闭包的好处
参考答案:
请参阅前面第 20 题以及第 36 题答案。
# 55. let、const、var 的区别
参考答案:
- var 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问,有变量提升。
- let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问,无变量提升,不可以重复声明。
- const 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改,无变量提升,不可以重复声明。
# 56. 闭包、作用域(可以扩充到作用域链)
参考答案:
什么是作用域?
ES5 中只存在两种作用域:全局作用域和函数作用域。在 JavaScript 中,我们将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套子作用域中根据标识符名称进行变量(变量名或者函数名)查找。
什么是作用域链?
当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没找到继续向上查找,直到全局作用域为止,,而作用域链,就是有当前作用域与上层作用域的一系列变量对象组成,它保证了当前执行的作用域对符合访问权限的变量和函数的有序访问。
闭包产生的本质
当前环境中存在指向父级作用域的引用
什么是闭包
闭包是一种特殊的对象,它由两部分组成:执行上下文(代号 A),以及在该执行上下文中创建的函数 (代号 B),当 B 执行时,如果访问了 A 中变量对象的值,那么闭包就会产生,且在 Chrome 中使用这个执行上下文 A 的函数名代指闭包。
一般如何产生闭包
- 返回函数
- 函数当做参数传递
闭包的应用场景
- 柯里化 bind
- 模块
# 57. Promise
参考答案:
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理且更强大。它最早由社区提出并实现,ES6将其写进了语言标准,统一了用法,并原生提供了Promise对象。
特点
对象的状态不受外界影响 (3 种状态)
Pending 状态(进行中)
Fulfilled 状态(已成功)
Rejected 状态(已失败)
一旦状态改变就不会再变 (两种状态改变:成功或失败)
- Pending -> Fulfilled
- Pending -> Rejected
用法
var promise = new Promise(function(resolve, reject){ // ... some code if (/* 异步操作成功 */) { resolve(value); } else { reject(error); } })
1
2
3
4
5
6
7
8
9
# 58. 实现一个函数,对一个 url 进行请求,失败就再次请求,超过最大次数就走失败回调,任何一次成功都走成功回调
参考答案:
示例代码如下:
/** @params url: 请求接口地址; @params body: 设置的请求体; @params succ: 请求成功后的回调 @params error: 请求失败后的回调 @params maxCount: 设置请求的数量 */ function request(url, body, succ, error, maxCount = 5) { return fetch(url, body) .then((res) => succ(res)) .catch((err) => { if (maxCount <= 0) return error("请求超时"); return request(url, body, succ, error, --maxCount); }); } // 调用请求函数 request( "https://java.some.com/pc/reqCount", { method: "GET", headers: {} }, (res) => { console.log(res.data); }, (err) => { console.log(err); } );
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
# 59. 冒泡排序
参考答案:
冒泡排序的核心思想是:
- 比较相邻的两个元素,如果前一个比后一个大或者小(取决于排序的顺序是小到大还是大到小),则交换位置。
- 比较完第一轮的时候,最后一个元素是最大或最小的元素。
- 这时候最后一个元素已经是最大或最小的了,所以下一次冒泡的时候最后一个元素不需要参与比较。
示例代码:
function bSort(arr) { var len = arr.length; // 外层 for 循环控制冒泡的次数 for (var i = 0; i < len - 1; i++) { for (var j = 0; j < len - 1 - i; j++) { // 内层 for 循环控制每一次冒泡需要比较的次数 // 因为之后每一次冒泡的两两比较次数会越来越少,所以 -i if (arr[j] > arr[j + 1]) { var temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } return arr; } //举个数组 myArr = [20, -1, 27, -7, 35]; //使用函数 console.log(bSort(myArr)); // [ -7, -1, 20, 27, 35 ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 60. 数组降维
参考答案:
数组降维就是将一个嵌套多层的数组进行降维操作,也就是对数组进行扁平化。在 ES5 时代我们需要自己手写方法或者借助函数库来完成,但是现在可以使用 ES6 新提供的数组方法 flat 来完成数组降维操作。
解析:使用 flat 方法会接收一个参数,这个参数是数值类型,是要处理扁平化数组的深度,生成后的新数组是独立存在的,不会对原数组产生影响。
flat 方法的语法如下:
var newArray = arr.flat([depth]);
1其中 depth 指定要提取嵌套数组结构的深度,默认值为 1。
示例如下:
var arr = [1, 2, [3, 4, [5, 6]]]; console.log(arr.flat()); // [1, 2, 3, 4, [5, 6]] console.log(arr.flat(2)); // [1, 2, 3, 4, 5, 6]
1
2
3上面的代码定义了一个层嵌套的数组,默认情况下只会拍平一层数组,也就是把原来的三维数组降低到了二维数组。在传入的参数为 2 时,则会降低两维,成为一个一维数组。
使用 Infinity,可展开任意深度的嵌套数组,示例如下:
var arr = [1, 2, [3, 4, [5, 6, [7, 8]]]]; console.log(arr.flat(Infinity)); // [1, 2, 3, 4, 5, 6, 7, 8]
1
2在数组中有空项的时候,使用 flat 方法会将中的空项进行移除。
var arr = [1, 2, , 4, 5]; console.log(arr.flat()); // [1, 2, 4, 5]
1
2上面的代码中,数组中第三项是空值,在使用 flat 后会对空项进行移除。
# 61. call apply bind
参考答案:
请参阅前面第 11 题答案。
# 62. promise 代码题
new Promise((resolve, reject) => {
reject(1);
console.log(2);
resolve(3);
console.log(4);
})
.then((res) => {
console.log(res);
})
.catch((res) => {
console.log("reject1");
});
try {
new Promise((resolve, reject) => {
throw "error";
})
.then((res) => {
console.log(res);
})
.catch((res) => {
console.log("reject2");
});
} catch (err) {
console.log(err);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
参考答案:
2 4 reject1 reject2
直播课或者录播课进行解析。
# 63. proxy 是实现代理,可以改变 js 底层的实现方式, 然后说了一下和 Object.defineProperty 的区别
参考答案:
两者的区别总结如下:
- 代理原理:Object.defineProperty 的原理是通过将数据属性转变为存取器属性的方式实现的属性读写代理。而 Proxy 则是因为这个内置的 Proxy 对象内部有一套监听机制,在传入 handler 对象作为参数构造代理对象后,一旦代理对象的某个操作触发,就会进入 handler 中对应注册的处理函数,此时我们就可以有选择的使用 Reflect 将操作转发被代理对象上。
- 代理局限性:Object.defineProperty 始终还是局限于属性层面的读写代理,对于对象层面以及属性的其它操作代理它都无法实现。鉴于此,由于数组对象 push、pop 等方法的存在,它对于数组元素的读写代理实现的并不完全。而使用 Proxy 则可以很方便的监视数组操作。
- 自我代理:Object.defineProperty 方式可以代理到自身(代理之后使用对象本身即可),也可以代理到别的对象身上(代理之后需要使用代理对象)。Proxy 方式只能代理到 Proxy 实例对象上。这一点在其它说法中是 Proxy 对象不需要侵入对象就可以实现代理,实际上 Object.defineProperty 方式也可以不侵入。
# 64. 使用 ES5 与 ES6 分别实现继承
参考答案:
如果是使用 ES5 来实现继承,那么现在的最优解是使用圣杯模式。圣杯模式的核心思想就是不通过调用父类构造函数来给子类原型赋值,而是取得父类原型的一个副本,然后将返回的新对象赋值给子类原型。具体代码可以参阅前面第 9 题的解析。
ES6 新增了 extends 关键字,直接使用该关键字就能够实现继承。
# 65. 深拷贝
参考答案:
有深拷贝就有浅拷贝。
浅拷贝就是只拷贝对象的引用,而不深层次的拷贝对象的值,多个对象指向堆内存中的同一对象,任何一个修改都会使得所有对象的值修改,因为它们共用一条数据。
深拷贝不是单纯的拷贝一份引用数据类型的引用地址,而是将引用类型的值全部拷贝一份,形成一个新的引用类型,这样就不会发生引用错乱的问题,使得我们可以多次使用同样的数据,而不用担心数据之间会起冲突。
解析:
「深拷贝」就是在拷贝数据的时候,将数据的所有引用结构都拷贝一份。简单的说就是,在内存中存在两个数据结构完全相同又相互独立的数据,将引用型类型进行复制,而不是只复制其引用关系。
分析下怎么做「深拷贝」:
- 首先假设深拷贝这个方法已经完成,为 deepClone
- 要拷贝一个数据,我们肯定要去遍历它的属性,如果这个对象的属性仍是对象,继续使用这个方法,如此往复
function deepClone(o1, o2) { for (let k in o2) { if (typeof o2[k] === 'object') { o1[k] = {}; deepClone(o1[k], o2[k]); } else { o1[k] = o2[k]; } } } // 测试用例 let obj = { a: 1, b: [1, 2, 3], c: {} }; let emptyObj = Object.create(null); deepClone(emptyObj, obj); console.log(emptyObj.a == obj.a); console.log(emptyObj.b == obj.b);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20递归容易造成爆栈,尾部调用可以解决递归的这个问题,Chrome 的 V8 引擎做了尾部调用优化,我们在写代码的时候也要注意尾部调用写法。递归的爆栈问题可以通过将递归改写成枚举的方式来解决,就是通过 for 或者 while 来代替递归。
# 66. async 与 await 的作用
参考答案:
async 是一个修饰符,async 定义的函数会默认的返回一个 Promise 对象 resolve 的值,因此对 async 函数可以直接进行 then 操作,返回的值即为 then 方法的传入函数。
await 关键字只能放在 async 函数内部, await 关键字的作用就是获取 Promise 中返回的内容, 获取的是 Promise 函数中 resolve 或者 reject 的值。
# 67. 数据的基础类型(原始类型)有哪些
参考答案:
JavaScript 中的基础数据类型,一共有 7 种:
string,symbol,number,boolean,undefined,null,bigInt
# 68. typeof null 返回结果
参考答案:
返回 object
解析:至于为什么会返回 object,这实际上是来源于 JavaScript 从第一个版本开始时的一个 bug,并且这个 bug 无法被修复。修复会破坏现有的代码。
原理这是这样的,不同的对象在底层都表现为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判断为 object 类型,null 的二进制全部为 0,自然前三位也是 0,所以执行 typeof 值会返回 object。
# 69. 对变量进行类型判断的方式有哪些
参考答案:
常用的方法有 4 种:
- typeof
typeof 是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括以下 7 种:number、boolean、symbol、string、object、undefined、function 等。
- instanceof
instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型。
- constructor
当一个函数 F 被定义时,JS 引擎会为 F 添加 prototype 原型,然后再在 prototype 上添加一个 constructor 属性,并让其指向 F 的引用。
- toString
toString( ) 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。
对于 Object 对象,直接调用 toString( ) 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。例如:
Object.prototype.toString.call(""); // [object String] Object.prototype.toString.call(1); // [object Number] Object.prototype.toString.call(true); // [object Boolean] Object.prototype.toString.call(Symbol()); //[object Symbol] Object.prototype.toString.call(undefined); // [object Undefined] Object.prototype.toString.call(null); // [object Null]
1
2
3
4
5
6
# 70. typeof 与 instanceof 的区别? instanceof 是如何实现?
参考答案:
- typeof
typeof 是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括以下 7 种:number、boolean、symbol、string、object、undefined、function 等。
- instanceof
instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型。
用一段伪代码来模拟其内部执行过程:
instanceof (A,B) = { varL = A.__proto__; varR = B.prototype; if(L === R) { // A的内部属性 __proto__ 指向 B 的原型对象 return true; } return false; }
1
2
3
4
5
6
7
8
9从上述过程可以看出,当 A 的 __proto__ 指向 B 的 prototype 时,就认为 A 就是 B 的实例。
需要注意的是,instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。
例如:[ ] instanceof Object 返回的也会是 true。
# 71. 引用类型有哪些,有什么特点
参考答案:
JS 中七种内置类型(null,undefined,boolean,number,string,symbol,object)又分为两大类型
两大类型:
- 基本类型:
null
,undefined
,boolean
,number
,string
,symbol
- 引用类型 Object:
Array
,Function
,Date
,RegExp
等![]()
基本类型和引用类型的主要区别有以下几点:
存放位置:
- 基本数据类型:基本类型值在内存中占据固定大小,直接存储在栈内存中的数据
- 引用数据类型:引用类型在栈中存储了指针,这个指针指向堆内存中的地址,真实的数据存放在堆内存里。
![]()
值的可变性:
基本数据类型: 值不可变,javascript 中的原始值(undefined、null、布尔值、数字和字符串)是不可更改的
引用数据类型:引用类型是可以直接改变其值的
比较:
基本数据类型: 基本类型的比较是值的比较,只要它们的值相等就认为他们是相等的
引用数据类型: 引用数据类型的比较是引用的比较,看其的引用是否指向同一个对象
# 72. 如何得到一个变量的类型---指函数封装实现
参考答案:
请参阅前面第 30 题答案。
# 73. 什么是作用域、闭包
参考答案:
请参阅前面第 56 题。
# 74. 闭包的缺点是什么?闭包的应用场景有哪些?怎么销毁闭包?
参考答案:
闭包是指有权访问另外一个函数作用域中的变量的函数。
因为闭包引用着另一个函数的变量,导致另一个函数已经不使用了也无法销毁,所以闭包使用过多,会占用较多的内存,这也是一个副作用,内存泄漏。
如果要销毁一个闭包,可以 把被引用的变量设置为null,即手动清除变量,这样下次 js 垃圾回收机制回收时,就会把设为 null 的量给回收了。
闭包的应用场景:
- 匿名自执行函数
- 结果缓存
- 封装
- 实现类和继承
# 75. JS的垃圾回收站机制
参考答案:
JS 具有自动垃圾回收机制。垃圾收集器会按照固定的时间间隔周期性的执行。
JS 常见的垃圾回收方式:标记清除、引用计数方式。
1、标记清除方式:
工作原理:当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。
工作流程:
垃圾回收器,在运行的时候会给存储在内存中的所有变量都加上标记;
去掉环境中的变量以及被环境中的变量引用的变量的标记;
被加上标记的会被视为准备删除的变量;
垃圾回收器完成内存清理工作,销毁那些带标记的值并回收他们所占用的内存空间。
2、引用计数方式:
工作原理:跟踪记录每个值被引用的次数。
工作流程:
声明了一个变量并将一个引用类型的值赋值给这个变量,这个引用类型值的引用次数就是 1;
同一个值又被赋值给另一个变量,这个引用类型值的引用次数加 1;
当包含这个引用类型值的变量又被赋值成另一个值了,那么这个引用类型值的引用次数减 1;
当引用次数变成 0 时,说明没办法访问这个值了;
当垃圾收集器下一次运行时,它就会释放引用次数是 0 的值所占的内存。
# 76. 什么是作用域链、原型链
参考答案:
什么是作用域链?
当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没找到继续向上查找,直到全局作用域为止,,而作用域链,就是有当前作用域与上层作用域的一系列变量对象组成,它保证了当前执行的作用域对符合访问权限的变量和函数的有序访问。
什么原型链?
每个对象都可以有一个原型__proto__,这个原型还可以有它自己的原型,以此类推,形成一个原型链。查找特定属性的时候,我们先去这个对象里去找,如果没有的话就去它的原型对象里面去,如果还是没有的话再去向原型对象的原型对象里去寻找。这个操作被委托在整个原型链上,这个就是我们说的原型链。
# 77. new 一个构造函数发生了什么
参考答案:
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。
new 关键字会进行如下的操作:
步骤 1:创建一个空的简单 JavaScript 对象,即 { } ;
步骤 2:链接该对象到另一个对象(即设置该对象的原型对象);
步骤 3:将步骤 1 新创建的对象作为 this 的上下文;
步骤 4:如果该函数没有返回对象,则返回 this。
# 78. 对一个构造函数实例化后. 它的原型链指向什么
参考答案:
指向该构造函数实例化出来对象的原型对象。
对于构造函数来讲,可以通过 prototype 访问到该对象。
对于实例对象来讲,可以通过隐式属性 __proto__ 来访问到。
# 79. 什么是变量提升
参考答案:
当 JavaScript 编译所有代码时,所有使用 var 的变量声明都被提升到它们的函数/局部作用域的顶部(如果在函数内部声明的话),或者提升到它们的全局作用域的顶部(如果在函数外部声明的话),而不管实际的声明是在哪里进行的。这就是我们所说的“提升”。
请记住,这种“提升”实际上并不发生在你的代码中,而只是一种比喻,与 JavaScript 编译器如何读取你的代码有关。记住当我们想到“提升”的时候,我们可以想象任何被提升的东西都会被移动到顶部,但是实际上你的代码并不会被修改。
函数声明也会被提升,但是被提升到了最顶端,所以将位于所有变量声明之上。
在编译阶段变量和函数声明会被放入内存中,但是你在代码中编写它们的位置会保持不变。
# 80. == 和 === 的区别是什么
参考答案:
简单来说: == 代表相同, === 代表严格相同(数据类型和值都相等)。
当进行双等号比较时候,先检查两个操作数数据类型,如果相同,则进行===比较,如果不同,则愿意为你进行一次类型转换,转换成相同类型后再进行比较,而 === 比较时,如果类型不同,直接就是 false。
从这个过程来看,大家也能发现,某些情况下我们使用 === 进行比较效率要高些,因此,没有歧义的情况下,不会影响结果的情况下,在 JS 中首选 === 进行逻辑比较。
# 81. Object.is 方法比较的是什么
参考答案:
Object.is 方法是 ES6 新增的用来比较两个值是否严格相等的方法,与 === (严格相等)的行为基本一致。不过有两处不同:
- +0 不等于 -0。
- NaN 等于自身。
所以可以将Object.is 方法看作是加强版的严格相等。
# 82. 基础数据类型和引用数据类型,哪个是保存在栈内存中?哪个是在堆内存中?
参考答案:
在 ECMAScript 规范中,共定义了 7 种数据类型,分为 基本类型 和 引用类型 两大类,如下所示:
基本类型:String、Number、Boolean、Symbol、Undefined、Null
引用类型:Object
基本类型也称为简单类型,由于其占据空间固定,是简单的数据段,为了便于提升变量查询速度,将其存储在栈中,即按值访问。
引用类型也称为复杂类型,由于其值的大小会改变,所以不能将其存放在栈中,否则会降低变量查询速度,因此,其值存储在堆(heap)中,而存储在变量处的值,是一个指针,指向存储对象的内存处,即按址访问。引用类型除 Object 外,还包括 Function 、Array、RegExp、Date 等等。
# 83. 箭头函数解决了什么问题?
参考答案:
箭头函数主要解决了 this 的指向问题。
解析:
在 ES5 时代,一旦对象的方法里面又存在函数,则 this 的指向往往会让开发人员抓狂。
例如:
//错误案例,this 指向会指向 Windows 或者 undefined var obj = { age: 18, getAge: function () { var a = this.age; // 18 var fn = function () { return new Date().getFullYear() - this.age; // this 指向 window 或 undefined }; return fn(); }, }; console.log(obj.getAge()); // NaN
1
2
3
4
5
6
7
8
9
10
11
12然而,箭头函数没有 this,箭头函数的 this 是继承父执行上下文里面的 this
var obj = { age: 18, getAge: function () { var a = this.age; // 18 var fn = () => new Date().getFullYear() - this.age; // this 指向 obj 对象 return fn(); }, }; console.log(obj.getAge()); // 2003
1
2
3
4
5
6
7
8
9
10
# 84. new 一个箭头函数后,它的 this 指向什么?
参考答案:
我不知道这道题是出题人写错了还是故意为之。
箭头函数无法用来充当构造函数,所以是无法 new 一个箭头函数的。
当然,也有可能是面试官故意挖的一个坑,等着你往里面跳。
# 85. promise 的其他方法有用过吗?如 all、race。请说下这两者的区别
参考答案:
promise.all 方法参数是一个 promise 的数组,只有当所有的 promise 都完成并返回成功,才会调用 resolve,
,都会进catch,被捕获错误,
promise.all 调用成功返回的结果是每个 promise 单独调用成功之后返回的结果组成的数组,如果调用失败的话,返回的则是第一个 reject 的结果promise.race 也会调用所有的 promise,返回的结果则是所有 promise 中最先返回的结果,不关心是成功还是失败。
# 86. class 是如何实现的
参考答案:
class 是 ES6 新推出的关键字,它是一个语法糖,本质上就是基于这个原型实现的。只不过在以前 ES5 原型实现的基础上,添加了一些 _classCallCheck、_defineProperties、_createClass等方法来做出了一些特殊的处理。
例如:
class Hello { constructor(x) { this.x = x; } greet() { console.log("Hello, " + this.x); } }
1
2
3
4
5
6
7
8"use strict"; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { console.log("Constructor::", Constructor); console.log("protoProps::", protoProps); console.log("staticProps::", staticProps); if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } var Hello = /*#__PURE__*/ (function () { function Hello(x) { _classCallCheck(this, Hello); this.x = x; } _createClass(Hello, [ { key: "greet", value: function greet() { console.log("Hello, " + this.x); }, }, ]); return Hello; })();
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
# 87. let、const、var 的区别
参考答案:
请参阅前面第 21 题答案。
# 88. ES6 中模块化导入和导出与 common.js 有什么区别
参考答案:
CommonJs 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化不会影响到这个值.
// common.js var count = 1; var printCount = () =>{ return ++count; } module.exports = { printCount: printCount, count: count }; // index.js let v = require('./common'); console.log(v.count); // 1 console.log(v.printCount()); // 2 console.log(v.count); // 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16你可以看到明明 common.js 里面改变了 count,但是输出的结果还是原来的。这是因为 count 是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动的值。将 common.js 里面的 module.exports 改写成
module.exports = { printCount: printCount, get count(){ return count } };
1
2
3
4
5
6这样子的输出结果是 1,2,2
而在 ES6 当中,写法是这样的,是利用 export 和 import 导入的
// es6.js export let count = 1; export function printCount() { ++count; } // main1.js import { count, printCount } from './es6'; console.log(count) console.log(printCount()); console.log(count)
1
2
3
4
5
6
7
8
9
10ES6 模块是动态引用,并且不会缓存,模块里面的变量绑定其所有的模块,而是动态地去加载值,并且不能重新赋值,
ES6 输入的模块变量,只是一个“符号连接符”,所以这个变量是只读的,对它进行重新赋值会报错。如果是引用类型,变量指向的地址是只读的,但是可以为其添加属性或成员。
另外还想说一个 export default
let count = 1; function printCount() { ++count; } export default { count, printCount} // main3.js import res form './main3.js' console.log(res.count)
1
2
3
4
5
6
7
8export 与 export default 的区别及联系:
export 与 export default 均可用于导出常量、函数、文件、模块等
你可以在其它文件或模块中通过 import + (常量 | 函数 | 文件 | 模块)名的方式,将其导入,以便能够对其进行使用
在一个文件或模块中,export、import 可以有多个,export default 仅有一个
通过 export 方式导出,在导入时要加{ },export default 则不需要。
# 89. 说一下普通函数和箭头函数的区别
参考答案:
请参阅前面第 8、83 题答案。
# 90. 说一下 promise 和 async 和 await 什么关系
参考答案:
await 表达式会造成异步函数停止执行并且等待promise的解决,当值被resolved,异步函数会恢复执行以及返回resolved值。如果该值不是一个promise,它将会被转换成一个resolved后的promise。如果promise被rejected,await 表达式会抛出异常值。
# 91. 说一下你学习过的有关 ES6 的知识点
参考答案:
这种题目是开放题,可以简单列举一下 ES6 的新增知识点。( ES6 的新增知识点参阅前面第 44 题)
然后说一下自己平时开发中用得比较多的是哪些即可。
一般面试官会针对你所说的内容进行二次提问。例如:你回答平时开发中箭头函数用得比较多,那么面试官极大可能针对箭头函数展开二次提问,询问你箭头函数有哪些特性?箭头函数 this 特点之类的问题。
# 92. 了解过 js 中 arguments 吗?接收的是实参还是形参?
参考答案:
JS 中的 arguments 是一个伪数组对象。这个伪数组对象将包含调用函数时传递的所有的实参。
与之相对的,JS 中的函数还有一个 length 属性,返回的是函数形参的个数。
# 93. ES6 相比于 ES5 有什么变化
参考答案:
ES6 相比 ES5 新增了很多新特性,这里可以自己简述几个。
具体的新增特性可以参阅前面第 44 题。
# 94. 强制类型转换方法有哪些?
参考答案:
JavaScript 中的数据类型转换,主要有三种方式:
- 转换函数
js 提供了诸如 parseInt 和 parseFloat 这些转换函数,通过这些转换函数可以进行数据类型的转换 。
- 强制类型转换
还可使用强制类型转换(type casting)处理转换值的类型。
例如:
- Boolean(value) 把给定的值转换成 Boolean 型;
- Number(value)——把给定的值转换成数字(可以是整数或浮点数);
- String(value)——把给定的值转换成字符串。
- 利用 js 变量弱类型转换。
例如:
转换字符串:直接和一个空字符串拼接,例如:
a = "" + 数据
转换布尔:!!数据类型,例如:
!!"Hello"
转换数值:数据*1 或 /1,例如:
"Hello * 1"
# 95. 纯函数
参考答案:
一个函数,如果符合以下两个特点,那么它就可以称之为纯函数:
- 对于相同的输入,永远得到相同的输出
- 没有任何可观察到的副作用
解析:
针对上面的两个特点,我们一个一个来看。
- 相同输入得到相同输出
我们先来看一个不纯的反面典型:
let greeting = 'Hello' function greet (name) { return greeting + ' ' + name } console.log(greet('World')) // Hello World
1
2
3
4
5
6
7上面的代码中,greet('World') 是不是永远返回 Hello World ? 显然不是,假如我们修改 greeting 的值,就会影响 greet 函数的输出。即函数 greet 其实是 依赖外部状态 的。
那我们做以下修改:
function greet (greeting, name) { return greeting + ' ' + name } console.log(greet('Hi', 'Savo')) // Hi Savo
1
2
3
4
5将 greeting 参数也传入,这样对于任何输入参数,都有与之对应的唯一的输出参数了,该函数就符合了第一个特点。
- 没有副作用
副作用的意思是,这个函数的运行,不会修改外部的状态。
下面再看反面典型:
const user = { username: 'savokiss' } let isValid = false function validate (user) { if (user.username.length > 4) { isValid = true } }
1
2
3
4
5
6
7
8
9
10
11可见,执行函数的时候会修改到 isValid 的值(注意:如果你的函数没有任何返回值,那么它很可能就具有副作用!)
那么我们如何移除这个副作用呢?其实不需要修改外部的 isValid 变量,我们只需要在函数中将验证的结果 return 出来:
const user = { username: 'savokiss' } function validate (user) { return user.username.length > 4; } const isValid = validate(user)
1
2
3
4
5
6
7
8
9这样 validate 函数就不会修改任何外部的状态了~
# 96. JS 模块化
参考答案:
模块化主要是用来抽离公共代码,隔离作用域,避免变量冲突等。
模块化的整个发展历史如下:
IIFE: 使用自执行函数来编写模块化,特点:在一个单独的函数作用域中执行代码,避免变量冲突。
(function () { return { data: [], }; })();
1
2
3
4
5AMD: 使用 requireJS 来编写模块化,特点:依赖必须提前声明好。
define("./index.js", function (code) { // code 就是index.js 返回的内容 });
1
2
3CMD: 使用 seaJS 来编写模块化,特点:支持动态引入依赖文件。
define(function (require, exports, module) { var indexCode = require("./index.js"); });
1
2
3CommonJS: nodejs 中自带的模块化。
var fs = require("fs");
1UMD:兼容 AMD,CommonJS 模块化语法。
webpack(require.ensure):webpack 2.x 版本中的代码分割。
ES Modules: ES6 引入的模块化,支持 import 来引入另一个 js 。
import a from "a";
1
# 97. 看过 jquery 源码吗?
参考答案:
开放题,但是需要注意的是,如果看过 jquery 源码,不要简单的回答一个“看过”就完了,应该继续乘胜追击,告诉面试官例如哪个哪个部分是怎么怎么实现的,并针对这部分的源码实现,可以发表一些自己的看法和感想。
# 98. 说一下 js 中的 this
参考答案:
请参阅前面第 17 题答案。
# 99. apply call bind 区别,手写
参考答案:
apply call bind 区别 ?
call 和 apply 的功能相同,区别在于传参的方式不一样:
- fn.call(obj, arg1, arg2, ...) 调用一个函数, 具有一个指定的 this 值和分别地提供的参数(参数的列表)。
- fn.apply(obj, [argsArray]) 调用一个函数,具有一个指定的 this 值,以及作为一个数组(或类数组对象)提供的参数。
bind 和 call/apply 有一个很重要的区别,一个函数被 call/apply 的时候,会直接调用,但是 bind 会创建一个新函数。当这个新函数被调用时,bind( ) 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。
实现 call 方法:
Function.prototype.call2 = function (context) { //没传参数或者为 null 是默认是 window var context = context || (typeof window !== "undefined" ? window : global); // 首先要获取调用 call 的函数,用 this 可以获取 context.fn = this; var args = []; for (var i = 1; i < arguments.length; i++) { args.push("arguments[" + i + "]"); } eval("context.fn(" + args + ")"); delete context.fn; }; // 测试 var value = 3; var foo = { value: 2, }; function bar(name, age) { console.log(this.value); console.log(name); console.log(age); } bar.call2(null); // 浏览器环境: 3 undefinde undefinde // Node环境:undefinde undefinde undefinde bar.call2(foo, "cc", 18); // 2 cc 18
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实现 apply 方法:
Function.prototype.apply2 = function (context, arr) { var context = context || (typeof window !== "undefined" ? window : global); context.fn = this; var result; if (!arr) { result = context.fn(); } else { var args = []; for (var i = 0, len = arr.length; i < len; i++) { args.push("arr[" + i + "]"); } result = eval("context.fn(" + args + ")"); } delete context.fn; return result; }; // 测试: var value = 3; var foo = { value: 2, }; function bar(name, age) { console.log(this.value); console.log(name); console.log(age); } bar.apply2(null); // 浏览器环境: 3 undefinde undefinde // Node环境:undefinde undefinde undefinde bar.apply2(foo, ["cc", 18]); // 2 cc 18
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实现 bind 方法:
Function.prototype.bind2 = function (oThis) { if (typeof this !== "function") { // closest thing possible to the ECMAScript 5 internal IsCallable function throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); } var aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this, fNOP = function () {}, fBound = function () { return fToBind.apply(this instanceof fNOP && oThis ? this : oThis || window, aArgs.concat(Array.prototype.slice.call(arguments))); }; fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; }; // 测试 var test = { name: "jack", }; var demo = { name: "rose", getName: function () { return this.name; }, }; console.log(demo.getName()); // 输出 rose 这里的 this 指向 demo // 运用 bind 方法更改 this 指向 var another2 = demo.getName.bind2(test); console.log(another2()); // 输出 jack 这里 this 指向了 test 对象了
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
# 100. 手写 reduce flat
参考答案:
reduce 实现:
Array.prototype.my_reduce = function (callback, initialValue) { if (!Array.isArray(this) || !this.length || typeof callback !== "function") { return []; } else { // 判断是否有初始值 let hasInitialValue = initialValue !== undefined; let value = hasInitialValue ? initialValue : tihs[0]; for (let index = hasInitialValue ? 0 : 1; index < this.length; index++) { const element = this[index]; value = callback(value, element, index, this); } return value; } }; let arr = [1, 2, 3, 4, 5]; let res = arr.my_reduce((pre, cur, i, arr) => { console.log(pre, cur, i, arr); return pre + cur; }, 10); console.log(res); //25
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21flat 实现:
let arr = [1, [2, 3, [4, 5, [12, 3, "zs"], 7, [8, 9, [10, 11, [1, 2, [3, 4]]]]]]]; //万能的类型检测方法 const checkType = (arr) => { return Object.prototype.toString.call(arr).slice(8, -1); }; //自定义flat方法,注意:不可以使用箭头函数,使用后内部的this会指向window Array.prototype.myFlat = function (num) { //判断第一层数组的类型 let type = checkType(this); //创建一个新数组,用于保存拆分后的数组 let result = []; //若当前对象非数组则返回undefined if (!Object.is(type, "Array")) { return; } //遍历所有子元素并判断类型,若为数组则继续递归,若不为数组则直接加入新数组 this.forEach((item) => { let cellType = checkType(item); if (Object.is(cellType, "Array")) { //形参num,表示当前需要拆分多少层数组,传入Infinity则将多维直接降为一维 num--; if (num < 0) { let newArr = result.push(item); return newArr; } //使用三点运算符解构,递归函数返回的数组,并加入新数组 result.push(...item.myFlat(num)); } else { result.push(item); } }); return result; }; console.time(); console.log(arr.flat(Infinity)); //[1, 2, 3, 4, 5, 12, 3, "zs", 7, 8, 9, 10, 11, 1, 2, 3, 4]; console.log(arr.myFlat(Infinity)); //[1, 2, 3, 4, 5, 12, 3, "zs", 7, 8, 9, 10, 11, 1, 2, 3, 4]; //自定义方法和自带的flat返回结果一致!!!! console.timeEnd();
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