JS面试题4
-
var const let
-
什么是变量提升(host)
-
var
- 变量提升:变量还为声明,我们就可以使用这个未声明的变量,这种情况就叫做变量提升
console.log(a) // undefined var a = 1 等同于 var a console.log(a) // undefined a=1
-
函数也会被提升
console.log(a) // ƒ a() {} function a() {} var a = 1
- 函数提升优于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部
- 使用var声明的变量会被提升到作用域的顶部
- 变量提升的根本原因是为了解决函数建互相调用的情况
-
-
var const let 的区别
var
存在提升,我们能在声明之前使用。let
、const
因为暂时性死区的原因,不能在声明前使用var
在全局作用域下声明变量会导致变量挂载在window
上,其他两者不会let
和const
作用基本一致,但是后者声明的变量不能再次赋值
-
-
原型继承和class继承
-
组合继承
function Parent(value) { this.val = value } Parent.prototype.getValue = function() { console.log(this.val) } function Child(value) { Parent.call(this, value) } Child.prototype = new Parent() const child = new Child(1) child.getValue() // 1 child instanceof Parent // true
- 以上继承的方式核心是在子类的构造函数中通过
Parent.call(this)
继承父类的属性,然后改变子类的原型为new Parent()
来继承父类的函数。 - 这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
- 以上继承的方式核心是在子类的构造函数中通过
-
寄生组合继承
function Parent(value) { this.val = value } Parent.prototype.getValue = function() { console.log(this.val) } function Child(value) { Parent.call(this, value) } Child.prototype = Object.create(Parent.prototype, { constructor: { value: Child, enumerable: false, writable: true, configurable: true } }) const child = new Child(1) child.getValue() // 1 child instanceof Parent // true
- 以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
-
class继承(ES6)
class Parent { constructor(value) { this.val = value } getValue() { console.log(this.val) } } class Child extends Parent { constructor(value) { super(value) this.val = value } } let child = new Child(1) child.getValue() // 1 child instanceof Parent // true
class
实现继承的核心在于使用extends
表明继承自哪个父类,并且在子类构造函数中必须调用super
,因为这段代码可以看成Parent.call(this, value)
。
-
-
模块化
-
为什么要使用模块化
- 解决命名冲突
- 提高复用性
- 提高代码可维护性
-
立即执行函数
-
通过函数作用域解决了命名冲突、污染全局作用域的问题
(function(globalVariable){ globalVariable.test = function() {} // ... 声明各种变量、函数都不会污染全局作用域 })(globalVariable)
-
-
AMD和CMD
// AMD(异步模块定义) define(['./a', './b'], function(a, b) { // 加载模块完毕可以使用, // 依赖必须一开始就写好 a.do() b.do() }) // CMD(通用模块定义) define(function(require, exports, module) { // 加载模块 // 可以把 require 写在函数体的任意地方实现延迟加载 var a = require('./a') //依赖可以就近书写 a.doSomething() })
- 区别
- .对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.
- CMD 推崇依赖就近,AMD 推崇依赖前置
- AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。
- 区别
-
commonJS
- CommonJS 最早是 Node 在使用,目前也仍然广泛使用,比如在 Webpack 中你就能见到它,当然目前在 Node 中的模块管理已经和 CommonJS 有一些区别了。
// a.js module.exports = { a: 1 } // or exports.a = 1 // b.js var module = require('./a.js') module.a // -> log 1 // 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了, // 重要的是 module 这里,module 是 Node 独有的一个变量
exports
和module.exports
用法相似,但是不能对exports
直接赋值。因为var exports = module.exports
这句代码表明了exports
和module.exports
享有相同地址,通过改变对象的属性值会对两者都起效,但是如果直接对exports
赋值就会导致两者不再指向同一个内存地址,修改并不会对module.exports
起效。
-
ES Module
-
ES Module 是原生实现的模块化方案,与 CommonJS 有以下几个区别
-
CommonJS 支持动态导入,也就是
require(${path}/xx.js)
,后者目前不支持 -
CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
-
CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
-
ES Module 会编译成
require/exports
来执行的// 引入模块 API import XXX from './a.js' import { XXX } from './a.js' // 导出模块 API export function a() {} export default function() {}
-
-
-
-
Proxy
-
Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。
let p = new Proxy(target, handler)
target
代表需要添加代理的对象,handler
用来自定义对象中的操作,比如可以用来自定义set
或者get
函数。
-
proxy可以实现一个数据响应
let onWatch = (obj, setBind, getLogger) => { let handler = { get(target, property, receiver) { getLogger(target, property) return Reflect.get(target, property, receiver) }, set(target, property, value, receiver) { setBind(value, property) return Reflect.set(target, property, value) } } 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
- 在上述代码中,我们通过自定义
set
和get
函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。 - 之所以 Vue3.0 要使用
Proxy
替换原本的 API 原因在于Proxy
无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是Proxy
可以完美监听到任何方式的数据改变,唯一缺陷可能就是浏览器的兼容性不好了。
- 在上述代码中,我们通过自定义
-
-
map,filter,reduce
-
map
-
map
作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后放入到新的数组中。[1, 2, 3].map(v => v + 1) // -> [2, 3, 4]
-
map
的回调函数接受三个参数,分别是当前索引元素,索引,原数组['1','2','3'].map(parseInt)
- 第一轮遍历
parseInt('1', 0) -> 1
- 第二轮遍历
parseInt('2', 1) -> NaN
- 第三轮遍历
parseInt('3', 2) -> NaN
- 第一轮遍历
-
-
filter
-
filter
的作用也是生成一个新数组,在遍历数组的时候将返回值为true
的元素放入新数组,我们可以利用这个函数删除一些不需要的元素let array = [1, 2, 4, 6] let newArray = array.filter(item => item !== 6) console.log(newArray) // [1, 2, 4]
-
filter
的回调函数也接受三个参数,用处也相同
-
-
reduce
-
reduce
可以将数组中的元素通过回调函数最终转换为一个值。const arr = [1, 2, 3] const sum = arr.reduce((acc, current) => acc + current, 0) console.log(sum)//6
-
接受两个参数,分别是回调函数和初始值
-
reduce过程
- 首先初始值为
0
,该值会在执行第一次回调函数时作为第一个参数传入 - 回调函数接受四个参数,分别为累计值、当前元素、当前索引、原数组
- 在一次执行回调函数时,当前值和初始值相加得出结果
1
,该结果会在第二次执行回调函数时当做第一个参数传入 - 所以在第二次执行回调函数时,相加的值就分别是
1
和2
,以此类推,循环结束后得到结果 6
- 首先初始值为
-
可以通过reduce来实现map函数
const arr = [1, 2, 3] const mapArray = arr.map(value => value * 2) const reduceArray = arr.reduce((acc, current) => { acc.push(current * 2) return acc }, []) console.log(mapArray, reduceArray) // [2, 4, 6]
-
-