跳转到主要内容

面试题库

JS 面试题精讲

JS 数据类型、判断类型、作用域、原型链、ES6+ 等,共 68 题。

  • 前端
  • JavaScript
  • 面试

JS 面试题精讲

来源:resource/面试题.zip → 03.御码-面试题.pdf

JS

说说JS的数据类型

  1. 基本类型 number/string/boolean undefined/null symbol/bigint
  2. 引用/对象类型 Object/Array/Function 其它内置或自定义类型 注意 number/string/boolean对就的包装类型Number/String/Boolean, 在读取属性或调用方法时会自 动转换 undefined/null 没有对应的包装类型, 不能读取属性或调用方法 symbol/bigint也有对应的包装类型, 但不能我们new调用 ==> 说出它的一些特点就行, 开发中 很少

说说JS如何判断数据类型

6种判断方式

  1. === undefined null
  2. typeof string number boolean undefined symbol bigint function object null 所有非函数对象
  3. instanceof A instanceof B: 判断A或者A原型链上的对象是否是B类型的实例 可以用于判断某个对象是否某个特定类型的 区别Object对象与数组对象
  4. obj.constructor 得到对象的构造函数 得到number/string/boolean值的包装类型 null/undefined没有constructor
  5. String.prototype.toString.call(obj) 得到构造函数的名称
  6. Array.isArray() 专门判断数组

说说常见的数组方法

  1. 更新数组的7个 push / pop / unshift / shift splice sort / reverse
  2. 遍历元素 forEach map / filter / reduce find / findIndex every / some
  3. 其它 slice concat join includes indexOf

函数传参是值传递, 还是引用传递?

函数调用时, 是将实参变量的数据拷贝一份赋值给形参变量 只是实参变量数据可能是基本类型值 ==> 值传递 也可能是引用类型的值(也就是地址值) ==> 引用传递/值传递 注意下面的代码, 准确的说不是将a内存的地址赋值给b, 而是将a中保存的地址值赋值给b

vara= {}
varb=a
vara1=2
vara2= {}
functionfn (x) {
}
fn(a1) // 值传递
fn(a2) // x = a2  拷贝a2内存中保存的值(地址值)传递给x

说说你对作用域与作用域链的理解

作用域 一个变量可以合法使用的范围/区域 作用域起到了隔离变量, 避免了变量重名冲突的问题(也就是允许了不同作用域中可以有同名的 变量) 分类: 全局作用域 函数作用域 块作用域 => ES6的let或const变量 作用域链 多个嵌套的作用域形成的由内向外的结构, 用于查找变量 本质: 包含由内向外的多个变量对象的数组 ==> 这个可以不用说 当查找一个变量, 在整个作用域链中都找不到时: 会报引用错误(RefrenceError), 错误信息 (message)为这个变量没有定义

说说变量提升与函数提升

变量提升( 变量声明提升) 变量声明语句会提升到当前作用域的最前面执行 在变量声明语句之前, 就可以访问到这个变量(undefined) 函数提升( 函数声明提升) 函数声明语句提升到当前作用域的最前面执行 在函数声明语句之前, 就可以执行该函数 原因: 简单来说就是在执行全局代码和函数前会进行预解析/处理 将var变量声明放在最前面执行 将function函数声明放在最前面执行 注意 const / let / class 没有提升 var 变量 / function 函数声明才有提升

区分执行函数定义与执行函数

执行函数定义: 创建函数对象, 如果指定了函数名, 同时会定义变量并指向这个函数对象 执行函数: 执行函数内部的语句 必须先执行函数定义, 再执行函数 ===> 注意: 函数定义有可能会提升到最上面执行

说说你对闭包的理解

是什么? 通过chrome的debugger调试工具得知: 闭包本质是内部函数中的一个容器(非js对象), 这个容器 中包含了引用的变量 如何产生? 嵌套的内部函数引用了外部函数的变量, 当调用外部时就会产生闭包 闭包不是在调用内部函数时产生, 而是在创建内部函数对象时产生 作用? 延长局部变量的生命周期 让函数外部能间接操作内部的局部变量 区别产生闭包与使用闭包及释放闭包? 产生闭包: 内部函数对象创建时产生, 包含那个被引用的变量的容器(不是js对象) 使用闭包: 执行内部函数 释放闭包: 让内部函数对象成为垃圾对象, 断开指向它的所有引用 应用? 举删除删除列表中的的某个商品的例子(带确定框) IIFE 模块编译之后的运行代码 写一个简单的闭包程序

如何判断函数中的this?

常规情况下, 函数中的this取决于执行函数的方式(四种绑定规则) 默认绑定: 直接调用 fn() => 严格模式下是undefined, 非严格模式下是window 隐式绑定: 通过对象调用 obj.fn()=> 对象 显式绑定: 通过call/apply/bind调用 fn.call/apply/bind()=> 第一个参数对象 构造函数绑定: 通过new调用 new fn() => new出来的对象 特殊情况: 箭头函数 ==> this是?外部作用域的this==》沿着作用域链去外部找this bind(obj)返回的函数 ==> this是? obj 回调函数它不是我们调用的 定时器/ajax/promise/数组遍历相关方法回调 ==> this是? window DOM事件监听回调 ==> 发生事件的DOM元素 vue控制的回调函数(生命周期/methods/watch/computed) ==> this是?组件的实例 React控制的生命周期回调, 事件监听回调 ==> this是?组件对象 / undefined

function fn1() {
var a = 2;
var b = 3;
function fn2() {
a++;
console.log(a);
}
return fn2;
}
var f = fn1();
f();
f();
f = null;

如何改变(指定)函数中的this?

情况一: 将任意对象指定为当前函数中的this 函数立即调用: call() / apply() 函数后面某个时候调用: bind() 情况二: 将外部的this指定为当前函数中的this ES6之前: 将外部this保存为其它名称变量, 当前函数中不使用this, 而使用这个变量 ES6之后: 箭头函数

说说原型与原型链

原型 每个函数都有一个显式原型属性: prototype 每个实例都有一个隐式原型属性: proto

实例的__proto__ 与对应函数的prototype 都指向原型对象

函数的默认的原型对象是Object的实例,也就是一个{} 原型对象上有一个constructor 属性指向对应的构造函数 原型链 从对象的__proto__ 开始, 连接的所有对象。也可称为“隐式原型链” 原型链的作用: 在查找对象属性或调用对象方法时,会先在对象自身上查找,找不到就会沿着原型链查 找 利用原型链可以实现继承

instanceof的内部原理和自定义实现

作用: 判断一个任意类型对象的具体类型 instanceof内部如何判断? 对于 A instanceof B A是实例对象, B是构造函数 如果B的prototype属性所指向的原型对象是A实例对象的原型链接上的某个对象, 返回true, 否则 返回false 原型链详图 自定义instanceof功能函数

/*
自定义instanceof工具函数:
语法: myInstanceOf(obj, Type)

功能: 判断obj是否是Type类型的实例 实现: Type的原型对象是否是obj的原型链上的某个对象, 如果是返回true, 否则返回false

*/
functionmyInstanceOf(obj, Type) {

// 得到原型对象

letprotoObj=obj.__proto__

// 只要原型对象存在

while(protoObj) {
// 如果原型对象是Type的原型对象, 返回true
if (protoObj===Type.prototype) {
returntrue
}

// 指定原型对象的原型对象

protoObj=protoObj.__proto__
}
returnfalse
}

编码实现继承

基于构造函数的继承 原型链 + 借用构造函数的组合式继承 让子类的原型为父类的实例: Student.prototype = new Person() 让子类型原型的构造器为子类型: Student.prototype.constructor = Student 借用父类型构造函数: Person.call(this, name, age) 基于ES6的类的继承 子类 extends 父类: class Teacher extends Person2 子类构造器中调用父类的构造: super(name, age)

// 父类型
functionPerson(name, age) {
this.name=name
this.age=age
}
Person.prototype.fn=function () {}
Person.prototype.sayHello=function () {
console.log(`我叫${this.name}, 年方${this.age}`)
}
// 子类型
functionStudent(name, age, price) {
// this.name = name
// this.age = age

// 借用父类型的构造函数

Person.call(this, name, age)  // 相当于执行this.Person(name, age)
this.price=price
}

// 让子类的原型为父类的实例

Student.prototype=newPerson()

// 让原型对象的构造器为子类型

Student.prototype.constructor=Student

// 重写方法

Student.prototype.sayHello=function () {
console.log(`我叫${this.name}, 年方${this.age}, 身价: ${this.price}`)
}
consts=newStudent('tom', 23, 14000)
s.sayHello()
s.fn()
// 父类
classPerson2 {
constructor (name, age) {
this.name=name
this.age=age
}
fn () {}
sayHello () {

说说面向对象的三大特征

封装: 将可复用的代码用一个结构包装起来, 后面可以反复使用 js的哪些语法体现了封装性: 函数 ==> 对象 ==> 模块 ==> 组件 ==> 库 封装都要有个特点: 不需要外部看到的必须隐藏起来, 只向外部暴露想让外部使用的功能或数据 继承 为什么要有继承? 复用代码, 从而减少编码 js中的继承都是基于原型的继承: ES6的类本质也是 编码实现: 原型链+借用构造函数的组合 / ES6的类继承 多态: 多种形态 理解 声明时指定一个类型对象, 并调用其方法, 实际使用时可以指定任意子类型对象, 运行的方法就是当前子类型对象的方法 JS中有多态: 由于JS是弱类型语言, 在声明时都不用指定类型 在使用时可以指定任意类型的数据 ==> 这已经就是多态的体现了

说说JS的垃圾回收机制

在JS中对象的释放(回收)是靠浏览器中的垃圾回收器来回收处理的 垃圾回调器 浏览器中有个专门的线程,它每隔很短的时间就会运行一次 主要工作:判断一个对象是否是垃圾对象,如果是,清除其内存数据,并标记内存是空闲状 态 如何判断对象是垃圾对象呢?

console.log(`我叫${this.name}, 年方${this.age}`)
}
}
// 子类
classTeacherextendsPerson2 {
constructor (name, age, course) {
super(name, age)
this.course=course
}
// 重写父类的方法
sayHello () {
console.log(`我叫${this.name}, 年方${this.age}, 课程:${this.course}`)
}
}
constt=newTeacher('bb', 34, 'CC')
t.sayHello()
t.fn()

