★ Object与Map的区别
Objects 和 Maps 类似的是,它们都允许你按键存取一个值、删除键、检测一个键是否绑定了值。因此(并且也没有其他内建的替代方式了)过去我们一直都把对象当成 Maps 使用。不过 Maps 和 Objects 有一些重要的区别,在下列情况里使用 Map 会是更好的选择:
| Map | Object | |
|---|---|---|
| 意外的键 | Map 默认情况不包含任何键。只包含显式插入的键。 | 一个 Object 有一个原型, 原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。备注: 虽然 ES5 开始可以用 Object.create(null) 来创建一个没有原型的对象,但是这种用法不太常见 |
| 键的类型 | 一个 Map的键可以是任意值,包括函数、对象或任意基本类型。 | 一个Object 的键必须是一个 String 或是Symbol。 |
| 键的顺序 | Map 中的 key 是有序的。因此,当迭代的时候,一个 Map 对象以插入的顺序返回键值。 | 一个 Object 的键是无序的备注: 自ECMAScript 2015规范以来,对象确实保留了字符串和Symbol键的创建顺序; 因此,在只有字符串键的对象上进行迭代将按插入顺序产生键。 |
| Size | Map 的键值对个数可以轻易地通过size 属性获取 | Object 的键值对个数只能手动计算 |
| 迭代 | Map 是 iterable 的,所以可以直接被迭代。 | 迭代一个Object需要以某种方式获取它的键然后才能迭代。 |
| 性能 | 在频繁增删键值对的场景下表现更好。 | 在频繁添加和删除键值对的场景下未作出优化。 |
★ Set
Set 理解
set类似数组,但是跟数组不一样的就是set里面可以放任何类型,大杂烩一样的东西 比如 [1,'a']
Set 用法 示例【 更多用法 点击示例】
let mySet = new Set();
mySet.add(1); // Set [ 1 ]
mySet.add("some text"); // Set [ 1, "some text" ]
mySet.has(1); // true
mySet.has(3); // false
mySet.size; // 2
mySet.delete(1); // true, 从set中移除5
mySet.has(1); // false, 5已经被移除
mySet.size; // 1, 刚刚移除一个值
const numbers = [2,3,4,4,2,3,3,4,4,5,5,6,6,7,5,32,3,4,5]
console.log([...new Set(numbers)])
// [2, 3, 4, 5, 6, 7, 32]
Set属性
Set方法
Set.prototype[@@iterator]()Set.prototype.add()Set.prototype.clear()Set.prototype.delete()Set.prototype.entries()Set.prototype.forEach()Set.prototype.has()Set.prototype.values()
★ Map
Map 理解
上面可以点击跳转到MDN,查看详情,下面说的是我自己的理解
map类似对象,不一样的是对象Object的key必须是字符串,而map里面的key和值可以是任何值 比如{1:'1','a':'a',2:2}
Map 用法 示例 【更多用法 点击示例】
let myMap = new Map()
myMap.set('bla','blaa')
myMap.set('bla2','blaa2')
console.log(myMap) // Map { 'bla' => 'blaa', 'bla2' => 'blaa2' }
myMap.has('bla') // true
myMap.delete('bla') // true
console.log(myMap) // Map { 'bla2' => 'blaa2' }
Map属性
Map方法
Map.prototype[@@iterator]()Map.prototype.clear()Map.prototype.delete()Map.prototype.entries()Map.prototype.forEach()Map.prototype.get()Map.prototype.has()Map.prototype.keys()Map.prototype.set()Map.prototype.values()
★ Object的常用方法有哪些
-
Object.assign()【后面的对象的属性覆盖前面的】 -
Object.create()【创建一个没有原型的对象,但是这种用法不太常见】 -
Object.entries()【返回键值对数组】 -
Object.freeze()【冻结一个对象。不能被修改】 -
Object.fromEntries()【把键值对列表转换为一个对象】 -
Object.hasOwn()(en-US)【是否有xxx属性 返回Boolean】 -
Object.prototype.hasOwnProperty()【是否具有指定的属性】 -
Object.keys() -
Object.values()【属性值的数组】
★ Object.keys
参数
obj: 要返回其枚举自身属性的对象。
返回值
一个表示给定对象的所有可枚举属性的字符串数组。
描述
Object.keys 返回一个所有元素为字符串的数组,其元素来自于从给定的object上面可直接枚举的属性。这些属性的顺序与手动遍历该对象属性时的一致。
例子
// 传入字符串,返回索引
var str = 'ab1234';
console.log(Object.keys(obj)); //[0,1,2,3,4,5]
// simple array 数组 返回索引
var arr = ['a', 'b', 'c'];
console.log(Object.keys(arr)); // console: ['0', '1', '2']
// array like object
var obj = { 0: 'a', 1: 'b', 2: 'c' };
console.log(Object.keys(obj)); // console: ['0', '1', '2']
0-9是数字的顺序,a-z是定义的顺序
// array like object with random key ordering !!!
var anObj = {c: 'ccccc', 2: '22222', 1: '11111', b: 'bbbb', a: 'aaa'};
console.log(Object.keys(anObj)); // console: ['1', '2', 'c', 'b', 'a']
console.log(Object.values(anObj)); // console: ['a', 'b', 'c', 'd', 'aaa']
// getFoo is a property which isn't enumerable
var myObj = Object.create({}, {
getFoo: {
value: function () { return this.foo; }
}
});
myObj.foo = 1;
console.log(Object.keys(myObj)); // console: ['foo']
// 构造函数 返回空数组或者属性名
function Pasta(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.toString = function () {
return (this.name + ", " + this.age + ", " + this.gender);
}
}
console.log(Object.keys(Pasta)); //console: []
var spaghetti = new Pasta("Tom", 20, "male");
console.log(Object.keys(spaghetti)); //console: ["name", "age", "gender", "toString"]
返回值顺序
Object.keys在内部会根据属性名key的类型进行不同的排序逻辑。分三种情况:
- 如果属性名的类型是
Number,那么Object.keys返回值是按照key从小到大排序
- 如果属性名的类型是
String,那么Object.keys返回值是按照属性被创建的时间升序排序。
- 如果属性名的类型是
Symbol,那么逻辑同String相同
★ map, filter, reduce
map
map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后放入到新的数组中。
[1, 2, 3].map(v => v + 1) // -> [2, 3, 4]
另外 map 的回调函数接受三个参数,分别是当前索引元素,索引,原数组
console.log([1, 2, 3].map(v => v + 1)) // [2, 3, 4]
console.log(['1','2','3'].map(parseInt)) // [1, NaN, NaN]
第一轮遍历 `parseInt('1', 0) -> 1`
第二轮遍历 `parseInt('2', 1) -> NaN`
第三轮遍历 `parseInt('3', 2) -> NaN`
parseInt
该parseInt() 函数解析一个字符串参数并返回一个指定的整数
parseInt(string, radix)
// radix 基数 进制
console.log( parseInt('1', 0)) // 1
console.log( parseInt('2', 3)) // 2
console.log( parseInt('2', 1)) // NaN
console.log( parseInt('02', 10)) // 2
filter
filter 的作用也是生成一个新数组,在遍历数组的时候将返回值为 true 的元素放入新数组,我们可以利用这个函数删除一些不需要的元素
let array = [1, 2, 4, 6]
let newArray = array.filter(item => item !== 6)
console.log(newArray) // [1, 2, 4]
和 map 一样,filter 的回调函数也接受三个参数,用处也相同。
reduce
- 作用:
reduce可以将数组中的元素通过回调函数最终转换为一个值。
- 写法:
如果我们想实现一个功能将函数里的元素全部相加得到一个值,可能会这样写代码
const arr = [1, 2, 3]
let total = 0
for (let i = 0; i < arr.length; i++) {
total += arr[i]
}
console.log(total) //6
但是如果我们使用 reduce 的话就可以将遍历部分的代码优化为一行代码
const arr = [1, 2, 3]
const sum = arr.reduce((acc, current) => acc + current, 0)
console.log(sum)
- 参数:
- 参数1:回调函数【(acc, current) => acc + current】
- 参数2:初始值【0】
4.过程
- 首先初始值为
0,该值会在执行第一次回调函数时作为第一个参数传入 - 回调函数接受四个参数,分别为
累计值、当前元素、当前索引、原数组,后三者想必大家都可以明白作用,这里着重分析第一个参数 - 在一次执行回调函数时,当前值和初始值相加得出结果
1,该结果会在第二次执行回调函数时当做第一个参数传入 - 所以在第二次执行回调函数时,相加的值就分别是
1和2,以此类推,循环结束后得到结果6
- 实现map函数示例
想必通过以上的解析大家应该明白 reduce 是如何通过回调函数将所有元素最终转换为一个值的,当然 reduce 还可以实现很多功能,接下来我们就通过 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]
如果你对这个实现还有困惑的话,可以根据上一步的解析步骤来分析过程。
★ Proxy
涉及面试题:Proxy 可以实现什么功能?
-
在 Vue3.0 中通过
Proxy来替换原本的Object.defineProperty来实现数据响应式。 -
Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。
let p = new Proxy(target, handler)
// target 代表需要添加代理的对象,
// handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。
- 接下来我们通过
Proxy来实现一个数据响应式
let onWatch = (obj, setBind, getLogger) => {
console.log('obj',obj)
let handler = {
get(target, property, receiver) {
console.log('target-',target) // {a: 2}
console.log('property-',property) //a
console.log('receiver-',receiver) //Proxy {a: 2} receiver是this
getLogger(target, property)
return Reflect.get(target, property, receiver)
},
set(target, property, value, receiver) {
console.log('target',target) //{a: 1}
console.log('property',property) //a
console.log('value',value) //1
console.log('receiver',receiver) //Proxy {a: 1} receiver是this
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改变 // 执行set方法
p.a // 'a' = 2
// Reflect.set() 在一个对象上设置一个属性。
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect/set
var obj1 = {};
Reflect.set(obj1, "prop", "value"); // true
obj1.prop; // "value"
在上述代码中,我们通过自定义 set 和 get 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。
当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,需要我们在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是Proxy可以完美监听到任何方式的数据改变,唯一缺陷可能就是浏览器的兼容性不好了。
★ 继承【小测】
★ 模块化 【】
常见面试题:1.为什么要使用模块化?2.都有哪几种方式可以实现模块化,3.各有什么特点?
立即执行函数
早期
AMD 和 CMD
AMD和CMD现在比较少见,使用方法就是define关键字,里面引用其他的js文件
// 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【exports 或者 module.exports 和 require】【node、webpack、后端】【动态导入、同步导入会卡主主线程但是影响不大】
CommonJS 最早是 Node 在使用,目前也仍然广泛使用,比如在 Webpack 中你就能见到它,
exports = module.exports = {}
exports 是 module.exports的引用
大概就是 var a = {}; var b = a; a 和 b 之间的区别吧
exports 和 module.exports 用法相似,但是不能对 exports 直接赋值。因为 var exports = module.exports 这句代码表明了如果直接对 exports 赋值就会导致两者不再指向同一个内存地址,修改并不会对 module.exports 起效。
// a.js
module.exports = {
a: 1
}
// or
exports.a = 1
// b.js
var module = require('./a.js')
module.a // -> log 1
ES Module 【ES6的es,所以是前端,浏览器端,是异步导入】
export 或 export default 和 import
ES Module 是原生实现的模块化方案,与 CommonJS 有以下几个区别
-
- CommonJS 支持
动态导入,也就是require(${path}/xx.js),后者目前不支持,但是已有提案
- CommonJS 支持
-
- CommonJS 是
同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而ES Module是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
- CommonJS 是
-
CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
-
- ES Module 会编译成
require/exports来执行的
- ES Module 会编译成
// 引入模块 API
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function() {}
★ var、let 及 const
常见面试题 1.var、let 及 const 区别;2.什么是提升;3.什么是暂时性死区
var、let 及 const 区别
(1)块级作用域: 块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:
- 内层变量可能覆盖外层变量
- 用来计数的循环变量泄露为全局变量 (2)变量提升: var存在变量提升,在var声明之前使用不会报错,只是值是undefined而已,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错,即使上级作用域中定义了也不行,因为会读取当前作用域下的定义。
console.log(tmp); // undefined
var tmp = 123;
console.log(tmp); // 123
if (true) {
// console.log(tmp); //ReferenceError: tmp is not defined
let tmp;
console.log(tmp);// undefined 上面报错就不会执行到这,报错注释掉,这里会是undefined
}
(3)给全局添加属性: 浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。
(4)重复声明: var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。【会报错 Identifier 'a' has already been declared】
(5)暂时性死区: 在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。
(6)初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。
(7)指针指向: let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。
| 区别 | var | let【es6】 | const【es6】 |
|---|---|---|---|
| 是否有块级作用域 | × | ✔️ | ✔️ |
| 是否存在变量提升 | ✔️ | × | × |
| 是否添加全局属性 | ✔️ var即使再块级作用域里面定义的也是全局变量 | × | × |
| 能否重复声明变量 | ✔️ | × | × |
| 是否存在暂时性死区 | × | ✔️ | ✔️ |
| 是否必须设置初始值 | × | × | ✔️ |
| 能否改变指针指向 | ✔️ | ✔️ | × const是不允许重新指向新的内存地址 |
-
函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部
-
var存在提升,我们能在声明之前使用。let、const因为暂时性死区的原因,不能在声明前使用 -
var在全局作用域下声明变量会导致变量挂载在window上,其他两者不会 -
let和const作用基本一致,但是后者声明的变量不能再次赋值块作用域由{ }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题: -
内层变量可能覆盖外层变量
-
用来计数的循环变量泄露为全局变量
1.首先let 和const是es6的;
2.let和const具有块级作用域,var不存在块级作用域;
3.var存在变量提升【函数提升优先于变量提升】,let和const不存在变量提升;
4.var声明的变量为全局变量,并且会添加为全局对象的属性,但是let和const不会。
5.var可以重复声明变量,后面的会覆盖之前的。const和let不允许重复声明变量。
【会报错 Identifier 'a' has already been declared】
6.let、const存在暂时性死区,var没有;
7.var 和 let 可以不用设置初始值 const必须设置初始值;
8.const 不能再次赋值 var 和 let 可以
什么是提升
console.log(a) // undefined
var a = 1
对于这种情况,我们可以把代码这样来看
var a
console.log(a) // undefined
a = 1
从上述代码中我们可以发现,虽然变量还没有被声明,但是我们却可以使用这个未被声明的变量,这种情况就叫做提升,并且提升的是声明。
不仅变量会提升函数也会被提升。
console.log(a) // ƒ a() {}
function a() {}
var a = 1
对于上述代码,打印结果会是 ƒ a() {},即使变量声明在函数之后,这也说明了函数会被提升,并且优先于变量提升。
什么是暂时性死区
let a = 1
function b (){
console.log(a) // 1
// let a = 2
}
b()
// 暂时性死区
let a = 1
function b (){
console.log(a) // 报错 截图如下
let a = 2
}
b()
上面代码中,存在全局变量`a`,但是块级作用域内`let`又声明了一个局部变量`a`,
导致后者a绑定这个块级作用域,所以在`let`声明变量前,对`a`赋值会报错。
ES6明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称TDZ)。