var 和 let const 的区别
- var 是 ES5 语法,let const 是 ES6 语法;var 有变量提升
- var 和 let 是变量,可修改;const 是常量,不可修改
- let const 有块级作用域,var 没有
示例:变量提升
console.log(a) // undefined var a = 100 // 相当于 var a console.log(a) // undefined a = 100
示例:块级作用域
for(let i = 0; i < 5; i++) { let j = i + 1 } console.log(i)
typeof 能判断哪些类型
- undefined string number boolean symbol
- object(注意:typeof null === ‘object’)
- function
列举强制类型转换和隐式类型转换
- 强制:parseInt parseFloat toString
- 隐式:if、逻辑运算、==、+ 拼接字符串
手写深度比较 lodash.isEqual
isObject 来判断传入的参数是否为对象
如果不是对象直接返回它们的全等
如果传入的是同一个对象或数组,直接返回 true
如果传入的是两个不相等的对象或数组
则先判断长度,不一样直接返回 false
一样的话,以 obj1 为基准,和 obj2 依次递归比较
不相等返回 false,全满足了返回 true
// 判断是否是对象或数组 function isObject(obj) { return typeof obj === 'object' && obj !== null } function isEqual(obj1, obj2) { if(!isObject(obj1) || !isObject(obj2)) { // 值类型(参与 equal 的一般不会是函数) return obj1 === obj2 } if(obj1 === obj2) { return true } // 两个都是对象或数组,而且不相等 // 1.先取出 obj1 和 obj2 的 keys,比较个数 const obj1Keys = Object.keys(obj1) const obj2Keys = Object.keys(obj2) if(obj1Keys.length !== obj2Keys.length) { return false } // 2.以 obj1 为基准,和 obj2 依次递归比较 for(let key in obj1) { // 比较当前 key 的 val —— 递归 const res = isEqual(obj1[key], obj2[key]) if(!res) { return false } } // 3.全相等 return true } // 测试 const obj1 = { a: 10, b: { x: 100, y: 200 } } const obj2 = { a: 10, b: { x: 100, y: 200 } } console.log(isEqual(obj1, obj2)) // true
split() 和 join() 的区别
- Array.join() 方法是 String.split() 方法的反向操作
- split('-') 把字符串去掉 - 转为数组
- join('-') 把数组加上
-
转为字符串
'1-2-3'.split('-') [1, 2, 3].join('-')
pop、shift、push、unshift
以 arr 为例
const arr = [10, 20, 30]
pop:
- 功能:删除数组的最后一个元素
- 返回值:被删除的元素
- 是否对原数组造成影响:是
// pop const popRes = arr.pop() console.log(popRes, arr)
shift:
- 功能:删除数组的第一个元素
- 返回值:被删除的元素
- 是否对原数组造成影响:是
const shiftRes = arr.shift() console.log(shiftRes, arr)
push:
- 功能:从尾部添加元素
- 返回值:数组的长度
- 是否对原数组造成影响:是
const pushRes = arr.push(40) console.log(pushRes, arr)
unshift:
- 功能:从头部添加元素
- 返回值:数组的长度
- 是否对原数组造成影响:是
const unshiftRes = arr.unshift(5) console.log(unshiftRes, arr)
数组的API,有哪些是纯函数
纯函数:
- 不改变源数组(没有副作用)
- 返回一个数组
const arr = [10, 20, 30]
concat:
// concat const arr1 = arr.concat([40, 50]) console.log(arr, arr1)
map:
// map const arr2 = arr.map(num => num * 2) console.log(arr, arr2)
filter:
// filter const arr3 = arr.filter(num => num > 15) console.log(arr, arr3)
slice:
// slice const arr4 = arr.slice(2) console.log(arr, arr4)
数组 slice 和 splice 的区别
- 功能区别:slice 切片,splice 剪接
- 参数和返回值
- 是否纯函数
slice:
- slice 切片
- 参数可传可不传,返回值是切片后的数组
- 是纯函数(不改变原始数组)
const arr = [10, 20, 30, 40, 50] // slice 纯函数 const arr1 = arr.slice() const arr2 = arr.slice(1, 4) const arr3 = arr.slice(2) const arr4 = arr.slice(-2) console.log(arr) // [10, 20, 30, 40, 50] console.log(arr1) // [10, 20, 30, 40, 50] console.log(arr2)// [20, 30, 40] console.log(arr3) // [30, 40, 50] console.log(arr4) // [40, 50]
splice:
- splice 剪接
- 参数可传可不传,返回值是剪接掉的元素的数组
- 非纯函数(改变原始数组)
- 解读:开始剪接的元素索引(1),剪接的元素个数(2),要添加的元素('a', 'b', 'c')
const arr = [10, 20, 30, 40, 50] // splice 非纯函数 const spliceRes = arr.splice(1, 2, 'a', 'b', 'c') console.log(spliceRes) // [20, 30] console.log(arr) // [10, 'a', 'b', 'c', 40, 50]
const arr = [10, 20, 30, 40, 50] // splice 非纯函数 const spliceRes1 = arr.splice(1, 0, 'a', 'b', 'c') console.log(spliceRes1) // [] console.log(arr) // [10, 'a', 'b', 'c', 20, 30, 40, 50]
const arr = [10, 20, 30, 40, 50] // splice 非纯函数 const spliceRes2 = arr.splice(1, 2) console.log(spliceRes2) // [20, 30] console.log(arr) // [10, 40, 50]
[10, 20, 30].map(parseInt) 输出结果
map:
- 第一个参数是当前元素
- 第二个参数是当前索引值
parseInt:
- 第一个参数是要被解析的字符串
- 第二个参数是要解析的数字的基数(进制数 2~36)
const res = [10, 20, 30].map(parseInt) console.log(res) // 拆解 const res2 = [10, 20, 30].map((num, index) => { return parseInt(num, index) }) console.log(res2)
ajax 请求 get 和 post 的区别
- get 一般用于查询操作,post 一般用于提交操作
- get 参数拼接在 url 上,post 放在请求体内(数据体积可更大)
- 安全性:post 易于防止 CSRF 攻击
函数 call 、apply 和 bind 的区别
- call 传入的参数直接放进去
- apply 传入的参数是一个数组或类数组
- bind 返回的是一个新的函数,必须调用它才会被执行
fn.call(this, p1, p2, p3) fn.apply(this, arguments) fn.bind(this, p1, p2, p3)()
示例:
let zhangsan = { name: '张三', age: 20, myFun(from, to) { console.log(this.name + this.age, '来自' + from + '去往' + to) } } let lisi = { name: '李四', age: 22 } zhangsan.myFun.call(lisi, '济南', '青岛') zhangsan.myFun.apply(lisi, ['济南', '青岛']) zhangsan.myFun.bind(lisi, '济南', '青岛')()
事件代理(委托)
- 把 每个 li 的点击事件委托给 ul
- 可以减少内存的消耗
- 可以减少重复性操作
<ul id="list"> <li>item 1</li> <li>item 2</li> <li>item 3</li> ... <li>item n</li> </ul> //获取目标元素 const lis = document.getElementsByTagName('li') //循环遍历绑定元素 for (let i = 0; i < lis.length; i++) { lis[i].onclick = function (event) { console.log(event.target.innerHTML) } }
闭包
闭包的影响:变量会常驻内存,得不到释放。闭包不要乱用
阻止事件冒泡和默认行为
- 阻止冒泡:event.stopPropagation() 点击激活,不会弹出取消
- 阻止默认行为:event.pr
eventDefault()
点击超链接,不会跳转
<p id="p1">激活</p> <a id="a1" href="https://baidu.com">取消</a> const p1 = document.getElementById('p1') const a1 = document.getElementById('a1') const body = document.body p1.addEventListener('click', function (event) { event.stopPropagation() alert('激活') }) body.addEventListener('click', function (event) { alert('取消') }) a1.addEventListener('click', function(event) { event.preventDefault() })
阻止冒泡 —> 点击激活,不会弹出取消:
如何减少 DOM 操作
- 缓存 DOM 查询结果
- 多次 DOM 操作,合并到一次插入
<ul id="list"></ul> const list = document.getElementById('list') // 创建一个文档片段,此时还没有插入到 DOM 结构中 const frag = document.createDocumentFragment() for(let i = 0; i < 10; i++) { const li = document.createElement('li') li.innerHTML = `List item ${i}` // 先插入文档片段中 frag.appendChild(li) } // 都完成之后,再统一插入到 DOM 结构中 list.appendChild(frag)
jsonp 的本质是 ajax 吗
- 不是,因为没有用到 XMLHttpRequest 对象,所以 jsonp 不属于 ajax。
- 它是通过能够绕过跨域的 <script> 标签实现的
document load 和 ready 的区别
window.addEventListener('load', function() { // 页面的全部资源加载完才会执行,包括图片、视频等 }) window.addEventListener('DOMContentLoaded', function() { // DOM 渲染完即可执行,此时图片、视频还可能没有加载完 })
== 和 ===
- == 会尝试类型转换
- === 严格相等
- 除了 == null 之外,其他都用 ===
函数声明和函数表达式的区别
- 函数声明:function fn() {…}
- 函数表达式:const fn = function () {…}
示例:函数声明
const res = sum(10, 20) console.log(res) // 30 function sum(x, y) { return x + y }
示例:函数表达式
const res = sum(10, 20) console.log(res) // 报错 const sum = function (x, y) { return x + y }
用 var 声明:
关于 this 的场景题
- 第一个输出:this 指向当前对象 obj
- 第二个输出:this 指向 window
const obj = { count: 1, getCount: function() { return this.count } } console.log(obj.getCount()) // 1 const func = obj.getCount console.log(func()) // undefined
new Object() 和 Object.create() 区别
- {} 等同于 new Object(),原型 Object.prototype
- Object.create(null) 没有原型
- Object.create({…}) 可以指定原型
示例:
const obj1 = { a: 10, b: 20, sum() { return this.a + this.b } } const obj2 = new Object ({ a: 10, b: 20, sum() { return this.a + this.b } })
示例:
const obj1 = { a: 10, b: 20, sum() { return this.a + this.b } } const obj2 = new Object(obj1)
示例:
const obj1 = Object.create(null) const obj2 = new Object(null)
示例:
const obj1 = { a: 10, b: 20, sum() { return this.a + this.b } } const obj2 = Object.create(obj1)
作用域和自由变量
- let 的 i 和 for 循环里面的 i 不在同一块级作用域,相当于在 for 循环里面用 var 声明
let i for(i = 0; i <= 3; i++) { setTimeout(function(){ console.log(i) },0) }
- for 循环里面用 let 声明,形成块级作用域,i 每增加一次,输出一次
for(let i = 0; i <= 3; i++) { setTimeout(function(){ console.log(i) },0) }
作用域和自由变量 2
- 先把函数当成一条语句,不要看具体内容,等调用它的时候再看里面的内容
let a = 100 function test() { console.log(a) // 100 a = 10 console.log(a) // 10 } test() console.log(a) // 10
常见正则表达式
// 邮政编码 /\d{6}/ // 小写英文字母 /^[a-z]+$/ // 英文字母 /^[a-zA-Z]+$/ // 日期格式 /^\d{4}-\d{1,2}-\d{1,2}$/ // 用户名 /^[a-zA-Z]\w{5, 17}$/
手写字符串 trim
- 在 String 原型上添加 trim 方法
- this 指向输入的字符串
- 使用正则表达式做替换
String.prototype.trim = function () { return this.replace(/^\s+/, '').replace(/\s+$/, '') }
获取多个数字中的最大值
- 把要比较的数转为数组
- 也可以用 [...arguments] 代替 Array.from(arguments)
示例:
function max() { const nums = Array.from(arguments) let max = nums[0] nums.forEach(n => { if(n > max) { max = n } }) return max } const res = max(-1, -2, 2, 4, 8) console.log(res) // 8
class 实现继承
- extends
- super
- 扩展或重写方法
// 父类 class People { constructor(name) { this.name = name } eat() { console.log(`${this.name} eat food`); } } // 子类 class Student extends People { constructor(name, number) { super(name) this.number = number } sayHi() { console.log(`姓名 ${this.name} 学号 ${this.number}`); } } // 子类 class Teacher extends People { constructor(name, major) { super(name) this.major = major } teach() { console.log(`${this.name} 教授 ${this.major}`) } } // 实例 const xialuo = new Student('夏洛', '2022') console.log(xialuo.name) // 夏洛 console.log(xialuo.number) // 2022 xialuo.sayHi() // 姓名 夏洛, 学号 2022 xialuo.eat() // 夏洛 eat food // 实例 const wanglaoshi = new Teacher('王老师', '语文') console.log(wanglaoshi.name) // 王老师 console.log(wanglaoshi.major) // 语文 wanglaoshi.teach() // 王老师 教授 语文 wanglaoshi.eat() // 王老师 eat food
如何捕获 JS 中的异常
- try…catch 手动捕获
try { // todo } catch (ex) { console.error(ex) } finally { // todo }
- 自动捕获
// 自动捕获 window.onerror = function (message, source, lineNum, colNum, error) { // 第一,对于跨域的 js,如 CDN 的,不会有详细的错误报告 // 第二,对于压缩的 js,还要配合 sourceMap 反查未压缩代码的行、列 }
什么是 JSON
JSON 是一种数据格式,本质是一段字符串
JSON 格式和 JS 对象结构一致,对 JS 语言更友好
window.JSON 是一个全局对象:JSON.stringify(JS 对象转为 JSON 格式) JSON.parse(JSON 格式转为 JS 对象)
获取当前页面 url 参数
传统方式,查找 location.search
- substr(1) 截取掉字符串的第一个字符
- 'i' 代表不区分大小写
// 传统方法 function query(name) { const search = location.search.substr(1) // search: 'a=10&b=20&c=30' const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i') const res = search.match(reg) if(res === null) { return null } console.log(res) return res[2] } let res = query('b') console.log(res)
- 新 API,URLSearchParams
// URLSearchParams function query(name) { const search = location.search const p = new URLSearchParams(search) return p.get(name) } let res = query('b') console.log(res) // 20
将 url 参数解析为 JS 对象
传统方式,分析 search:
- 定义 res 为一个空对象,用来存放对象
- substr(1) 去掉前面的 ?
- 两次 split,分别去掉 & 和 =
- key 充当键,val 充当值
- 最后返回结果 res
// 传统方式,分析 search function queryToObj() { const res = {} const search = location.search.substr(1) search.split('&').forEach(paramStr => { const arr = paramStr.split('=') const key = arr[0] const val = arr[1] res[key] = val }) return res } let res = queryToObj() console.log(res)
使用 URLSearchParams:
// 使用 URLSearchParams function queryToObj() { const res = {} const pList = new URLSearchParams(location.search) pList.forEach((val, key) => { res[key] = val }) return res } let res = queryToObj() console.log(res)
数组去重
- 传统方式,遍历元素挨个比较、去重
- indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置。
- 如果没有找到匹配的字符串则返回 -1。
function unique(arr) { const res = [] arr.forEach(item => { if(res.indexOf(item) < 0) { res.push(item) } }) return res } let res = unique([20, 10, 20, 30, 10]) console.log(res)
- 使用 Set(计算效率更高)
function unique(arr) { const newArr = new Set(arr) return [...newArr] } let res = unique([20, 10, 20, 30, 10]) console.log(res)
手写深拷贝
- 深拷贝:一个对象B复制另一个对象A,当改变B对象的值是A对象的值不会改变。
function deepClone(obj = {}) { if (typeof obj !== 'object' || obj == null) { // obj 是 null,或者不是对象和数组,直接返回 return obj } // 初始化返回结果 let result if (obj instanceof Array) { result = [] } else { result = {} } for (let key in obj) { // 保证 key 不是原型的属性 if (obj.hasOwnProperty(key)) { // 递归 result[key] = deepClone(obj[key]) } } // 返回结果 return result } const obj1 = { name: '张三', age: { realAge: 20 }, arr: ['a', 'b', 'c'] } const obj2 = deepClone(obj1) obj2.name = '李四' obj2.age.realAge = 22 obj2.arr[0] = 'x' console.log(obj1.name) // 张三 console.log(obj1.age.realAge) // 20 console.log(obj1.arr[0]) // a
Object.assign()
assign 的属性拷贝是浅拷贝
用于将源对象的所有可枚举属性复制到目标对象中
如果目标对象和源对象有同名属性,或者多个源对象有同名属性,则后面的属性会覆盖前面的属性
如果该函数只有一个参数,当参数为对象时,直接返回该对象;当参数不是对象时,会先将参数转为对象然后返回
let sourceObj = { a: { b: 1 } } let targetObj = { c: 3 } let res = Object.assign(targetObj, sourceObj) console.log(res) // {c: 3, a: {b: 2}} targetObj.a.b = 2 console.log(sourceObj.a.b) // 2
了解 RAF requestAnimationFrame
- 要想动画流畅,更新频率要60帧/s,即 16.67ms 更新一次视图
- setTimeout 要手动控制频率,而 RAF 浏览器会自动控制
- 后台标签或隐藏 iframe 中,RAF 会暂停,而 setTimeout 不会停止
#div1 { width: 100px; height: 50px; background-color: red; } <div id="div1"></div> const div1 = document.getElementById('div1') let curWidth = 100 const maxWidth = 640 function animate() { curWidth = curWidth + 1 div1.style.width = `${curWidth}px` if(curWidth <= maxWidth) { window.requestAnimationFrame(animate) } } animate()
https://www.bilibili.com/video/BV1cW4y1h7VG?t=1.0
RAF
优化性能的考虑方面
- 原则:多使用内存、缓存、减少计算、减少网络请求
- 方向:加载页面更快,页面渲染更快,页面操作流畅度更好
Map 和 Object 的区别
须知:有序和无序
- 有序:操作慢
- 无序:操作快,但无序
- map、数组是有序的,对象是无序的
- 结合两者优点:二叉树
- Map 和 Object 的区别
- API 不同,Map 可以以任意类型为 key
- Map 是有序结构
- Map 操作同样很快
基本 API:
- 增改:set
- 删:delete
- 长度:size
示例:基本 API(set、delete、size)
const m = new Map([ ['key1', 'hello'], ['key2', 100], ['key3', {x: 100}] ]) console.log('原始 m', m) m.set('name', '张三') console.log('添加张三', m) m.set('key1', 'hello world') console.log('修改 key1', m) m.delete('key2') console.log('删除 key2', m) console.log('有无 key3', m.has('key3')) m.forEach((value, key) => console.log('遍历结果', key, value)) console.log('m 的长度', m.size)
- Map 可以以任意类型为 key
- 对象只能以字符串作为 key
const m = new Map([ ['key1', 'hello'], ['key2', 100], ['key3', {x: 100}] ]) // Map 以任意类型为 key const o = {name: 'xxx'} m.set(o, 'object key') function fn() {} m.set(fn, 'fn key') console.log(m)
object 有多快?
示例:查找第500万个元素的用时(object)
const obj = {} for(let i = 0; i < 1000 * 10000; i++) { obj[i + ''] = i } console.time('obj find') obj['5000000'] console.timeEnd('obj find') console.time('obj delete') delete obj['5000000'] console.timeEnd('obj delete')
map 有多快?
示例:查找第500万个元素的用时(map)
const m = new Map() for(let i = 0; i < 1000 * 10000; i++) { m.set(i + '', i) } console.time('map find') m.has('500000') console.timeEnd('map find') console.time('map delete') m.delete('500000') console.timeEnd('map delete')
可见,都在一个数量级上,相差无几
Set 和 数组的区别
- API 不同
- Set 元素不能重复
- Set 是无序结构,操作很快
- 示例:set 的 API
- add 添加
- delete 删除
- has 是否有
- size 长度
const set = new Set([10, 20, 30]) console.log('初始值', set) set.add(40) console.log('添加40', set) set.delete(10) console.log('删除10', set); set.has(30) let length = set.size console.log('长度', length); set.forEach(val => console.log('遍历set', val))
示例:数组去重
- set 元素是不能重复的(数组去重)
const arr = [10, 20, 20, 30, 10] console.log(new Set(arr))
示例:数组有多慢?set 有多快?
- 新增一条数据
// arr 有多慢? const arr = [] for(let i = 0; i < 100 * 10000; i++) { arr.push(i) } console.time('arr unshift') arr.unshift('a') console.timeEnd('arr unshift') // set 有多快? const set = new Set() for(let i = 0; i < 100 * 10000; i++) { set.add(i) } console.time('set add') set.add('a') console.timeEnd('set add')
- 查找 500000,这个元素
// arr 有多慢? const arr = [] for(let i = 0; i < 100 * 10000; i++) { arr.push(i) } console.time('arr find') arr.includes(500000) console.timeEnd('arr find') // set 有多快? const set = new Set() for(let i = 0; i < 100 * 10000; i++) { set.add(i) } console.time('set find') set.has(50000) console.timeEnd('set find')
WeakMap 和 WeakSet
- 弱引用,防止内存泄露
- WeakMap 只能用对象作为 key,WeakSet 只能用对象作为 value
- 没有 forEach 和 size,只能用 add delete has
- WeakMap:
- WeakMap 弱引用,可防止内存泄露
- 没有 forEach,size(因为和 key有关,对象不知道什么时候就没了),只能 has delete add
示例:
const wMap = new WeakMap() function fn() { const obj = {name: '张三'} wMap.set(obj, 'name info') // 只能用对象作为 key } fn()
WeakMap 使用场景(避免内存泄露):
// WeakMap 场景(避免内存泄露) const wMap = new WeakMap() const userInfo = {name: '张三'} const cityInfo = {city: '北京'} // 建立一种关联关系,而且两者保持独立,不影响彼此的销毁逻辑 wMap.set(userInfo, cityInfo) let res = wMap.get(userInfo) console.log(res) console.log(userInfo)
WeakSet
WeakMap 弱引用,可防止内存泄露,只能用对象作为 value
没有 forEach,size(因为和 key有关,对象不知道什么时候就没了),只能 has delete add
reduce 的使用
传统方法求和
封装一个 sum 函数
初始化 sum,通过 forEach 遍历数组
返回 sum
function sum(arr) { let sum = 0 arr.forEach(item => sum += item) return sum } const arr = [1, 2, 3] let res = sum(arr) console.log(res) // 6
reduce 传递的参数
- sum 当前和
- curVal 当前值
- index 索引
- arr 数组
const arr = [1, 2, 3] const res = arr.reduce((sum, curVal, index, arr) => { console.log('reduce function ....') console.log('sum', sum) console.log('curVal', curVal) console.log('index', index) console.log('arr', arr) // 返回值,作为下一次执行时的第一个参数 sum 的值 return sum + curVal }, 0) console.log(res)
reduce 求和
const arr = [1, 2, 3] const res = arr.reduce((sum, curVal) => sum + curVal, 0) console.log(res) // 6
reduce 计数
// 计数 const arr = [10, 20, 30, 20, 30, 10] const n = 10 const count = arr.reduce((count, val) => { return val === n ? count + 1 : count }, 0) console.log('count', count) // count 2
输出字符串
示例:map 写法
// 输出字符串 const arr = [ {name: '张三', age: '20'}, {name: '李四', age: '21'}, {name: '王五', age: '22'}, ] const str = arr.map(item => { return `${item.name} - ${item.age}` }).join('\n') console.log(str)
示例:reduce 写法
// 输出字符串 const arr = [ {name: '张三', age: '20'}, {name: '李四', age: '21'}, {name: '王五', age: '22'}, ] const str = arr.reduce((s, item) => { return `${s}${item.name} - ${item.age}\n` }, '') console.log(str)
不积跬步无以至千里 不积小流无以成江海