机制1:引用计数法 机制2:标记清除法 V8垃圾回调机制: 分代回收 垃圾回收机制1:引用计数法 最初级的垃圾收集算法: 判断一个对象的引用数,引用数为0 就垃圾对象,引用数大于0 就不 是垃圾对象 问题: 有循环引用问题 如果2个对象内部存在相互引用,断开对象的引用后,它们还不是垃圾对象 垃圾回收机制2:标记-清除法 现代垃圾回收算法的基础: 将可达的对象标记起来,不可达的对象当成垃圾回收 从根对象(也就是window)开始递归深度查找所有引用的对象,并标记为‘活动’,没有标记为 活动的对象就是垃圾对象 没有循环引用问题 问题: 反复对所有可达的对象进行遍历标记, 但有的对象存话时间是较长的,这样效率太低了 V8垃圾回收机制: 分代回收 将堆分为两个空间,一个叫新生代区,一个叫老生代区

新生代区

老生代区

大小 较小(1-8M), 包含2个子空间 较大 对象的特点 存活周期短 存活周期长 回收器 副垃圾回收器 主垃圾回收器 核心算法 Scavenge算法 Mark-Sweep && Mark-Compact算法 对象初始创建时都在新生代, 经过几次收集后还在存在, 就会转移到老生代存储 新生代 (以空间换时间) 1、标记活动对象和非活动对象(根据可达性) 2、复制from-space 的活动对象到to-space 中并进行排序 3、清除from-space 中的非活动对象 4、将from-space 和to-space 进行角色互换,以便下一次的Scavenge算法垃圾 回收 老生代 Mark-Sweep算法(标记清理) 标记阶段:对老生代对象进行第一次扫描,对活动对象进行标记 清理阶段:对老生代对象进行第二次扫描,清除未标记的对象,即非活动对象 问题: 清除非活动对象之后,留下了很多零零散散的空位, 不利于对象分配空间 Mark-Compact算法(标记整理) 把剩下的活动对象,整理到内存的一侧,整理完成后,直接回收掉边界上的内存

区别一下内存溢出与内存泄漏

内存溢出 运行程序需要分配的内存超过了系统能给你分配的最大剩余内存 抛出内存溢出的错误,程序中断运行 演示代码 内存泄漏 理解: 当程序中的某个内存数据不再需要使用,而由于某种原因,没有被释放 常见情况: 意外的全局变量 没有及时清除的定时器 没有及时解绑的监听

const arr = []
for (let index = 0; index < 100000000; index++) {
arr[index] = new Array(1000)
}
function fn () {a = new Array(100000)}
fn()
this.intervalId = setInterval(() => {}, 1000)
// clearInterval(this.intervalId)
this.$bus.$on('xxx', this.handle)
// this.$bus.$off('xxx')

没有及时释放的闭包

区别一下同步与异步

同步: 从上往下按顺序依次执行 只有将一个任务完全执行完后, 才执行后面的 会阻塞后面的代码执行 异步 启动任务后, 立即向下继续执行, 等同步代码执行完后才执行回调函数 不会阻塞后面的代码执行 异步回调函数会即使触发了, 也是要先放入队列中待执行, 只有当同步任务或前面的异步任务执 行完才执行

说一下JS的事件循环机制

JS内部是通过事件循环机制来实现单线程异步执行执行的 分析事件循环机制 所有任务(同步/异步)都在主线程上执行,形成一个执行栈 执行栈之外有用于存储待执行异步回调的任务队列(task queue) ==> 准备的说是2个(宏列队与 微队列) 浏览器中有在其它分线程执行相关管理模块 定时器管理模块 ajax请求管理模块 DOM事件管理模块 JS引擎执行代码的顺序为 在执行栈中执行初始化同步代码 执行过程中如果有启动异步任务, 交给对应的管理模块处理, 管理模块会在后面特定时间 将回调函数放入任务队列中待执行 在执行栈中所有代码都执行完后, 依次取出任务队列中的回调到执行栈中依次执行 宏任务与微任务 script(整体代码) setTimeout / setInterval, Ajax DOM事件监听 postMessage (H5, 向其它窗口分发异步消息) setImmediate(Node.js 环境) 宏队列与微队列 Promise async & await mutationobserver(H5, 监视DOM元素变化) 整体的执行顺序

  1. script(整体代码)
  2. 所有微队列中的微任务
  3. 宏队列中的第一个宏任务
  4. 所有微队列中的微任务 后面3-4循环处理

说说ES6的promise

ES6推出的更好的异步编程解决方案(相对于纯回调的方式) 解决嵌套回调的回调地狱问题 ==> 通过promise.then的链式调用 指定读取结果数据的回调函数时机更灵活 ==> 请求后/请求前/请求完成后都可以 promise对象有3种状态 pending resolved/fulfilled rejected promise状态的2种变化 pending —> resolved pending —> rejected 如何改变promise的状态 调用resolve() 调用reject() throw error

说说ES6中Promise的then方法

then指定成功和失败的回调, 用于得到成功/失败的结果数据 then()总是返回一个新的promise 新promise的结果状态由then指定的回调函数执行的结果决定 抛出错误 => 失败且reason就是抛出的错误 返回失败的promise => 失败且reason是返回的promise的reason 返回成功的promise => 成功且value是返回的promise的value 返回其它任何值 => 成功且value是返回的值 返回pending的promise => pending的promise

区别Promise的all与race方法

Promise.all([p1, p2, p3]) 接收包含多个promise的数组, 返回一个新的promise 只有当所有接收的promise都成功了, 返回的promise才成功, 且成功的value为所有成功promise 的value组成的数组 一旦有一个失败了, 返回的promise就失败了, 且失败的reason就是失败promise的reason Promise.race([p1, p2, p3]) 接收包含多个promise的数组, 返回一个新的promise 返回的promise的结果由第一个完成的promise决定

说说async与await的理解和使用

async与await是异步编程的终极解决方案 => 消灭回调函数 作用: 简化promise对象的使用, 不用再使用then/catch来指定回调函数 使用 await一般在结果为promise的表达式左侧 async在await所在函数定义的左侧 注意: 调用async函数得到是一个promise, 其结果状态由async函数体执行的结果决定 await的右侧也可以不是promise, 如果不是, 直接返回表达式的值

var、let、const之间的区别

const定义常量, let和var定义变量 let相对于var 有块作用域 没有变量提升 不会添加到window上 不能重复声明

箭头函数的特点

编码简洁 没有自己的this, 使用外部作用域中的this, 不能通过bind来绑定this 不能通过new来创建实例对象 内部没有arguments, 可以通过rest参数来代替

ES6的模块化语法

导出语法: 整个模块总是一个对象, 所有导出语法都是向模块对象中添加属性或方法 分别导出: export const a = 2 默认导出: export default 3 统一导出: export {b, c} 导入 静态导入 import {a} from ’./test’ import value from ’./test’ import {default as xxx} from ’./test’ import * as test from ’./test’ 静态导入的模块打包到一起 动态导入 import(’./test’).then(module => {}) 动态导入的模块会被单独打包

说出几个ES6常用新语法

const与let 箭头函数 解构赋值 形参默认值 rest/剩余参数 类语法: class / extends / constructor / static /super 扩展运算符: … 模板字符串 异步语法: promise / async & await 对象的属性与方法简写 模块化语法: export / default / import / import() set / map

说说事件冒泡与事件委托

事件冒泡 事件在传递给目标元素后, 会由内向外传递给外层的元素处理 事件委托 不直接给多个子元素绑定多个事件监听, 而是给它们共同的父元素绑定一个监听 当操作任意子元素时, 事件会冒泡到父元素上处理 在事件回调中通过event.target得到发生事件的目标元素, 并进行相关处理

HTTP请求的常用响应状态码

2XX: 表示成功处理请求, 如200(成功), 201(添加数据成功) 3XX: 需要重定向, 浏览器直接跳转, 如302(重定向) 4XX: 客户端请求错误, 如: 401(token失效), 404 (不存在) 5XX: 服务器端错误, 如: 500

对原生ajax进行简单的封装

/*
xhr + promise 封装一个异步ajax请求的通用函数简洁版
ajax ('xxx.json')
*/
functionajax(url) {
returnnewPromise((resolve, reject) => {
// 创建一个XHR对象
constxhr=newXMLHttpRequest()

// 初始化一个异步请求(还没发请求)

xhr.open('GET', url, true)

// 绑定状态改变的监听

xhr.onreadystatechange=function () {
/*
ajax引擎得到响应数据后
将xhr的readyState属性指定为4
将响应数据保存在response / responseText属性上
调用此回调函数
*/

// 如果状态值不为4, 直接结束(请求还没有结束)

if (xhr.readyState!==4) {
return
}
// 如果响应码在200~~299之间, 说明请求都是成功的
if (xhr.status>=200&&xhr.status<300) {
// 指定promise成功及结果值
resolve(JSON.parse(xhr.responseText))
} else { // 请求失败了
// 指定promise失败及结果值
reject(newError('request error staus '+request.status))
}

说说你对跨域问题的理解和解决办法

同源: 协议, 域名, 端口, 三者都相同 同源策略 ajax请求时, 浏览器要求当前网页和Server必须同源(安全), 否则会抛出跨域的错误 加载image/link/script不受同源策略限制 解决ajax跨域问题的办法 JSONP CORS Proxy

说说你知道的ajax跨域解决方案

JSONP: 利用script发跨域请求目标接口, 得到响应数据 CORS: 浏览器直接请求跨域的目标接口, 服务器返回响应头告诉浏览器允许跨域 Proxy: 浏览器发同源请求代理服务器, 代理服务器转发请求跨域的目标接口

说说你项目中的axios的二次封装

  1. 配置通用的基础路径和超时
  2. 显示请求进度条
  3. 显示进度条: 请求拦截器回调
  4. 结束进度条: 响应拦截器回调
  5. 成功返回的数据不再是response, 而直接是响应体数据response.data
  6. 统一处理请求错误, 具体请求也可以选择处理或不处理
  7. 每个请求自动携带userTempId的请求头: 在请求拦截器中实现
  8. 如果当前有token, 自动携带token的请求头
  9. 对token过期的错误进行处理

说说axios的整体执行流程

基本执行顺序 请求拦截器的回调函数 xhr发请求 响应拦截器成功/失败的回调 具体请求成功/失败的回调 内部原理: 通过promise.then的链式调用将这4个任务串连越来, 依次执行并进行数据传递

}
xhr.send()
})
}

