1. var、let 及 const 区别

       面试题:什么是提升?什么是暂时性死区?var、let 及 const 区别?

       在变量声明之前使用变量叫做变量提升,并且提升的是声明,使用 var 声明的变量会被提升到作用域的顶部。

1
2
3
4
5
6
7
var a = 10;
var a ;
console.log(a) //10
//相当于
var a;
var a;
a = 10;

       函数也会被提升,并且优于变量提升,函数提升会把整个函数挪到作用域顶部。

1
2
3
4
5
6
7
8
9
10
11
12
var a = 1
let b = 1
const c = 1
console.log(window.a) // 1
console.log(window.b) // undefined
console.log(window. c) // undefined

function test(){
console.log(a)
let a
}
test() // 报错:a is not defined

       总结:

  • let 和 const 存在暂时性死区,不能在声明前使用变量
  • 在全局作用域下使用 let 和 const 声明变量,变量不会被挂载到 window 上
  • let 和 const 声明变量使用的是块作用域
  • let 和 const 不允许重复定义
  • let 在循环中引入了新的环境变量,针对每次迭代都会创建新的作用域
  • const 声明的变量不能再次赋值

2. 原型继承和 class 继承

       面试题:原型如何实现继承?Class 如何实现继承?Class 本质是什么?

1
2
3
4
5
6
7
//父类
function Parent(value) {
this.val = value;
}
Parent.prototype.getValue = function() {
console.log(this.val);
}
  • 组合继承
1
2
3
4
5
6
7
8
function Child(value) {
Parent.call(this, value); //在子类的构造函数中继承父类的属性
}
Child.prototype = new Parent(); //改变子类的原型为new Parent()来继承父类的函数
const child = new Child(1);

child.getValue(); // 1
child instanceof Parent // true

       缺点:继承父类函数时调用了父类构造函数,导致子类的原型上多了不需要的父类属性,内存上存在浪费。

  • 寄生组合继承
1
2
3
4
5
6
7
8
9
10
11
function Child(value) {
Parent.call(this, value);
}
Child.prototype = Object.create(Parent.prototype, {
constructor:{
value: Child, // 将父类的原型赋给子类,将构造函数设置为子类
enumerable: false,
writable: true,
configurable: true
}
})
  • class 继承
1
2
3
4
5
6
class Child extends Parent {
constructor(value) {
super(value); // 必须调用,继承父类属性,相当于Parent.call(this, value)
this.val = value;
}
}

       class 本质是函数,JS 中并不存在类

3. 模块化

       面试题:为什么要使用模块化?都有哪几种方式可以实现模块化,各有什么特点?

       优点:解决命名冲突、提供复用性、提高代码可维护性
       实现模块化的方式:

  • 立即执行函数
1
2
3
(function(globalVariable) {
// 声明各种变量、函数都不会污染全局作用域
})(globalVariable)
  • AMD/CMD
1
2
3
4
5
6
7
8
9
10
11
12
13
// AMD
define(['./a', './b'], function(a, b) {
// 加载模块完毕可以使用
a.do()
b.do()
})
// CMD
define(function(require, exports, module) {
// 加载模块
// 可以把 require 写在函数体的任意地方实现延迟加载
var a = require('./a')
a.doSomething()
})
  • CommonJS
1
2
3
4
5
6
7
8
9
10
// a.js
module.exports = {
a: 1
}
// or
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1
  • ES Module
  1. 异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  2. 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  3. 会编译成 require/exports 来执行的
1
2
3
4
5
6
// 引入模块 API
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function() {}

4. Proxy

       面试题:Proxy 可以实现什么功能?

       解释:

  1. 代理(Proxy)是一种可以拦截并改变底层 JavaScript 引擎操作的包装器,通过暴露内部运作的对象,从而可以创建内建的对象
  2. 底层被拦截后会触发响应特定操作的陷阱函数
  3. 反射 API 以 Reflect 对象的形式存在,Reflect 对象中的方法的默认特性和相同的底层操作保持一致,代理可以覆写这些操作,每个代理陷阱对应一个命名和参数都相同的 Reflect 方法
  4. 代理和反射的关系举例:改写内置 get 方法,代理负责拦截原来的 get 方法,并触发陷阱函数修改 get 的读取属性值特性,反射就是原来的内置方法,即 Reflect.get()是之前的 get 方法
           使用 set 陷阱验证属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let target = {
name: "target"
}
let proxy = new Proxy(target, {
//set陷阱
set(trapTarget, key, value, receiver) {
//忽略已有属性
if(!trapTarget.hasOwnProperty(key)) {
if(isNaN(value)) {
throw new TypeError('属性必须为数字');
}
}
return Reflect.set(trapTarget, key, value, receiver); //反射,用于使用内置set方法添加属性
}
})
proxy.count = 1; // 此时set陷阱被调用,是数字所以可以赋值
console.log(target.count) // 1
proxy.name = 'proxy';
console.log(target.name) // 'proxy',target已有name属性,所以可以赋值
proxy.notNumber = 'a'; // Uncaught TypeError: 属性必须为数字,给不存在的属性赋值并且值不是数字则抛错

       实现简单版响应式:

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
let onWatch = (obj, setBind, getLogger) => {
let handler = {
//set陷阱
set(target, property, value, receiver) {
setBind(value, property)
return Reflect.set(target, property, value)
},
//get陷阱
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver)
}
}
return new Proxy(obj, handler)
}

let obj = { a: 1 }
let p = onWatch(
obj,
(v, property) => {
console.log(`监听到属性${property}改变为${v}`)
},
(target, property) => {
console.log(`'${property}' = ${target[property]}`)
}
)
p.a = 2 // 监听到属性a改变
p.a // 'a' = 2

       应用举例:用 get 陷阱验证对象结构、用 has 陷阱隐藏已有属性、用 deleteProperty 陷阱防止删除属性

4. 数组方法

       面试题:map, filter, reduce 各自有什么作用?

  • map:[].map((当前索引元素,索引,原数组) => {}),遍历原数组,每个元素做相同的操作,生成一个新数组
1
[1, 2, 3].map(v => v + 1) // -> [2, 3, 4]
  • filter:[].filter((当前索引元素,索引,原数组) => {}),遍历原数组时将返回 true 的元素放入新数组
1
let newArray = [1, 2, 4, 6].filter(item => item !== 6) // [1,2,4]
  • reduce:[].reduce((累计值、当前元素、当前索引、原数组) => {}, 初始值),将数组中的元素通过回调函数最终转换为一个值
1
2
//在一次执行回调函数时,当前值和初始值相加得出结果 1,该结果会在第二次执行回调函数时当做第一个参数传入
const sum = [1, 2, 3].reduce((acc, current) => acc + current, 0) // 累加为6