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 } })
|
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)
|
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() })
|
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
|
- 异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
- 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
- 会编译成 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 可以实现什么功能?
解释:
- 代理(Proxy)是一种可以拦截并改变底层 JavaScript 引擎操作的包装器,通过暴露内部运作的对象,从而可以创建内建的对象
- 底层被拦截后会触发响应特定操作的陷阱函数
- 反射 API 以 Reflect 对象的形式存在,Reflect 对象中的方法的默认特性和相同的底层操作保持一致,代理可以覆写这些操作,每个代理陷阱对应一个命名和参数都相同的 Reflect 方法
- 代理和反射的关系举例:改写内置 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
|