从输入url到渲染出页面的整个过程

  1. 得到服务器对应的IP地址 ==> DNS解析
  2. 通过IP连接上服务器: 3次握手
  3. 向服务器发请求,接收服务器返回的响应
  4. 解析响应数据(html/css/js)显示页面 解析html => dom树 解析css => cssom树 解析js => 更新dom树/cssom树 生成渲染树 = dom树 + cssom树 布局 渲染
  5. 断开连接:4次挥手

说说前台数据存储

Promise.resolve(config)
.then((config) => {  // 请求拦截器
// 显示进度/携带token/userTempId
return config
})
.then((config) => {  // 使用xhr发ajax请求
return new Promise((resolve, reject) => {
// 根据config创建xhr对象发送异步ajax
// 如果请求成功(响应状态码200--299之间)
const response = {
data: JSON.parse(xhr.responseText),
status: xhr.statusCode
}
resolve(response)
// 如果请求失败
reject(new Error('请求失败 code ' + xhr.statusCode))
})
})
.then( // 响应拦截器
response => {
return response.data
},
error => {
throw error
}
)
.then(result => { // 特定请求的回调
}).catch(error => {
})

自定义call和bind

存储方式:

cookie: 会话/持久化两种
sessionStorage
localStorage

数据库(很少用) 区别:

---------------------sessionStorage VS localStorage

刷新在/ 关闭不在       关闭还在

---------------------cookie VS sessionStorage与localStorage
- 容量                小
- 请求时是否自动携带     会
- API易用性            不好用
- 浏览器是否可禁用       可
/*

自定义函数对象的call方法

*/
functioncall (fn, obj, ...args) {
// 如果传入的是null/undefined, this指定为window
if (obj===null||obj===undefined) {
// obj = window
returnfn(...args)
}

// 给obj添加一个方法: 属性名任意, 属性值必须当前调用call的函数对象

obj.tempFn=fn
// 通过obj调用这个方法
constresult=obj.tempFn(...args)

// 删除新添加的方法

deleteobj.tempFn

// 返回函数调用的结果

returnresult
}
/*

自定义函数对象的bind方法 重要技术:

闭包
call()

三点运算符

*/
functionbind (fn, obj, ...args) {
returnfunction (...args2) {
returncall(fn, obj, ...args, ...args2)
}
}

区别函数节流与防抖

自定义函数节流与防抖

  1. 事件高频触发处理的问题 如果更新界面 => 界面更新卡顿 如果发送ajax请求 => 发送了很多没必要的请求
  2. 解决办法 函数节流 函数防抖
3. 区别:

当事件高频发生很多次时, 防抖只执行最后一次, 而节流执行少量几次 4. 应用场景

节流: 窗口调整(resize)/ 页面滚动(scroll)/ OM元素的拖拽功能实现(mousemove)

防抖: 输入搜索联想功能

/*

用于产生节流函数的工具函数

*/
functionthrottle (callback, delay) {

// 用于保存处理事件的时间, 初始值为0, 保证第一次会执行

letstart=0

// 返回事件监听函数 ==> 每次事件发生都会执行

returnfunction (event) {
console.log('---throttle')

// 发生事件的当前时间

constcurrent=Date.now()

// 与上一次处理事件的时差大于delay的时间

if (current-start>delay) {
// 执行处理事件的函数
callback.call(event.target, event)
// 保证当前时间
start=current
}
}
}
/*

用于产生防抖函数的工具函数

*/
functiondebounce (callback, delay) {

// 返回事件监听函数 ==> 每次事件发生都会执行

returnfunction (event) {
console.log('---debounce')

// 如果还有未执行的定时器, 清除它

if (callback.timeoutId) {
clearTimeout(callback.timeoutId)
}
// 启动延时delay的定时器, 并保证定时器id
callback.timeoutId=setTimeout(() => {
// 执行处理事件的函数

自定义数组扁平化

自定义new

callback.call(event.target, event)
// 删除保存的定时器id
callback.timeoutId=null
}, delay);
}
}
/*

数组扁平化: 取出嵌套数组(多维)中的所有元素放到一个新数组(一维)中

如: [1, [3, [2, 4]]]  ==>  [1, 3, 2, 4]
*/
/*
方法一: 递归 + reduce() + concat() + some()
*/
functionflatten1 (array) {
returnarray.reduce((pre, item) => {
if (Array.isArray(item) &&item.some((cItem=>Array.isArray(cItem)))) {
returnpre.concat(flatten1(item))
} else {
returnpre.concat(item)
}
}, [])
}
/*
方法二: ... + some() + concat()
*/
functionflatten2 (arr) {

// 只要arr是一个多维数组(有元素是数组)

while (arr.some(item=>Array.isArray(item))) {
// 对arr进行降维
arr= [].concat(...arr)
}
returnarr
}
/*

自定义new工具函数

语法: newInstance(Fn, ...args)

功能: 创建Fn构造函数的实例对象

实现: 创建空对象obj, 调用Fn指定this为obj, 返回obj
*/
functionnewInstance(Fn, ...args) {

自定义深拷贝

// 创建一个新的对象

constobj= {}

// 执行构造函数

constresult=Fn.apply(obj, args) // 相当于: obj.Fn()

// 如果构造函数执行的结果是对象, 返回这个对象

if (resultinstanceofObject) {
returnresult
}
// 给obj指定__proto__为Fn的prototype
obj.__proto__=Fn.prototype

// 如果不是, 返回新创建的对象

returnobj
}
/*

1). 大众乞丐版 问题1: 函数属性会丢失 问题2: 循环引用会出错

*/
exportfunctiondeepClone1(target) { // 从后台获取的数据都可以用
returnJSON.parse(JSON.stringify(target))
}
/*

获取数据的类型字符串名

*/
functiongetType(data) {
returnObject.prototype.toString.call(data).slice(8, -1)  // -1代表最后一位
}
/*

2). 面试基础版本 解决问题1: 函数属性还没丢失

*/
functiondeepClone2(target) {
consttype=getType(target)
if (type==='Object'||type==='Array') {
constcloneTarget=type==='Array'? [] : {}
for (constkeyintarget) {
if (target.hasOwnProperty(key)) {
cloneTarget[key] =deepClone2(target[key])
}
}
returncloneTarget
} else { // 基本类型 / function
returntarget
}
}
/*

3). 面试加强版本 解决问题2: 循环引用正常

*/
functiondeepClone3(target, map=newMap()) {
consttype=getType(target)
if (type==='Object'||type==='Array') {
// 从map容器取对应的clone对象
letcloneTarget=map.get(target)
// 如果有, 直接返回这个clone对象
if (cloneTarget) {
returncloneTarget
}
cloneTarget=type==='Array'? [] : {}
// 将clone产生的对象保存到map容器
map.set(target, cloneTarget)
for (constkeyintarget) {
if (target.hasOwnProperty(key)) {
cloneTarget[key] =deepClone3(target[key], map)
}
}
returncloneTarget
} else {
returntarget
}
}
deepClone3(obj)
/*

4). 面试加强版本2(优化遍历性能)

数组: while | for | forEach() 优于 for-in | keys()&forEach()
对象: for-in 与 keys()&forEach() 差不多
*/
functiondeepClone4(target, map=newMap()) {
consttype=getType(target)
if (type==='Object'||type==='Array') {
letcloneTarget=map.get(target)
if (cloneTarget) {
returncloneTarget
}
if (type==='Array') {
cloneTarget= []
map.set(target, cloneTarget)
target.forEach((item, index) => {
cloneTarget[index] =deepClone4(item, map)
})
} else {
cloneTarget= {}
map.set(target, cloneTarget)
Object.keys(target).forEach(key=> {
cloneTarget[key] =deepClone4(target[key], map)
})
}
returncloneTarget

实现数组的冒泡排序和sort排序

说说重排(回流)与重绘

页面显示过程 解析HTML生成DOM树 解析CSS生成CSSOM树 解析JS更新DOM树和CSSOM树 DOM树 + CSSOM树生成渲染树 布局(也称回流, 确定个节点显示的位置) 渲染绘制

} else {
returntarget
}
}
/*

冒泡排序的方法

*/
functionbubbleSort (array) {
// 1.获取数组的长度
varlength=array.length;

// 2.反向循环, 因此次数越来越少

for (vari=length-1; i>=0; i--) {
// 3.根据i的次数, 比较循环到i位置
for (varj=0; j<i; j++) {
// 4.如果j位置比j+1位置的数据大, 那么就交换
if (array[j] >array[j+1]) {
// 交换
// const temp = array[j+1]
// array[j+1] = array[j]
// array[j] = temp
[array[j+1], array[j]] = [array[j], array[j+1]];
}
}
}
returnarr;
}
constproducts= [{price: 23, sales: 103}, {price: 22, sales: 101}]
/*

根据销量升序

*/
products.sort((p1, p2) => {  // 比较函数  ==> 返回数值   如果大于o, p2放在左边
returnp1.sales-p2.sales
})

更新DOM或Style 可能会导致局部重排(也称回流, 重新布局) 可能会导致局部重绘 注意: 重排肯定会重绘, 但重绘不一定有重排 重排比重绘开销更大, 更消耗性能 哪些操作会导致重排 浏览器窗口尺寸改变 元素位置和尺寸发生改变的时候 新增和删除可见元素 内容发生改变(文字数量或图片大小等等) 元素字体大小变化。 激活CSS伪类(例如::hover)。 设置style属性 查询某些属性。比如说: offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、 scrollHeight、clientTop、clientLeft、clientWidth、clientHeight 哪些操作会导致重绘 更新元素的部分属性(影响元素的外观,风格,而不会影响布局),比如 visibility、outline、背景色等属性的改变。 例子代码 如何减少重排次数 更新节点的样式,尽量通过类名而不是通过style来更新 分离样式的读定操作,不要将读写操作混合调用 将DOM操作离线处理,比如使用DocumentFragment

Vue

vars=document.body.style;
s.padding="2px"; // 回流+重绘
s.border="1px solid red"; // 再一次回流+重绘
s.color="blue"; // 重绘
s.backgroundColor="#ccc"; // 重绘
s.fontSize="14px"; // 再一次回流+重绘
document.body.appendChild(document.createTextNode('abc!'));// 添加node,再一次回流

+重绘

1. 区别v-if与v-show

隐藏: v-if干掉标签, v-show通过样式来隐藏 重新显示: v-if需要重新创建标签对象, v-show只需要修改样式显示出来就可以 v-show重新显示更快, 但隐藏时还占用着内存空间: 以空间换时间 v-show更适合切换频繁/需要隐藏的DOM结构比较大 使用v-if解决模板中初始解析undefined的bug 比如: {{a.b.c}} a初始为一个空对象

2. 为什么v-for与v-if不适合一起使用

对遍历的item数据进行限制判断 问题: 如果使用v-if, 每个数组元素都会解析指令来判断 ==> 效率低 解决: 不使用v-if, 使用计算属性, 过滤产生一个子数组 ==> 效率高 根据外部的数据判断 问题: 如果在当前标签上用v-if, 执行n次 ==> 效率低 解决: 添加一个/在父标签, 使用v-if, 执行1次 ==> 效率高

3. computed与watcher以及method的区别

computed与watch的区别 计算属性必须同步返回计算结果, 而watch中可以在异步操作后更新数据显示 watch可以深度监视, 计算属性只是监视了使用到的数据 选择: 如果是根据现在的数据同步计算就可以确定要显示的另一个数据 ==> computed 如果涉及到异步操作/深度监视 ==> watch 一旦一个数据变化, 我们需要做一系列操作 ===> watch computed与method的区别 计算属性有缓存, 多次读取显示只计算一次 method, 多处显示计算多次

4. 说说Vue的常用指令

v-text v-html v-show v-if / v-else / v-else-if v-for v-on v-once: 只初始渲染一次, 用于优化更新性能 v-bind v-model v-slot: 插槽

5. Vue组件的生命周期

单个组件生命周期 初始化: beforeCreate: 不能通过this读取data数据和调用methods中的方法 执行了一些初始化的准备工作 created: 可以通过this读取data数据和调用methods中的方法 编译模板 beforeMount: 不能通过ref读取到页面中内容 挂载编译好的模板, 显示页面 mounted: 能通过ref读取到页面中内容 更新: this.msg += ’—’ beforeUpdate (在数据更新后, 界面更新前调用): 读取的数据是最新的, 但页面是老的 更新页面 updated: 读取的数据和页面都是新的 死亡: $destroy() beforeDestroy: 做一些收尾的工作, 比如: 清除定时器/解绑监听/… destroyed

6. Vue父子组件的生命周期顺序

初始化: beforeCreate created beforeMount —child beforeCreate —child created —child beforeMount —child mounted mounted 更新: beforeUpdate —child beforeUpdate —child updated updated 死亡: beforeDestroy — child beforeDestroy — child destroyed destroyed

7. 区别组件的钩子函数 actived与mounted

mounted:初始化执行一次 activated: 初始化mounted之后 / 每次再回到当前路由

8. 说说对动态组件、缓存组件与异步组件的理解

动态组件 通过的is属性动态加载一个组件 is属性初始为A组件名, 加载A组件, 切换为B组件名, 加载B组件 缓存组件 默认路由组件离开或动态组件被切换, 组件都会立即死亡 能让原本要死亡的组件不死亡, 在背后缓存起来, 后面需要时, 直接使用缓存的 可以通过include与exclude属性来控制哪些组件要缓存或不缓存 异步组件 在引入组件时使用import动态引入: const Home = () => import(’./Home.vue’) 组件会被单独打包, 且只有在第一次访问时才会请求加载对应的打包文件 ==> 减小首屏打包文 件打小 除了组件, 其它模块也可以异步懒加载

9. 说说对递归组件的理解

递归组件: 组件内部有自己的子组件标签 应用场景: 用于显示树状态结构的界面 注意: 递归组件必须有name 编码: 实现一个简单的可开关的树状结构界面的 Tree 组件

<template>
<ul>
<liv-for="(item, index) in data":key="index"
@click.stop="handleClick(item)">
{{item.text}}
<Tree2v-if="item.expend":data="item.children"/>
</li>
</ul>
</template>
<script>
exportdefault {  //
name: 'Tree2', // 作为内部的标签名
props: ['data'],
methods: {
handleClick (item) {
if (item.hasOwnProperty('expend')) {
item.expend=!item.expend
} else {
this.$set(item, 'expend', true)
}
}
}
}

10. Vue组件间有哪些通信方式

根据组件间关系分类

  1. 父向子 props(非函数) v-model $refs, $children 插槽
  2. 子向父 props(函数) vue自定义事件 v-model .sync $parent 作用域插槽
  3. 祖孙间 $attrs与$listeners 与v-bind/v-on配合使用 provide与inject
  4. 兄弟或其它/任意 全局事件总线 Vuex 另一种分类方式
  5. 属性相关 props v-model $attrs与$listeners 作用域插槽(子向父传递)
  6. 自定义事件相关 自定义事件 全局事件总线 v-model .sync
  7. 其它 $refs, $children, $parent provide与inject 插槽 vuex
</script>

11. 子向父通信有哪些方式

props(函数) vue自定义事件 v-model .sync $parent 作用域插槽

12. 说说Vue的自定义事件与全局事件总线

vue自定义事件 实现子组件向父组件通信 相关语法: 父组件中绑定自定义事件监听: <Child @eventName=“callback($event)”> child.$on(‘eventName’, callback) 子组件中分发事件 this.$emit(‘eventName’, 2) 应用: elment-ui的组件的事件监听语法都用的是自定义事件 <el-button @click=“test”> 我们项目中的组件也用了不少自定义事件 全局事件总线 实现任意组件间通信 编码 将入口js中的vm作为全局事件总线对象: beforeCreate() { Vue.prototype.$bus = this } 传递数据的组件分发事件: this.$bus.$emit(‘eventName’, data) 接收数据的组件处理监听: this.$bus.$on(‘eventName’, (data) => {}) 应用: 前台项目中使用全局事件总线 理解: 为什么将vm放到Vue的原型对象上, 所有组件都可见呢? VueComponent.prototype = Object.create(Vue.prototype); VC的原型对象的原型对象就是Vue的原型对象

13. Vue响应式数据原理

简洁表达 对象: 通过Object.defineProperty()添加setter方法来监视属性数据的改变 + 订阅-发布 数组: 重写更新数组元素的一系列方法 + 订阅-发布 调用原生的对应对数组元素进行相应的操作 更新界面去 详细表达(主要说对象的) 初始化 实现数据代理 通过defineproperty给vm定义与data中属性对应的带getter/setter的属性 在getter中, 读取data中对应的属性值返回 => 读取this.msg => 读取的是data中msg 属性值 在setter中, 将最新值保存到data对应的属性上 => this.msg = ‘abc’ => ‘abc’会保存到 data的msg上 创建observer 目标: 对data中所有层次的属性进行监视/劫持 通过defineproperty给data中所有层次属性, 都重新定义, 加上getter与setter getter: 用来建立dep与watcher的关系 setter: 用来当data数据发生改变去更新界面 为data中所有层次的属性创建一个对应的dep ==> 用来将来更新界面的 创建compile 目标1: 实现界面的初始化显示 / 目标2: 为将更新做准备 为模板中每个包含表达式(事件表达式除外)的节点创建一个对应的watcher 给watcher绑定用于更新对应节点的回调函数 将watcher添加到n个对应的dep中 更新 this.msg = ‘abc’ 由于有数据代理 ==> data的msg更新为了’abc’ 由于有数据劫持 ==> data中msg的setter调用了 在setter中, 通过对应的dep去通知所对应的watcher去更新对应的节点 ==> 使用了订阅发 布模式 Vue数据响应式原理结构图

14. Vue双向数据绑定原理

通过v-model来实现双向数据绑定 v-model的本质 将动态的data数据通过value属性传给input显示 ==> data到view的绑定 给input标签绑定input监听, 一旦输入改变读取最新的值保存到data对应的属性上 ==> view到 data的绑定 双向数据绑定是在单向数据绑定(data—>view)的基础, 加入input事件监听(view ==> data)

15. $set和$nextTick的使用场景

项目功能: 列表项点击动态显示输入框, 并自动获取焦点 编码实现: 动态给列表项数据对象添加edit属性为true ==> 标识显示输入框 获取当前input对象, 调用focus() ==> 获取焦点 问题 向响应式对象上直接点添加属性不是响应式 ==> 输入框不会显示 ==> 使用$set添加edit属性 得不到input, 调用focus会报错 ==> 因为界面还没有更新, 还没有input ==> 使用$nextTick指定在 DOM更新后才去执行回调, 在回调中获取input

16. 区别MVVM与MVC

MVVM: 前台的技术 M: Model模型, 也就是包含数据的js对象 ==> data对象 V: View视图,动态显示模型对象中的数据的页面(前台渲染) ==> 模板页面 VM: ViewModel视图模型, 通过vm读取model中的数据显示到view上, 同时view输入数据改变, vm也可以将输入数据保存到model中 ==> Vue/组件的实例 MVC: 后台的技术 M: Model(模型)包含从数据库中查询得到的数据的对象 V: View(视图)动态显示模型对象中的数据的页面(后台渲染) C: Controller(控制器)接收用户提交的请求参数, 操作数据库生成动态数据并产生模型对象

17. Vue.use()做了什么

对象插件: 调用插件对象install方法(传入Vue)来安装插件(执行定义新语法的代码) 函数插件: 直接将其作为install来调用(传入Vue)来安装插件(执行定义新语法的代码)

18. 说说Vue的mixin技术

用来复用多个组件中相关的js代码的技术 将多个组件相同的js代码提取出来, 定义在一个mixin中配置对象 在多个组件中通过mixins配置引入mixin中的代码, 会自动合并到当前组件的配置中

19. Vue的组件data为什么必须是一个函数

同一个组件的多个组件实例的data必须是不同的对象(内容初始数据可以相同) 如果是data是对象, 组件的多个实例共用一个data对象 如果是函数, 组件对象通过调用函数得到的一个新的data对象

20. vuex的5大属性

state mutations actions getters modules namespaced: true

21. vuex的数据流结构图

22. vuex中的mutation可以执行异步操作吗?

功能可以 ==> 异步更新数据后界面确实会自动更新 问题 ==> vuex的调试工具监视不到mutation中的异步更新, 工具记录还是更新前的数据(不对) 扩展: 工具如何记录数据变化? ==> 每次mutation函数执行完后, 立即记录当前的数据 ==> 在mutation 中同步更新state, 才能被记录到

23. vuex中的状态数据的响应式的原理?

  1. 创建了一个vm对象
  2. state中的数据都是vm的data数据(是响应式的)
  3. 组件中读取的state数据本质读取的就是data中的数据
  4. 一旦更新了state中的数据, 所有用到这个数据的组件就会自动更新

24. vuex数据刷新丢失的问题

绑定事件监听: 在卸载前保存当前数据 在初始时读取保存数据作为状态的初始值

25. 路由组件间通信方式

query参数 params参数 props(需要配置, 而不是标签属性) true ==> 将路由的params参数映射成props ==> 只能传递param参数 对象 ==> 将对象中的属性映射成props ==> 只能传递自定义的参数 route => {} ==> 将函数返回的对象中的属性映射成props ==> 能会传递params和query参数和自 定义的 meta(也是配置) meta vuex

26. 跳转携带的参数, 刷新就丢失了

如果注册没有指定/:xxx的点位, 而跳转时通过params配置携带的参数数据, 刷新时就会丢失 因为url中没有携带的参数数据路径

new Vue({
data: {
home: {
categoryList: [],
xxx: {}
},
user: {
userInfo: {}
}
}
})
window.addEventListener('beforeunload', () => { // 当页面刷新时, 页面卸载前的事件回调
sessionStorage.setItem('CART_LIST_KEY',
JSON.stringify(this.$store.state.shopCart.cartList))
})
window.removeEventListener('beforeunload')
cartList: JSON.parse(sessionStorage.getItem('CART_LIST_KEY')) || [],

27. 编程式路由跳转到当前路由, 参数不变, 会报出错误?

前一个项目没这个问题, 后一个项目有问题 3.1.0版本(2019.8)没这个问题, 3.1.0这后才有这个问题 3.1.0之前: 返回值为undefined push(location) push(location, () => {}, () => {}) 3.1.0之后: 如果没有指定回调函数返回promise对象 push(location).then(() => {}).catch(() => {}) vue-router在3.1.0版本(2019.8)引入了push()的promise的语法, 如果没有通过参数指定回调函数 就返回一个promise来指定成功/失败的回调, 且内部会判断如果要跳转的路径和参数都没有变 化, 会抛出一个失败的promise 说明文档: https://github.com/vuejs/vue-router/releases?after=v3.3.1 解决: 办法1: 在每次push时指定回调函数或catch错误 办法2: 重写VueRouter原型上的push方法 (比较好) 1). 如果没有指定回调函数, 需要调用原本的push()后catch()来处理错误的promise 2). 如果传入了回调函数, 本身就没问题, 直接调用原本的push()就可以 扩展问题 声明式路由跳转之所有没有问题, 是因为默认传入了成功的空回调函数

28. history与hash路由的区别和原理

区别: history: 路由路径不#, 刷新会携带路由路径, 默认会出404问题, 需要配置返回首页 404: history有: 刷新请求时会携带前台路由路径, 没有对应的资源返回 hash没有: 刷新请求时不会携带#路由路径 解决: 开发环境: 如果是脚手架项目本身就配置好

push('/xxx', () => {})   ===>
push('/xxx').catch(() => {})
constoriginPush=VueRouter.prototype.push
VueRouter.prototype.push=function (location, onComplete, onAbort) {
console.log('push()', onComplete, onAbort)

// 判断如果没有指定回调函数, 通过call调用源函数并使用catch来处理错误

if (onComplete===undefined&&onAbort===undefined) { // 使用的新语法
returnoriginPush.call(this, location).catch(() => {})
} else { // 如果有指定任意回调函数, 通过call调用源push函数处理
returnoriginPush.call(this, location, onComplete, onAbort)
}
}

==> webpack ==> devServer: { historyApiFallback : true} 当使用HTML5 History API时, 所有的404请求都会响应index.html的内容 生产环境打包运行: 配置nginx hash: 路由路径带#, 刷新不会携带路由路径, 请求的总是根路径, 返回首页, 没有404问题 原理: history: 内部利用的是history对象的pushState()和replaceState() (H5新语法) hash: 内部利用的是location对象的hash语法 写hash路径 location.hash = ’#/xxx’ 读hash路径: location.hash 监视hash路径的变化: window.onhashchange = () => {}

29. 如何实现登陆后, 自动跳转到前面要访问的路由界面

在全局前置守卫中, 强制跳转到登陆页面时携带目标路径的redirect参数 在登陆成功后, 跳转到redirect参数的路由路径上

30. 路由导航守卫的理解和使用

导航守卫是什么? 导航守卫是vue-router提供的下面2个方面的功能 监视路由跳转 —>回调函数 控制路由跳转 —> 放行/不放行/强制跳转到指定位置 next(path) 应用 在跳转到界面前, 进行用户权限检查限制(如是否已登陆/是否有访问路由权限) 在跳转到登陆界面前, 判断用户没有登陆才显示

location / {
try_files$uri $uri/ /index.html; # 所有404的请求都返回index页面
}
if (userInfo.name) {
next()
} else {
// 如果还没有登陆, 强制跳转到login
next('/login?redirect='+to.path)  // 携带目标路径的参数数据
}
awaitthis.$store.dispatch('login', {mobile, password})
// 成功了, 跳转到redirect路由或首页
constredirect=this.$route.query.redirect
this.$router.replace(redirect||'/')

导航守卫分类 全局守卫: 针对任意路由跳转 全局前置守卫 全局后置守卫 router.afterEach((to, from) => {}) 路由独享守卫 前置守卫 组件守卫: 只针对当前组件的路由跳转 进入 更新: beforeRouteUpdate (to, from, next) {} 离开 beforeRouteLeave (to, from, next) {}

31. 请说明key的作用和原理

router.beforeEach((to, from, next) => {
// ...
})
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {}
},
{
path: '/ff',
component: Foo,
},
beforeRouteEnter (to, from, next) {
// 不能使用this, this是undefined
next((comp) => { // 延迟到组件对象创建后才执行
// comp就是组件对象
})
},
1. 虚拟DOM的key的作用?

1). 简单的说: key是虚拟DOM对象的标识, 在更新显示时key起着极其重要的作用 2). 详细的说: 当列表数组中的数据发生变化生成新的虚拟DOM后, 进行新旧虚拟DOM的diff比较

a. 有一个对应的key
item数据没变, 直接使用原来的真实DOM
item数据变了, 对原来的真实DOM进行数据更新

32. 理解Vue的虚拟DOM

33. 说说Vue的 Diff算法

b. 没有一个对应的key
根据item数据创建新的真实DOM显示
2. key为index的问题

1). 添加/删除/排序 => 产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低 2). 如果item界面还有输入框 => 产生错误的真实DOM复用 ==> 不仅效率低, 界面效果也有问题 注意: 如果不存在添加/删除/排序操作, 用index没有问题

3. 解决:

使用item数据的标识数据作为key, 比如id属性值 一个较轻的普通JS对象   ==> 真实DOM是一个较重的对象   => 轻重看对象内部属性多少 虚拟DOM对象有时也称为虚拟节点对象(vNode), 它包含了用于生成一个真实DOM/Node的必要信息, 比如: 标签结构:

<ul id="list">
<li key="1">abc1</li>
<li key="2">abc2</li>
</ul>
虚拟DOM/节点
{
tagName: 'ul',
props: {
id: 'list',
},
children: [
{tagName: 'li', props: {}, children: ['abc1'], key: '1'},
{tagName: 'li', props: {}, children: ['abc2'], key: "2"},
]
}
目标:

比较的结果是要确定: 哪些原来真实DOM可以复用(但内部内容可能要更新), 要创建哪些真实DOM 处理流程 只做同层比较 => 虚拟DOM也是一个倒立树状态结构, 只进行同层比较, 这样比较次数少,效率高 确定要比较的新旧虚拟节点

没有key: 依次比较 ==> 多出的虚拟DOM, 直接创建新的真实DOM
有key: 找同名的key比较 ==> 没有找到, 直接创建新的真实DOM

先比较标签名

如果不同, 直接创建新的真实DOM

如果相同, 复用原来对应的真实DOM => 如果数据内容有变化, 更新真实DOM内部内容

简单流程: 同层比较 => 比较key => 比较标签名 => 复用

34. nextTick的理解

Vue项目优化

1. Vue代码层面优化

v-for 遍历列表 指定非下标的唯一key, 尽量不用index, 如果只用于展示就没关系 不同时使用 v-if 图片资源懒加载 使用vue-lazyload element-ui/vant-ui Image组件本身就有懒加载的功能 路由组件懒加载 ==> 预加载 第三方插件的按需引入打包 element-ui / vant /lodash 对高频事件进行节流或防抖处理 是什么? 官方描述: 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的

DOM
在数据更新后, 调用nextTick(callback),在callback中才可以读取到更新后的DOM

使用场景? 更新数据后, 想操作更新后的DOM 项目功能1: 使用swiper实现动态轮播 项目功能2: 动态显示input输入框后, 需要自动获取焦点 原理? 更新数据后, DOM不会立即更新, 而是在所有数据都变化完后, 将DOM更新作为一个异步任务统一执行 理解Tick: 取出队列中的一个回调任务到调用栈中执行就是一个tick, 有时也指定队列中的一个回调任务

nextTick(callback): 将callback指定为下一个异步回调任务执行
为什么要先更新数据再调用nextTick()呢?

第一个数据更新才触发将DOM更新放入任务队列

这样nextTick指定的callback就是放在DOM更新任务之后

这样callback中才能读取到更新后的DOM

nextTick中用的是哪个异步技术呢?

简单说: 优先使用微任务, 如果不支持才选择宏任务

详细说: promise => MutationObserver => setImmediate => setTimeout

及时销毁事件监听 大数组优化 冻结响应式数据 虚拟列表

2. webpack配置层面优化

浏览器兼容处理 JS: @babel/polyfill => core-js配置useBuiltIns: ‘usage’ CSS: 给C3样式自动添加浏览器厂商前缀 => autoprefixer => postcss-loader 拆分打包与压缩 资源预加载(prefetch) 生产环境时不生成 SourceMap 文件名hash化=>利用浏览器缓存 代码Tree Shaking

3. 基础的Web技术层面的优化

对打包文件开启 Gzip压缩 静态资源(css/js/img)使用CDN引入

TS + Vue3

TS

1. 区别TS与JS?(说说TS的特点)

强类型, 声明变量时可以指定特定类型, 编码时可以有更友好的提示(错误或补全), 易于写出更健壮的 程序 TS支持JS的所有语法特性, 也扩展了一些新的数据类型 新的基本类型: 联合类型, 元组, 枚举 新的复杂类型: 接口, 泛型 TS浏览器是不能直接运行的, 需要编译为JS才能运行

2. 说说你对接口的理解

接口是对状态(属性)或行为(方法)的抽象(描述) 接口可以用来约束一个对象/函数/类

3. 说说你对泛型的理解

泛型: 代表不确定的类型 泛型可以用在函数/接口/类上 什么时候需要泛型呢? 定义函数/接口/类时, 要操作数据类型不确定 泛型的3个操作 定义泛型类型 函数: 定义函数的函数名的右侧 接口: 定义接口的接口名的右侧 类: 定义类的类名的右侧 使用泛型: T 函数: 参数/返回值/函数体 接口: 接口体内 类: 类体内 指定泛型对应的具体类型: <具体类型名> 函数: 调用函数时函数名的右侧 接口: 定义实现类时接口名的右侧 类: 创建实例时类名的右侧

Vue3

Vue3 比 Vue2 有什么优势

更好的代码组织和逻辑抽离

设计composition API 来代替option API

更便于可复用功能代码的封装提取, 代码可阅读性更高 体积更小 将功能以多个函数提供出来, 我们会进行按需引入使用 引入tree-shaking , 可以在打包压缩时, 将无用模块“摇掉” 性能更好/更快 使用proxy代替defineProperty来实现数据劫持 diff算法优化: 静态虚拟节点添加静态标记, 不进行diff比较 静态提升: 静态虚拟节点提升到render的外面, 缓存起来, 不创建新的 更好的 TS 支持 对TS的类型检查更友好 更好的脚手架工具vite 启动运行快了很多

Vue3 生命周期

组合API VS 选项API

composition API 优点: 更好的代码组织 更好的逻辑复用 更好的类型推导 如何选择: 不建议共用,会引起混乱 小型项目,业务逻辑简单,用 Options API 中大型项目,逻辑复杂,用 Composition API

选项式 API(Options API )

所有方法都写在 methods 中,如果 data 中数据越来越多,找数据会非常困难

<template>
<h1@click="changeCount">{{ count }}</h1>
</template>
<script>
exportdefault {
name: 'App',
data() {
return {
count: 0,
}
},
methods: {
changeCount() {
this.count++
},
组合式 API(Composition API )

逻辑会清晰,可以让功能的代码集中抽取到一个函数中进行逻辑复用

常用的组合API

启动函数 setup() 响应式: 核心 ref() reactive() computed() watch() 响应式: 工具 toRefs() 生命周期勾子 onMounted() onBeforeUnmount()

},
}
</script>
<template>
<h1@click="changeNum">{{ num }}</h1>
</template>
<script>
import { ref } from'vue'
functionuseNum() {
constnum=ref(0)
functionchangeNum() {
num.value++
}
return { changeNum, num }
}
exportdefault {
name: 'App',
setup() {
const { changeNum, num } =useNum()
return {
changeNum,
num,
}
},
}
</script>

比较Vue2与Vue3的响应式(重要)

1) vue2的响应式

核心: 对象: 通过defineProperty对对象的已有属性值的读取和修改进行劫持(监视/拦截) 数组: 通过重写数组更新数组一系列更新元素的方法来实现元素修改的劫持 问题 对象直接新添加的属性或删除已有属性, 界面不会自动更新 直接通过下标替换/添加元素或更新length, 界面不会自动更新

2) Vue3的响应式

核心: 通过Proxy(代理): 拦截对data任意属性的任意(13种)操作, 包括属性值的读写, 属性的添加, 属性 的删除等… 通过 Reflect(反射): 动态对代理对象的相应属性进行特定的操作 文档:

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Pro

xy

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Refl

ect

Object.defineProperty(data, 'count', {
get () {},
set () {}
})
constp=newProxy(data, {

// 拦截读取属性值

get (target, prop) {
returnReflect.get(target, prop)
},

// 拦截设置属性值或添加新属性

set (target, prop, value) {
returnReflect.set(target, prop, value)
},
// 拦截删除属性
deleteProperty (target, prop) {
returnReflect.deleteProperty(target, prop)
}
})
p.name='tom'
<!DOCTYPE html>
<htmllang="en">
<head>
<metacharset="UTF-8">
<metaname="viewport"content="width=device-width, initial-scale=1.0">
<title>Proxy 与 Reflect</title>
</head>
<body>
<script>
constuser= {
name: "John",
age: 12
};
/*
proxyUser是代理对象, user是被代理对象

后面所有的操作都是通过代理对象来操作被代理对象内部属性

*/
constproxyUser=newProxy(user, {
get(target, prop) {
console.log('劫持get()', prop)
returnReflect.get(target, prop)
},
set(target, prop, val) {
console.log('劫持set()', prop, val)
returnReflect.set(target, prop, val); // (2)
},
deleteProperty (target, prop) {
console.log('劫持delete属性', prop)
returnReflect.deleteProperty(target, prop)
}
});
// 读取属性值
console.log(proxyUser===user)
console.log(proxyUser.name, proxyUser.age)
// 设置属性值
proxyUser.name='bob'
proxyUser.age=13
console.log(user)
// 添加属性
proxyUser.sex='男'
console.log(user)
// 删除属性
deleteproxyUser.sex
console.log(user)
</script>
</body>
</html>

pinia VS vuex

Pinia 没有mutations , 在actions中可以直接同步更新state或异步更新state Pinia中可以包含多个store, 而且相互独立, 且不进行合并, 没有模块的嵌套结构 无需手动注册 store,创建出的store直接就可以使用 更好的TypeScript支持, 提示补全很到位

vue-router V4的变化

创建路由器有变化 new Router 变成 createRouter createWebHistory()与createWebHashHistory() 取代了 ‘history’ 与 ‘hash’ 动态添加路由 以前可以一次添加多个: router.addRoutes(routes) 现在只能一次添加一个: router.addRoute(route) 通配路由的path变了 以前的path: * 现在的path: /:pathMatch(.*)

vue3 & TS 语法

声明接收props 原生事件 原生标签上绑定 组件标签上绑定: 组件内部没有声明为自定义事件 自定义事件 全局事件总线

// 定义接口, 约束prop
interfaceProps {
count: number;
updateCount(val: number): void;
}
defineProps<Props>()

// 声明事件

constemit=defineEmits<{
(e: 'xxx', val: string): void
(e: 'click', val: object): void
(e: 'increment', val: number): void
}>()
// ts 中分发事件
emit('xxx', 'abc')

// 模板中分发事件

$emit('increment', 5)

vue本身不再提供事件总线的API: 没有$on方法了 需要使用mitt或pubsub-js 第三方工具包 v-model的本质 原生标签上: 动态value和原生的input监听 组件标签上: 动态modelValue(默认)和自定义的input监听 向外暴露方法 组件内部的方法默认在外部是不能调用的 可以通过 defineExpose暴露 通过ref得到组件对象

微信小程序相关面试题

1. 谈谈你对微信小程序的理解

<inputtype="text":value="msg1"@input="msg1=($event.target as
HTMLInputElement).value"/>
<custom-inputtitle="消息2":modelValue="msg2"
@update:modelValue="msg2=$event"/>
defineExpose({
borrowMoney
})

// 使用ref标识子组件

<Sonref="sonRef"/>
// 定义ref
const sonRef = ref<InstanceType<typeof Son> | null>(null)

// 通过ref得到子组件对象, 调用其暴露的方法

sonRef.value?.borrowMoney(num)

2. 微信小程序的分包

4. vue和小程序的区别

  1. 2017年度百度百科十大热词之一
  2. 张小龙对其的定义是无需安装,用完即走,实际上是需要安装的,只不过小程序的体积特别小,下载速度很快, 用户感觉不到下载的过程
3. 2017年1月9日0点上线
4.  没有DOM,组件化开发
  1. 体积小,单个压缩包体积不能大于2M,否则无法上线
  2. 小程序的四个重要的文件
a)  *.js
b)  *.wxml ---> view结构 ----> html
c)  *.wxss ---> view样式 -----> css
d)  *. json ----> view 数据 -----> json文件
7.  小程序适配方案: rpx (responsive pixel响应式像素单位)

1 什么要分包

  1. 小程序要求压缩包体积不能大于2M,否则无法发布
  2. 实际开发中小程序体积如果大于2M就需要使用分包机制进行发布上传
  3. 分包后可解决2M限制,并且能分包加载内容,提高性能
  4. 分包后单个包的体积不能大于2M
  5. 分包后所有包的体积不能大于20M 2 分包形式
1.  常规分包
2.  独立分包
3.  分包预下载

3 常规分包

1.  开发者通过在 app.json subpackages 字段声明项目分包结构
2.  特点:

a) 加载小程序的时候先加载主包,当需要访问分包的页面时候才加载分包内容 b) 分包的页面可以访问主包的文件,数据,图片等资源

c)  主包:
  1. 主包来源:除了分包以外的内容都会被打包到主包中
4.  通常放置启动页/tabBar页面

4 独立分包

1.  设置independent为true
2.  特点:

a) 独立分包可单独访问分包的内容,不需要下载主包 b) 独立分包不能依赖主包或者其他包的内容

3.  使用场景

a) 通常某些页面和当前小程序的其他页面关联不大的时候可进行独立分包

b)  如:临时加的广告页 || 活动页

5 分包预下载

1.  配置
a)  app.json中设置preloadRule选项
b)  key(页面路径): {packages: [预下载的包名 ||  预下载的包的根路径])}

5. 小程序部分常见面试题

vue是渐进式的框架 小程序是一种全新的连接用户与服务的方式,它可以在微信内被便捷地获取和传播

  1. vue的生命周期和小程序的生命周期对比
  2. vue中的数据代理和小程序中的是否有数据代理进行对比
  3. 组件化的对比
  4. vue中的组件通信,小程序中的页面之间通信
  5. vue中的路由和小程序的路由对比
  6. vue组件中的方法定义位置和小程序中的方法定义位置对比
7. 异步请求的方式(axios/wx.request)
  1. vue需要ie8以上浏览器,小程序在安卓/ios系统上 提高微信小程序的应用速度的常见方式有哪些?
  2. 提高页面加载速度
2. 用户行为预测
3. 减少默认data的大小
4. 组件化方案
5. 分包预下载

小程序与原生App相比优缺点? 优点: 基于微信平台开发,微信本身自带的流量是最大的优势,无需安装,只要打开微信就能用,不占用用户手机内存,体验 好,开发周期短,一般最多一个月可以上线完成,开发所需的资金少,所需资金是开发原生APP一半不到,小程序名称是 唯一性的,在微信的搜索里权重很高 容易上手,只要之前有HTML+CSS+JS基础知识,写小程序基本上没有大问题;当然如果了解ES6+CSS3则完全可以编 写出即精简又动感的小程序; 基本上不需要考虑兼容性问题,只要微信可以正常运行的机器,就可以运行小程序; 发布、审核高效,基本上上午发布审核,下午就审核通过,升级简单,而且支持灰度发布; 开发文档比较完善,开发社区比较活跃; 新增webview组件,可以展示网页 支持插件式开发,一些基本功能可以开发成插件,供多个小程序调用; 缺点: 1.局限性很强,(比如压缩体积不能超过2M。样式单一。小程序的部分组件已经是成型的了,样式不可以修改。例 如:幻灯片、导航。)只能依赖于微信依托于微信,无法开发后台管理功能。 2.不利于推广推广面窄,不能分享朋友圈,只能通过分享给朋友,附近小程序推广。其中附近小程序也受到微信的限 制 3.后台调试麻烦,因为API接口必须https请求,且公网地址,也就是说后台代码必须发布到远程服务器上;当然我们 可以修改host进行dns映射把远程服务器转到本地,或者开启tomcat远程调试;不管怎么说终归调试比较麻烦。 4.前台测试有诸多坑,最头疼莫过于模拟器与真机显示不一致

5.小程序中对js使用做了很多限制,不能使用:new Function,eval,Generator,不能操作cookie,不能操作
DOM;

原生App优点: 1、原生的响应速度快 2、对于有无网络操作时,譬如离线操作基本选用原生开发 3、需要调用系统硬件的功能(摄像头、方向传感器、重力传感器、拨号、GPS、语音、短信、蓝牙等功能) 4、在无网络或者若网的情况下体验好。 缺点: 开发周期长,开发成本高 需要下载 简述微信小程序原理? 答:微信小程序采用JavaScript、WXML、WXSS三种技术进行开发,从技术讲和现有的前端开发差不多,但深入挖 掘的话却又有所不同。 JavaScript:首先JavaScript的代码是运行在微信App中的,并不是运行在浏览器中,因此一些H5技术的应用,需 要微信App提供对应的API支持,而这限制住了H5技术的应用,且其不能称为严格的H5,可以称其为伪H5,同理,微 信提供的独有的某些API,H5也不支持或支持的不是特别好。 WXML:WXML微信自己基于XML语法开发的,因此开发时,只能使用微信提供的现有标签,HTML的标签是无法使用的。 WXSS:WXSS具有CSS的大部分特性,但并不是所有的都支持,而且支持哪些,不支持哪些并没有详细的文档。 微信的架构,是数据驱动的架构模式,它的UI和数据是分离的,所有的页面更新,都需要通过对数据的更改来实现。

小程序分为两个部分webview和appService。其中webview主要用来展现UI,appService有来处理业务逻辑、数

据及接口调用。它们在两个进程中运行,通过系统层JSBridge实现通信,实现UI的渲染、事件的处理 分析下微信小程序的优劣势? 优势: 1、无需下载,通过搜索和扫一扫就可以打开。 2、良好的用户体验:打开速度快。 3、开发成本要比App要低。 4、安卓上可以添加到桌面,与原生App差不多。 5、为用户提供良好的安全保障。小程序的发布,微信拥有一套严格的审查流程,不能通过审查的小程序是无法发布 到线上的。 劣势: 1、限制较多。页面大小不能超过1M。不能打开超过5个层级的页面。 2、样式单一。小程序的部分组件已经是成型的了,样式不可以修改。例如:幻灯片、导航。 3、推广面窄,不能分享朋友圈,只能通过分享给朋友,附近小程序推广。其中附近小程序也受到微信的限制。 4、依托于微信,无法开发后台管理功能。 小程序的发布流程(开发流程) 注册微信小程序账号 获取微信小程序的 AppID 下载微信小程序开发者工具

创建demo项目

去微信公众平台配置域名 手机预览 代码上传 提交审核 小程序发布 webview中的页面怎么跳回小程序中 首先,需要在你的html页面中引用一个js文件。

<script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.3.0.js">
</script>

然后为你的按钮标签注册一个点击事件:

$(".kaiqi").click(function(){undefined
wx.miniProgram.redirectTo({url: '/pages/indexTwo/indexTwo'})
});

这里的redirectTo跟小程序中的wx.redirectTo()跳转页面是一样的,会关闭当前页跳转到页面。 你也可以替换成navigateTo,跳转页面不会关闭当前页。 使用webview直接加载要注意哪些事项? 一、必须要在小程序后台使用管理员添加业务域名; 二、h5页面跳转至小程序的脚本必须是1.3.1以上; 三、微信分享只可以都是小程序的主名称了,如果要自定义分享的内容,需小程序版本在1.7.1以上; 四、h5的支付不可以是微信公众号的appid,必须是小程序的appid,而且用户的openid也必须是用户和小程序的。 小程序授权登录流程。

HTML + CSS

1. 标签语义化的意义

(授权,微信登录获取code,微信登录,获取 iv , encryptedData 传到服务器后台,如果没有注册,需要注 册。) (授权,微信登录获取code,微信登录,获取 iv , encryptedData 传到服务器后台,如果没有注册,需要注 册。) 小程序支付如何实现? 1、小程序注册,要以公司的以身份去注册一个小程序,才有微信支付权限; 2、绑定商户号。 3、在小程序填写合法域

4.调用wx.login()获取appid
5.调用
wx.requestPayment(
{
'timeStamp': '',//时间戳从1970年1月1日00:00:00至今的秒数,即当前的时间
'nonceStr': '',//随机字符串,长度为32个字符以下。
'package': '',//统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=*
'signType': 'MD5',//签名类型,默认为MD5,支持HMAC-SHA256和MD5。注意此处需与统一下单的签名类型一

致 ‘paySign’: ”,//签名,具体签名方案参见微信公众号支付帮助文档;

'success':function(res){},//成功回调
'fail':function(res){},//失败
'complete':function(res){}//接口调用结束的回调函数(调用成功、失败都会执行)
})

小程序还有哪些功能? 客服功能,录音,视频,音频,地图,定位,拍照,动画,canvas 微信小程序与H5的区别? 第一条是运行环境的不同 传统的HTML5的运行环境是浏览器,包括webview,而微信小程序的运行环境并非完整的浏览器,是微信开发团队基 于浏览器内核完全重构的一个内置解析器,针对小程序专门做了优化,配合自己定义的开发语言标准,提升了小程序的 性能。 第二条是开发成本的不同 只在微信中运行,所以不用再去顾虑浏览器兼容性,不用担心生产环境中出现不可预料的奇妙BUG 第三条是获取系统级权限的不同 系统级权限都可以和微信小程序无缝衔接 第四条便是应用在生产环境的运行流畅度 长久以来,当HTML5应用面对复杂的业务逻辑或者丰富的页面交互时,它的体验总是不尽人意,需要不断的对项目优化 来提升用户体验。但是由于微信小程序运行环境独立 小程序怎么实现下拉刷新?

方式1:
通过在 app.json 中,将 "enablePullDownRefresh": true, 开启全局下拉刷新。
或者通过在组件 .json ,将 "enablePullDownRefresh": true, 单组件下拉刷新。
方式2:
scroll-view :使用该滚动组件自定义刷新,通过 bindscrolltoupper 属性,当滚动到顶部/左边,会触发

scrolltoupper事件,所以我们可以利用这个属性,来实现下拉刷新功能。

  1. 开发者更容易理解,减少差异化,方便团队开发和维护
  2. 机器更容易理解结果(搜索爬虫、方便其他设备解析(读屏幕软件、盲人设备、移动设备)

2. 写页面结构应该注意什么

  1. 尽可能少的使用没有语义的 div 和 span 元素
  2. 块级元素和内联元素的嵌套一定要符合 web 标准,比如内联元素就是不能嵌套块级元素

3. HTML5 新特性

  1. 新的语义化元素:article 、footer 、header 、nav 、section
  2. 表单增强,新的表单控件:calendar 、date 、time 、email 、url 、search
  3. 新的 API:音频(用于媒介回放的 video 和 audio 元素)、图形(绘图 canvas 元素)
  4. 新的 API:离线,通过创建 cache manifest 文件,创建应用程序缓存
  5. 新的 API:本地存储,localStorage-没有时间限制的数据存储,sessionStorage-session 数据存储 (关闭浏览器窗口数据删除)
  6. 新的 API:实时通讯,设备能力

4. CSS3 新特性

  1. CSS3实现圆角(border-radius),阴影(box-shadow),
  2. 对文字加特效(text-shadow、),线性渐变(gradient),旋转(transform)
  3. transform:rotate(9deg) scale(0.85,0.90) translate(0px,-30px) skew(-9deg,0deg);//旋转,缩放,定位,倾斜3.
  4. 增加了更多的CSS选择器多背景 rgba
  5. 在CSS3中唯一引入的伪元素是::selection.
  6. 媒体查询,多栏布局

5. 盒子模型

页面渲染时,dom 元素所采用的布局模型。可通过box-sizing 进行设置。根据计算宽高的区域可分 为: content-box (W3C 标准盒模型) border-box (IE 盒模型)

padding-box

margin-box (浏览器未实现)

6. BFC

块级格式化上下文,是一个独立的渲染区域,让处于 BFC 内部的元素与外部的元素相互隔离,使内外元 素的定位不会相互影响。 IE 下为 Layout,可通过 zoom:1 触发 触发条件: 根元素

position: absolute/fixed
display: inline-block / table
float元素
ovevflow  !== visible

规则: 属于同一个 BFC 的两个相邻 Box 垂直排列 属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠 BFC 中子元素的 margin box 的左边,与包含块 (BFC) border box 的左边相接触 (子元素 absolute 除外) BFC 的区域不会与 float 的元素区域重叠 计算 BFC 的高度时,浮动子元素也参与计算 - 文字层不会被浮动层覆盖,环绕于周围 应用:

阻止margin 重叠

可以包含浮动元素 —— 清除内部浮动(清除浮动的原理是两个div 都位于同一个 BFC 区域之中) 自适应两栏布局 可以阻止元素被浮动元素覆盖

7. 选择器优先级

!important > 行内样式 > #id > .class > tag > * > 继承 > 默认 选择器从右往左解析

8. 去除浮动影响,防止父级高度塌陷

通过增加尾元素清除浮动

:after / <br> : clear: both

创建父级 BFC 父级设置高度

link 功能较多,可以定义 RSS,定义 Rel 等作用,而@import 只能用于加载 css 当解析到link 时,页面会同步加载所引的 css,而@import 所引用的 css 会等到页面加载完才被 加载 @import 需要 IE5 以上才能使用 link 可以使用 js 动态引入,@import 不行

10. CSS 预处理器(Sass/Less/Postcss)

CSS 预处理器的原理: 是将类 CSS 语言通过Webpack 编译转成浏览器可读的真正 CSS。在这层编译之 上,便可以赋予 CSS 更多更强大的功能,常用功能: 嵌套 变量 循环语句 条件语句 自动前缀 单位转换 mixin 复用

11. 单行文本溢出

12. 多行文本溢出

13. flex布局

Flex是Flexible Box的缩写,意为”弹性布局”,用来为盒状模型提供最大的灵活性。 布局的传统解决方案,基于盒状模型,依赖display属性 + position属性 + float属性。它对于那 些特殊布局非常不方便,比如,垂直居中就不容易实现。 分清主轴和交叉轴 flex-direction 为 row 时,主轴是横向的,相反的为 column 时,主轴是纵向的。 简单的分为容器属性和元素属性 容器的属性: flex-direction:决定主轴的方向(即子item的排列方法) flex-wrap:决定换行规则 flex-flow:flex-direction 和flex-wrap 的简写形式,默认值为row nowrap justify-content:对其方式,水平主轴对齐方式 align-items:对齐方式,竖直轴线方向 项目的属性(元素的属性): flex:是flex-grow和flex-shrink、flex-basis的简写,默认值为0 1 auto。

white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2; /*2*/
-webkit-box-orient: vertical;

order属性:定义项目的排列顺序,顺序越小,排列越靠前,默认为0 flex-grow属性:定义项目的放大比例,即使存在空间,也不会放大 flex-shrink属性:定义了项目的缩小比例,当空间不足的情况下会等比例的缩小,如果定义个 item的flow-shrink为0,则为不缩小 flex-basis属性:定义了在分配多余的空间,项目占据的空间。

14. 圣杯布局

圣杯特点简单记为:有头、有尾、包三列,圣杯布局中间有container大容器包裹着左、中、右三列 区域 左、中、右是独立的三个区域,都处于一个层级

15. 双飞翼布局

左、中、右是独立的三个区域,中间区域属于最上面的层级

移动端

1. 基础知识

1英寸(inch) = 2.54厘米(cm) IPhone 6 的屏幕分辨率为 750 * 1334 设备独立像素为 375 * 667 物理像素:由屏幕制造商决定,屏幕生产后无法修改 css像素:单位是px,它是为 Web 开发者创造的 设备独立像素的出现,使得即使在【高清屏】下,也可以让元素有正常的尺寸,让代码不受到设备 的影响,它是设备厂商根据屏幕特性设置的,无法更改。 1个位图像素对应1个物理像素,图片才能得到完美清晰的展示 pc端视口:默认宽度和浏览器窗口的宽度一致,也被称为初始包含块

document.documentElement.clientWidth

移动端视口: 布局视口:一般是980px左右,布局视口经过压缩后,横向的宽度用css像素表达就不再是 375px了,而是980px 视觉视口:用户可见的区域,它的绝对宽度永远和设备屏幕一样宽 理想视口:布局视口宽度与屏幕等宽(设备独立像素),靠meta标签实现

2. 适配

<metaname="viewport"content="width=device-width,initial-scale=1.0"/>

1.viewport 适配

方法:拿到设计稿之后,设置布局视口宽度为设计稿宽度,然后直接按照设计稿给宽高进行布局即 可。 一般适用于:计图稿宽度 < 375

2.rem适配

方案一:(百度)

  1. 设置完美视口
  2. 通过js设置根字体大小 = ( 当前设备横向独立像素值 *100) / 设计稿宽度
  3. 编写样式时,直接以rem为单位,值为:设计值 / 100
  4. 增加 JS 代码进行实时适配 方法二:(淘宝、搜狐、唯品会)
  5. 设置完美视口
  6. 通过js设置根字体大小 = 当前设备横向独立像素值 / 10
  7. 编写样式时,直接以rem为单位,值为:设计值 / (设计稿宽度 / 10)
  8. 增加 JS 代码进行实时适配

3.vw适配

vw和vh是两个相对单位 1vw = 等于布局视口宽度的1% 1vh = 等于布局视口高度的1%

4.1px物理像素边框

高清屏幕下 1px 对应更多的物理像素,所以 1 像素边框看起来比较粗,解决方法如下

方法一

使用媒查询:

方法二

根据dpr扩大布局视口,例如dpr为n则布局视口改为原来的n倍,则元素尺寸均变为原来的n分之一,为了 保证元素尺寸比例不变,扩大根字体为原来的n倍,但整个过程中边框一直用px作为单位,不用rem。

  1. rem 页面布局
  2. 元素的边框设置为 1px
  3. 通过 viewport 中的 initial-scale 将布局视口扩大n倍,这样页面元素就比原来缩小了n倍
@mediascreenand (-webkit-min-device-pixel-ratio:2){
#demo{
border: 0.5pxsolidblack;
}
}

或

@mediascreenand (-webkit-min-device-pixel-ratio:2){
#demo2::after{
transform:scaleY(0.5);
}
}
  1. 重新设置根元素字体

3. 移动端事件

touchstart 元素上触摸开始时触发 touchmove 元素上触摸移动时触发 touchend 手指从元素上离开时触发 touchcancel 触摸被打断时触发

4. 移动端中touchstart,touchend,click执行顺序

touchstart touchend click,浏览器在 click 后会等待约300ms去判断用户是否有双击行为,如果300ms内没有再一次 click,那么就判定这是一次单击行为

5. 点击穿透

touch 事件结束后会默认触发元素的 click 事件 方法一:阻止默认行为 方法二:使背后元素不具备click特性,用touchXxxx代替click 方法三:让背后的元素暂时失去click事件,300毫秒左右再复原,属性pointer-events: none; 方法四:让隐藏的元素延迟300毫秒左右再隐藏

varviewport=document.querySelector('meta[name=viewport]')
varscale=1/window.devicePixelRatio
viewport.setAttribute('content', 'width=device-width,initial-scale='+scale);
varfontSize=parseInt(document.documentElement.style.fontSize);
document.documentElement.style.fontSize=fontSize*window.devicePixelRatio+'px'