vue2
是用过Object.defineProperty
实现数据响应式, 组件初始化时,对 data 中的 item 进行递归遍历,对 item 的每一个属性进行劫持,添加 set , get 方法。我们后来 新加的属性 ,并没有通过Object.defineProperty
设置成响应式数据,修改后不会视图更新。
通过数组索引号修改了数组,界面会不会相应更新?为什么?
答:不会。vue 监听不到
vue 为什么没有提供 arr[下标] = val
变成响应式?
尤大:“因为性能问题,性能代价和获得的用户体验收益不成正比”
对象:使用了Object.defineProperty
中的 get 和 set
如何监测对象中的数据?
通过 setter 实现监视,且要在 new Vue 时就传⼊要监测的数据
- 对象中后追加的属性,Vue 默认不做响应式处理
- 如需给后添加的属性做响应式,请使⽤如下 API:
Vue.set(target,propertyName/index,value)
vm.$set(target,propertyName/index,value)
数组: Vue
重写了数组的原型,更准确的表达是拦截了数组的原型
如何监测数组中的数据?
通过包裹数组更新元素的⽅法实现,本质就是做了两件事:
调⽤原⽣对应的⽅法对数组进⾏更新
重新解析模板,进⽽更新⻚⾯
在 Vue 修改数组中的某个元素⼀定要⽤如下⽅法:
- 使 ⽤ 这 些 API :
push() pop() shift() unshift() splice() sort() reverse()
Vue.set()
或vm.$set()
- 覆盖整个数组
为什么对象和数组要分开处理?
对象
的属性通常比较少,对每一个属性都劫持set和get
,并不会消耗很多性能
数组
有可能有成千上万个元素,如果每一个元素都劫持set和get
,无疑消耗太多性能了所以
对象
通过defineProperty
进行正常的劫持set和get
数组
则通过修改数组原型上的部分方法
,来实现修改数组触发响应式
生命周期 | 执行时机 |
---|---|
beforeCreate |
在组件实例被创建之初、组件的属性⽣效之前被调用 |
created |
在组件实例已创建完毕。此时属性也已绑定,但真实 DOM 还未⽣成,$el 还不可⽤ |
beforeMount |
在组件挂载开始之前被调⽤。相关的 render 函数⾸次被调⽤ |
mounted |
在 el 被新建的 vm.$el 替换并挂载到实例上之后被调用 |
beforeUpdate |
在组件数据更新之前调⽤。发⽣在虚拟 DOM 打补丁之前 |
update |
在组件数据更新之后被调用 |
activited |
在组件被激活时调⽤(使用了 的情况下) |
deactivated |
在组件被销毁时调⽤(使用了 的情况下) |
beforeDestory |
在组件销毁前调⽤ |
destoryed |
在组件销毁后调⽤ |
加载渲染过程
->父beforeCreate
->父created
->父beforeMount
->子beforeCreate
->子created
->子beforeMount
->子mounted
->父mounted
更新过程
父beforeUpdate->子beforeUpdate->子updated->父updated
销毁过程
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
keep-alive
可以实现组件缓存,当组件切换时不会对当前组件进行卸载
https://juejin.cn/post/6844903641866829838#heading-11
https://juejin.cn/post/7114252241166401573
官方文档:对于任何复杂逻辑,你都应当使用计算属性
扩展:vue 中 Computed、Methods、Watch 区别
computed(计算属性) | watch(监视属性/侦听器) |
---|---|
根据你所依赖的数据动态显示新的计算结果 不用再 data 中声明,否则报错 |
data 的数据监听回调,依赖 data 的数据变化 直接使用 data 声明的数据 |
支持缓存 | 不支持缓存 |
不支持异步 | 支持异步 |
有 get 和 set 方法,当数据变化时,调用 set 方法 | 可以深度监视 deep,加载就调用 immediate 监听的函数接收两个函数,newVal 和 oldVla |
当需要进行数值计算,并且依赖于其它数据时,用 computed | 在某个数据变化时做一些事情或需要异步操作时,用 watch |
computed 能做的 | watch 都能做到 |
props
$emit
.sync
v-model
$parent / $children
$parent 获取父组件的实例,任意调用父组件的方法,修改父组件的数据ref
父组件获取子组件实例,任意调用子组件的方法获取子组件的属性provide / inject
prpvide 父组件内部提供数据 inject 嵌套的子组件可以注入数据$attrs / $listeners
$attrs(没有被 props 接收的所有自定义属性) $listeners(可以获取所有的父组件传递过来的自定义事件)eventBus
定义一个事件总线 使用$on
绑定$emit
触发vuex
- 路由传参
https://www.wpgdadatong.com/cn/blog/detail?BID=B3650
https://juejin.cn/post/7110223595359436813
NextTick 是什么
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM
// 修改数据
this.message = "修改后的值";
// 此时DOM还没有更新
console.log(this.$el.textContent); // => '原始的值'
this.$nextTick(function () {
// DOM 更新了
console.log(this.$el.textContent); // => '修改后的值'
});
引用:https://juejin.cn/post/7026867875990208543#heading-22
stop
stop
修饰符的作用是阻止冒泡prevent⭐
prevent
修饰符的作用是阻止默认事件(例如 a 标签的跳转)capture
self
self
修饰符作用是,只有点击事件绑定的本身才会触发事件once
once
修饰符的作用是,事件只执行一次native⭐
native
修饰符是加在自定义组件的事件上,保证事件能执行native
是用来是在父组件中给子组件绑定一个原生的事件,就将子组件变成了普通的 HTML 标签看待passive
sync⭐
当父组件
传值进子组件
,子组件想要改变这个值时,可以这么做
// 父组件里
<children :foo.sync="bar"></children>
// 子组件里
this.$emit('update:foo', newValue)
camel
.camel
修饰符允许在使用 DOM 模板时将 v-bind
property 名称驼峰化,例如 SVG 的 viewBox
property:
<svg :view-box.camel="viewBox"></svg>
left
right
middle
trim⭐
trim
修饰符的作用类似于 JavaScript 中的trim()
方法,作用是把v-model
绑定的值的首尾空格给过滤掉。lazy
lazy
修饰符作用是,改变输入框的值时 value 不会改变,当光标离开输入框时,v-model
绑定的值 value 才会改变number
number
修饰符的作用是将值转成数字,但是先输入字符串和先输入数字,是两种情况:先输入数字的话,只取前面数字部分
先输入字母的话,
number
修饰符无效
.ctrl
、.alt
、.shift
、.meta
配合键盘事件使用:
.enter
.tab
.delete
.esc
.space
.up
.down
left
…
FileReader 与 URL.createObjectURL 实现图片、视频上传预览
event.target.files 就是用户上传的图片信息
配合 cropperjs 可以实现图片裁剪功能
// 如果接口要求 Content-Type 是 multipart/form-data
// 则你必须传递 FormData 对象
为什么 v-if 和 v-for 不能同时使用?
v-if 不能和 v-for 一起使用的原因是 v-for 的优先级比 v-if 高,一起使用会造成性能浪费
解决方案有两种,把 v-if 放在 v-for 的外层或者把需要 v-for 的属性先从计算属性中过滤一次
v-if 和 v-for 的优先级问题在 vue3 中不需要考虑,vue3 更新了 v-if 和 v-for 的优先级,使 v-if 的优先级高于 v-for
v-if 和 v-show
v-show
隐藏则是为该元素添加display:none
。v-if
是将dom
元素整个添加或删除
v-show
由false
变为true
的时候不会触发组件的生命周期v-if
由false
变为true
的时候,触发组件的beforeCreate
、create
、beforeMount
、mounted
钩子,由true
变为false
的时候触发组件的beforeDestory
、destoryed
方法性能消耗:v-if
有更高的切换消耗;v-show
有更高的初始渲染消耗;
如果需要非常频繁地切换,则使用 v-show 较好
如果在运行时条件很少改变,则使用 v-if 较好
Vue.use
是用来安装插件的
用法:Vue.use(plugin)
install
方法。会将 Vue 作为参数传入
。总结:Vue.use 是官方提供给开发者的一个 api,用来注册、安装类似 Vuex、vue-router、ElementUI 之类的插件的。
跨域问题是浏览器的同源策略所导致的
其中,域名、协议、端口号相同,称之为同源,如果不同,称之为跨源或跨域
跨域常见的解决方法:
代理适用的场景是:生产环境不发生跨域,但开发环境发生跨域
因此,只需要在开发环境使用代理解决跨域即可,这种代理又称之为开发代理
在实际开发中,只需要对开发服务器稍加配置即可完成
// vue 的开发服务器代理配置
// vue.config.js
module.exports = {
devServer: {
// 配置开发服务器
proxy: {
// 配置代理
"/api": {
// 若请求路径以 /api 开头
target: "http://dev.taobao.com", // 将其转发到 http://dev.taobao.com
},
},
},
};
阮一峰 CORS: https://www.ruanyifeng.com/blog/2016/04/cors.html
CORS
是基于http1.1
的一种跨域解决方案,它的全称是Cross-Origin Resource Sharing,跨域资源共享。CORS 需要浏览器和后端同时支持。
整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。
浏览器将 CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)
只要同时满足以下两大条件,就属于简单请求。
凡是不同时满足下面两个条件,就属于非简单请求。
(1) 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
(2)HTTP 的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
JSONP 的做法是:当需要跨域请求时,不使用 AJAX,转而生成一个 script 元素去请求服务器,由于浏览器并不阻止 script 元素的请求,这样请求可以到达服务器。服务器拿到请求后,响应一段 JS 代码,这段代码实际上是一个函数调用,调用的是客户端预先生成好的函数,并把浏览器需要的数据作为参数传递到函数中,从而间接的把数据传递给客户端
JSONP 优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持 get 方法具有局限性,不安全可能会遭受 XSS 攻击。
什么是 cookie
cookie 是储存在用户本地终端上的数据,是网站为了识别用户和跟踪会话而存储在用户本地终端中的文本数据
怎么操作
可以使用
js-cookie
插件模块化开发时直接引入
import Cookies from 'js-cookie'
js-cookie.js 常用的 API 和方法
设置 cookie
Cookies.set("name", "value", { expires: 7, path: "" }); //7天过期 Cookies.set("name", { foo: "bar" }); //设置一个json
读取 cookie
Cookies.get("name"); //获取cookie Cookies.get(); //读取所有的cookie
删除 cookie
Cookies.remove("name"); //删除cookie时必须是同一个路径。
- vue 自带的组件 >> 主要功能是缓存组件 >> 提升性能
- 使用场景:可以少网络请求,如果当前组件数据量比较大,就可以节省网络请求 >> 提升用户体验
- 举例:如果详情页面之间进行切换,就可以使用
keep-alive
进行缓存组件,防止同样的数据重复请求
keep-alive
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们
keep-alive
可以设置以下props
属性:
include
- 字符串或正则表达式。只有名称匹配的组件会被缓存exclude
- 字符串或正则表达式。任何名称匹配的组件都不会被缓存max
- 数字。最多可以缓存多少组件实例关于keep-alive
的基本用法:
<keep-alive>
<component :is="view"></component>
</keep-alive>
匹配首先检查组件自身的 name
选项,如果 name
选项不可用,则匹配它的局部注册名称 (父组件 components
选项的键值),匿名组件不能被匹配
设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(activated
与deactivated
):
beforeRouteEnter
> beforeCreate
> created
> mounted
> activated
> … … > beforeRouteLeave
> deactivated
beforeRouteEnter
>activated
> … … > beforeRouteLeave
> deactivated
使用原则:当我们在某些场景下不需要让页面重新加载时我们可以使用keepalive
举个栗子:
当我们从首页
–>列表页
–>商详页
–>再返回
,这时候列表页应该是需要keep-alive
从首页
–>列表页
–>商详页
–>返回到列表页(需要缓存)
–>返回到首页(需要缓存)
–>再次进入列表页(不需要缓存)
,这时候可以按需来控制页面的keep-alive
在路由中设置keepAlive
属性判断是否需要缓存
{
path: 'list',
name: 'itemList', // 列表页
component (resolve) {
require(['@/pages/item/list'], resolve)
},
meta: {
keepAlive: true,
title: '列表页'
}
}
使用
<div id="app" class='wrapper'>
<keep-alive>
<!-- 需要缓存的视图组件 -->
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<!-- 不需要缓存的视图组件 -->
<router-view v-if="!$route.meta.keepAlive"></router-view>
</div>
解决方案可以有以下两种:
作用:
作用:使样式私有化(模块化),不对全局造成污染
原理:动态的给组件加上一个 hash 值,用属性选择器去匹配
$route:当前的路由信息对象,获取到路由参数、路径
- $route.path: 字符串,对应当前路由的路径,总是解析为绝对路径,如
/foo/bar
。- $route.params: 一个 key/value 对象,包含了 动态片段 和 全匹配片段,如果没有路由参数,就是一个空对象。
- ** r o u t e . q u e r y : ∗ ∗ 一个 k e y / v a l u e 对象,表示 U R L 查询参数。例如,对于路径 / f o o ? u s e r = 1 ,则有 route.query:** 一个 key/value 对象,表示 URL 查询参数。例如,对于路径 /foo?user=1,则有 route.query:∗∗一个key/value对象,表示URL查询参数。例如,对于路径/foo?user=1,则有route.query.user == 1,如果没有查询参数,则是个空对象。
- $route.hash: 当前路由的 hash 值 (不带#) ,如果没有 hash 值,则为空字符串。锚点
- $route.fullPath: 完成解析后的 URL,包含查询参数和 hash 的完整路径。
- $route.matched: 数组,包含当前匹配的路径中所包含的所有片段所对应的配置参数对象。
- $route.name: 当前路径名字
- $route.meta: 路由元信息
** r o u t e r ∗ ∗ :全局路由 v u e R o u t e r 的实例,挂载到 V u e 原型上 router**:全局路由 vueRouter 的实例,挂载到 Vue 原型上 router∗∗:全局路由vueRouter的实例,挂载到Vue原型上router 属性,可以获取到全局路由配置信息,跳转方法
$router.replace({path:‘home’}),//替换路由,没有历史记录
$router.push(‘/login’) ,跳转到指定路由
$router.back()
$router.go()
发布订阅模式中有三个角色,发布者
Publisher
,信息中心Event Channel
,订阅者Subscriber
。我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern)
通常是通过 on 事件订阅消息,emit 事件发布消息,remove 事件删除订阅
class PubSub {
constructor() {
// 事件中心
// 存储格式: warTask: [], routeTask: []
// 每种事件(任务)下存放其订阅者的回调函数
this.events = {};
}
// 订阅方法
subscribe(type, cb) {
if (!this.events[type]) {
}
this.events[type].push(cb);
}
// 发布方法
publish(type, ...args) {
if (this.events[type]) {
this.events[type].forEach((cb) => cb(...args));
}
}
// 取消订阅方法
unsubscribe(type, cb) {
if (this.events[type]) {
const cbIndex = this.events[type].findIndex((e) => e === cb);
if (cbIndex !== -1) {
this.events[type].splice(cbIndex, 1);
}
}
if (this.events[type].length === 0) {
delete this.events[type];
}
}
unsubscribeAll(type) {
if (this.events[type]) {
delete this.events[type];
}
}
}
当对象之间存在一对多的依赖关系时,其中一个对象的状态发生改变,所有依赖它的对象都会收到通知,这就是观察者模式
- 观察者(订阅者) – Watcher
- update():当事件发生时,具体要做的事情
- 目标(发布者) – Dep
- subs 数组:存储所有的观察者
- addSub():添加观察者
- notify():当事件发生,调用所有观察者的 update() 方法
- 没有事件中心
设计模式 | 观察者模式 | 发布订阅模式 |
---|---|---|
主体 | Watcher 观察者、Dep 目标对象 | Publisher 发布者、Event Channel 信息中心、Subscribe 订阅者 |
主体关系 |
Dep 中通过 subs 记录 Watcher | Publisher 和 Subscribe 不想不知道对方,通过中介联系 |
优点 | 角色明确,Watcher 和 Dep 要遵循约定的成员方法 | 松散耦合,灵活度高,通常应用在异步编程中 |
缺点 | 紧耦合 | 当事件类型变多时,会增加维护成本 |
使用案例 | 双向数据绑定 | 事件总线 EventBus |
相同点:都是语法糖,都可以实现父子组件中的数据的双向通信。
// v-model <son v-model="num"/> //父组件使用子组件 model:{ prop:'newValue', // 默认为 value 可以使用prop自定义属性名 event:"updateValue", // event 修改事件名 默认为input }, props: { // 子组件接收 value: { // 默认为value type: Number, } } // .sync <son :title.sync="doc.title"></son> // 父组件 props:{ title:{ type:... } } this.$emit('update:title', newTitle) // 子组件
区别点:格式不同: v-model=“num”, :num.sync=“num”
v-model
: @input + value
:num.sync
: @update:num另外需要特别注意的是:
v-model
只能用一次;.sync
可以有多个
背景:修改当前组件嵌套的子组件内部的样式
问题:
如何解决:添加/deep/
/ ::v-deep
scss: 使用
::v-deep
less: 使用
/deep/
SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。
优点:
缺点:
①单向数据流是指数据从父组件传向子组件,子组件没有权限直接修改该数据;
②子组件需要在 data 或者 computed 中重新定义变量来接收父组件传来的值,以便修改;
③子组件可以通过 $emit 的方式通知父组件修改值,再重新传回给子组件;
防抖
和节流
防抖:在限定时间内,总是执行最后一次 ( 类似回城,打断就得重新回 )
节流:在限定时间内,只会执行第一次 ( 类似技能需要冷却时间到了才能用 )
/**
* debounce 防抖
* @param fn [function] 需要防抖的函数
* @param delay [number] 毫秒,防抖期限值
*/
function debounce(fn, delay) {
let timer = null; //借助闭包
return function (...arg) {
if (timer) {
//进入该分支语句,说明当前正在一个计时过程中,并且又触发了相同事件。
// 所以要取消当前的计时,重新开始计时
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, arg); // 使用apply将fn函数的this指向修改为return后的function
}, delay); // 进入该分支说明当前并没有在计时,那么就开始一个计时
};
}
/**
* debounce 节流
* @param fn [function] 需要节流的函数
* @param delay [number] 毫秒
*/
function throttle(fn, delay) {
let valid = false; // 节流阀
return function (...arg) {
if (valid) {
//休息时间 暂不接客
return;
}
// 工作时间,执行函数并且在间隔期内把状态位设为无效
valid = true;
setTimeout(() => {
fn.apply(this, arg);
valid = false;
}, delay);
};
}
1.是否存在变量提升?
var
声明的变量存在变量提升(将变量提升到当前作用域的顶部)。即变量可以在声明之前调用,值为undefined
let
和const
不存在变量提升。即它们所声明的变量一定要在声明后使用,否则报ReferenceError
错2.是否存在暂时性死区?
let和const存在暂时性死区
。即只要块级作用域内存在 let 命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响3.是否允许重复声明变量?
var
允许重复声明变量。
let
和const
在同一作用域不允许重复声明变量。4.是否存在块级作用域?
- var 不存在块级作用域。
- let 和 const 存在块级作用域。
- 块作用域由
{ }
包括,if
语句和for
语句里面的{ }
也属于块作用域5. 是否能修改声明的变量?
var
和let
可以。const
声明一个只读的常量。一旦声明,常量的值就不能改变。const
声明的变量不得改变值,这意味着,const
一旦声明变量,就必须立即初始化,不能留到以后赋值。
1.箭头函数是匿名函数,不能作为构造函数,不能使用 new
2.箭头函数内没有
arguments
,可以用展开运算符...
解决3.箭头函数的 this,始终指向父级上下文(箭头函数的
this
取决于定义位置父级的上下文
,跟使用位置没关系,普通函数this
指向调用的那个对象)4.箭头函数不能通过
call() 、 apply() 、bind()
方法直接修改它的 this 指向。(call、apply、bind
会默认忽略第一个参数,但是可以正常传参)5.箭头函数没有原型属性
状态
进行中:pending
已成功:resolved
已失败:rejected
特点
对象的状态不受外界影响
一旦状态改变就不会再变,任何时候都可得到这个结果
声明:new Promise((resolve, reject) => {})
出参
resolve:将状态从未完成
变为成功
,在异步操作成功时调用,并将异步操作的结果作为参数传递出去
reject:将状态从未完成
变为失败
,在异步操作失败时调用,并将异步操作的错误作为参数传递出去
① 什么是 Promise?
容器
,包含异步操作结果的对象对象
,从它可以获取异步操作的的最终状态(成功或失败)。构造函数
,对外提供统一的 API,自己身上有 all、reject、resolve 等方法,原型上有 then、catch 等方法。② Promise 有什么用?
解决回调地狱
③Promise 有哪些方法
**then():**分别指定resolved状态
和rejected状态
的回调函数
第一参数:状态变为resolved
时调用
第二参数:状态变为rejected
时调用(可选)
链式调用 promise.then():then
方法返回一个 Promise 对象,其允许方法链,从而创建一个 promise 链
catch():指定发生错误时的回调函数
Promise.resolve():将对象转为 Promise 对象 等价于 new Promise(resolve => resolve())
Promise 实例:原封不动地返回入参
thenable 对象:
thenable
对象指的是具有then
方法的对象
Promise.resolve
方法会将这个对象转为 Promise 对象,然后就立即执行thenable
对象的then
方法
不具有 then()的对象:将此对象转为 Promise 对象并返回,状态为resolved
不带参数:返回 Promise 对象,状态为resolved
Promise.reject():将对象转为状态为rejected
的 Promise 对象(等价于new Promise((resolve, reject) => reject())
)
Promise.all():
并发,发起多个并发请求,将多个实例包装成一个新实例(数组形式),然后在所有 promise 都被解决后执行一些操作(齐变更再返回)
fulfilled
( 成功 ),最终状态才会变成fulfilled
rejected
,最终状态就会变成rejected
rejected
Promise.race():
赛跑机制
将多个实例包装成一个新实例,返回全部实例状态优先变更后的结果(先变更先返回)
status
和value
,status
为fulfilled
,value
为返回值status
和value
,status
为rejected
,value
为错误原因成功:其中一个实例状态变成fulfilled
,最终状态就会变成fulfilled
失败:只有全部实例状态变成rejected
,最终状态才会变成rejected
then()
指定下一步流程,使用catch()
捕获错误常见的错误
Uncaught TypeError: undefined is not a promise
如果在控制台中收到 Uncaught TypeError: undefined is not a promise
错误,则请确保使用 new Promise()
而不是 Promise()
UnhandledPromiseRejectionWarning
这意味着调用的 promise 被拒绝,但是没有用于处理错误的 catch。 在 then 之后添加 catch 则可以正确地处理
扩展:手写 Promise
原始数据类型(基本类型):按值访问,可以操作保存在变量中实际的值。
null
):用于未知的值 —— 只有一个 null
值的独立类型。undefined
): 用于未定义的值 —— 只有一个 undefined
值的独立类型。boolean
):用于 true
和 false
。number
):用于任何类型的数字:整数或浮点数,在 ±(253-1)
范围内的整数。string
):用于字符串:一个字符串可以包含 0 个或多个字符,所以没有单独的单字符类型。symbol
):用于唯一的标识符。引用类型(复杂数据类型):引用类型的值是保存在内存中的对象。
⚠️ 注意: 与其他语言不同的是,JavaScript 不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。所以引用类型的值是按引用访问的。
typeof、instanceof、constructor、Object.prototype.toString.call()
console.log(
typeof 100, //"number"
typeof undefined, //"undefined"
typeof null, //"object"
typeof function () {
console.log("aaa");
}, //"function"
typeof new Number(100), //'object'
typeof new String("abc"), // 'string'
typeof new Boolean(true) //'boolean'
);
typeof 可以正常检测出:number、boolean、string、object、function、undefined、symbol、bigint
- 检测基本数据类型,null 会检测 object,因为 null 是一个空的引用对象
- 检测复杂数据类型,除 function 外,均为 object
instanceof
运算符需要指定一个构造函数,或者说指定一个特定的类型,用来判断这个构造函数的原型是否在给定对象的原型链上
基本数据类型中:Number,String,Boolean。字面量值不可以用 instanceof 检测,但是构造函数创建的值可以
注意:null 和 undefined 都返回了 false,这是因为它们的类型就是自己本身,并不是 Object 创建出来它们,所以返回了 false。
console.log(
100 instanceof Number, //false
undefined instanceof Object, //false
[1, 2, 3] instanceof Array, //true
new Error() instanceof Error //true
);
constructor 是 prototype 对象上的属性,指向构造函数。根据实例对象寻找属性的顺序,若实例对象上没有实例属性或方法时,就去原型链上寻找,因此,实例对象也是能使用 constructor 属性的。可以检测出字面量方式创建的对象类型
如果输出一个类型的实例的 constructor,就如下所示:
console.log(new Number(123).constructor);
//ƒ Number() { [native code] }
可以看到它指向了 Number 的构造函数,因此,可以使用num.constructor === Number
来判断一个变量是不是 Number 类型的
除了 undefined 和 null 之外,其他类型都可以通过 constructor 属性来判断类型。
var num = 123;
var str = "abcdef";
var bool = true;
var arr = [1, 2, 3, 4];
// undefined和null没有constructor属性
console.log(
num.constructor === Number,
str.constructor === String,
bool.constructor === Boolean,
arr.constructor === Array
);
//所有结果均为true
const toString = Object.prototype.toString;
toString.call(123); //"[object Number]"
toString.call(undefined); //"[object Undefined]"
toString.call(null); //"[object Null]"
toString.call(/^[a-zA-Z]{5,20}$/); //"[object RegExp]"
toString.call(new Error()); //"[object Error]"
可以使用Object.prototype.toString.call(obj).slice(8,-1)
来判断并截取
使用Object.prototype.toString.call()
的方式来判断一个变量的类型是最准确的方法
function getType(obj) {
const type = typeof obj;
if (type !== "object") {
return type;
}
//如果不是object类型的数据,直接用typeof就能判断出来
//如果是object类型数据,准确判断类型必须使用Object.prototype.toString.call(obj)的方式才能判断
return Object.prototype.toString.call(obj).replace(/^\[object (\S+)]$/, "$1");
}
isArray 可以检测出是否为数组
const arr = [];
Array.isArray(arr); // true
下面前三种是对原数组产生影响的增添方法,第四种则不会对原数组产生影响
push( ) unshift( ) splice( ) concat( )
push()
push()
方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度
unshift()
unshift()在数组开头添加任意多个值,然后返回新的数组长度
splice
传入三个参数,分别是开始位置、0(要删除的元素数量)、插入的元素,返回的是空数组
concat()
合并
首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组
let colors = ["red"].concat("yellow", ["black"]);// ["red", "yellow", "black"]
下面三种都会影响原数组,最后一项不影响原数组:
pop() shift() splice() slice()
pop()
pop()
方法用于删除数组的最后一项,同时减少数组的length
值,返回被删除的项
shift()
shift()
方法用于删除数组的第一项,同时减少数组的length
值,返回被删除的项
splice()
传入两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数组
slice()
slice(开始索引, 结束索引) ,返回一个新数组,不会影响原始数组
即修改原来数组的内容,常用splice
splice()
传入三个参数,分别是(开始位置,要删除元素的数量,要插入的任意多个元素),返回删除元素的数组
会改变原数组
查找元素,返回元素坐标或者元素值
indexOf( ) includes( ) find( )
indexOf()
返回要查找的元素在数组中的位置,如果没找到则返回 -1
includes()
返回要查找的元素在数组中的位置,找到返回true
,否则false
find()
返回第一个匹配的元素
reverse()sort()
翻转
排序
function sortArr(a, b) {
return a - b; // 升序
return b - a; // 降序
}
join() 方法接收一个参数,即字符串分隔符,返回包含所有项的字符串,转为字符串
常用来迭代数组的方法(除 forEach 外其他都不会对空数组进⾏检测、不会改变原始数组)有如下:
some() every() forEach() filter() map()
对数组每一项都运行传入的测试函数,如果至少有 1 个元素返回 true ,则这个方法返回 true
对数组每一项都运行传入的测试函数,如果所有元素都返回 true ,则这个方法返回 true
对数组每一项都运行传入的函数,没有返回值
对数组每一项都运行传入的函数,函数返回 true
的项会组成数组之后返回
映射 对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组
基本类型数据保存在在栈内存中
引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中
1.浅拷贝: 将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用。
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址,即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址
2.深拷贝: 创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”
数组浅拷贝:
// 直接遍历
function shallowCopy(arr) {
const newArr = [];
arr.forEach((item) => newArr.push(item));
return newArr;
}
// slice
const arr2 = [1, 2, 3, 4, 5];
const newArr2 = arr2.slice();
// concat() 合并空数组实现
const arr3 = [11, 22, 33, 44, 55];
const newArr3 = arr3.concat([]);
对象浅拷贝:
// 直接遍历
function shallowCopy(obj) {
const newObj = {};
for (let item in obj) {
newObj[item] = obj[item];
}
return newObj;
}
// 使用拓展运算符
const obj2 = { name: "Bob", age: 17 };
const newObj2 = { ...obj2 };
用深拷贝最后要递归到全部是基本值,不然可能会陷入死循环/循环引用,导致栈溢出
TODO(待理解)⭐: 处理过的数据使用 map 结构缓存起来 >> 递归的时候碰到相同的数据 >> 直接使用缓存里面的
1. 先转换成字符串,在转换成(数组/对象) JSON.parse(JSON.stringify(XXXX))
有一个缺点 里面的函数不能拷贝
const array = [{ number: 1 }, { number: 2 }, { number: 3 }];
const str = JSON.stringify(array);
const copyArray = JSON.parse(str);
递归实现简单的深拷贝:
function deepClone(obj = {}) {
if (typeof obj !== "object" || obj == null) {
// obj 是 null ,或者不是对象和数组,直接返回
return obj;
}
// 初始化返回结果
let result;
obj instanceof Array ? (result = []) : (result = {});
for (let key in obj) {
// 保证 key 不是原型的属性
if (obj.hasOwnProperty(key)) {
// 递归调用!!!
result[key] = deepClone(obj[key]);
}
}
// 返回结果
return result;
}
小结
前提为拷贝类型为引用类型的情况下:
什么是闭包
通俗地讲闭包就是在一个函数里边再定义一个函数,这个内部函数一直保持有对外部函数中作用域的访问权限(小房间一直可以有大房子的访问权限)
闭包的作用
- 访问其他函数内部变量
- 保护变量不被 JS 的垃圾回收机制回收
- 避免全局变量被污染 方便调用上下文的局部变量 加强封装性
闭包的优点
(一)变量长期驻扎在内存中
(二)另一个就是可以重复使用变量,并且不会造成变量污染
① 全局变量可以重复使用,但是容易造成变量污染。不同的地方定义了相同的全局变量,这样就会产生混乱。
② 局部变量仅在局部作用域内有效,不可以重复使用,不会造成变量污染。
③ 闭包结合了全局变量和局部变量的优点。可以重复使用变量,并且不会造成变量污染
闭包的缺点
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在 IE 中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
https://segmentfault.com/a/1190000016418021
const arr = [1, 2, 3, 3, 3, 2, 3, 4, 5, 4, 4];
const newArr = [...new Set(arr)]; // [ 1, 2, 3, 4, 5 ]
function unique(arr) {
let newArr = [];
for (let i = 0; i < arr.length; i++) {
if (newArr.indexOf(arr[i]) === -1) newArr.push(arr[i]);
}
return newArr;
}
function unique(arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[i] === arr[j]) {
arr.splice(j, 1);
j--;
}
}
}
return arr;
}
function unique(arr) {
const newArr = [];
for (let i = 0; i < arr.length; i++) {
if (!newArr.includes(arr[i])) {
newArr.push(arr[i]);
}
}
return newArr;
}
function unique(arr) {
return arr.filter(function (item, index, arr) {
//当前元素,在原始数组中的第一个索引===当前索引值,否则返回当前元素
return arr.indexOf(item) === index;
});
}
function unique(arr) {
// todo:待总结数组去重方法
// 方法一
return arr.filter((item, index) => {
return arr.findIndex((child) => child.id === item.id) === index;
});
}
function unique(arr) {
const res = new Map();
return arr.filter((item) => !res.has(item.id) && res.set(item.id, 1));
}
function unique(arr) {
let obj = {};
return arr.reduce((pre, item) => {
obj[item.id] ? "" : (obj[item.id] = true && pre.push(item));
return pre;
}, []);
}
import { isEqual, uniqWith, uniqBy } from "lodash";
let arr = [
{ id: 1, name: "sli", year: 2012 },
{ id: 2, name: "ap", year: 2015 },
{ id: 1, name: "alslion", year: 2012 },
{ id: 3, name: "pose", year: 2012 },
{ id: 3, name: "pose", year: 2012 },
];
// 根据id去掉相同的元素:
console.log(uniqBy(arr, "id"));
// 深检查数组每一项进行去重:
console.log(uniqWith(arr, isEqual));
||运算符: 条件 1 || 条件 2
console.log(0 || ""); // ''
console.log("" || 0); // 0
&&运算符: 条件 1 && 条件 2
console.log(0 && ""); // 0
console.log("" && 0); // ''
分析一下 new 的整个过程:
简单实现一下 new:
function myNew(fn, ...args) {
// 第一步:创建一个空对象
const obj = {};
// 第二步:this指向obj,并调用构造函数
fn.apply(obj, args);
// 第三步:继承构造函数的原型
obj.__proto__ = fn.prototype;
// 第四步:返回对象
return obj;
}
是什么?
首先,
JavaScript
是一门单线程的语言,意味着同一时间内只能做一件事,如果前面一个任务耗时太长,后续的任务不得不等待,可能会导致程序假死的问题,但是这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环
在JavaScript
中,所有的任务都可以分为
ajax
网络请求,setTimeout
定时函数等同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。过程不断重复就是事件循环
https://www.mianshiya.com/qd/bf4a0bf261c7e2500090d9482499675f
下面代码执行顺序是什么
console.log(1) // 同步
setTimeout(()=>{ // 宏任务
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise') // 同步
resolve()
}).then(()=>{
console.log('then') // 微任务
})
console.log(3) // 同步
1
=>'new Promise'
=> 3
=> 'then'
=> 2
Javascript 把异步任务又做了进一步的划分,异步任务又分为两类,分别是:
微任务:
一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前
常见的微任务有:
宏任务:
宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合
常见的宏任务有:
promise里面的代码是同步任务 promise的方法.then()等是异步任务 微任务
每一个宏任务执行完之后,都会检查是否存在待执行的微任务,如果有,则执行完所有微任务之后,再继续执行下一个宏任务。
1、微任务比宏任务的执行时间要早
2、微任务在DOM渲染之前执行,宏任务在DOM渲染之后执行
async
是异步的意思,await
是等待。所以可以理解async
就是用来声明一个异步方法,而await
是用来等待异步方法执行
async
函数返回一个promise
对象
正常情况下,await
命令后面是一个 Promise
对象,返回该对象的结果。如果不是 Promise
对象,就直接返回对应的值
不管await
后面跟着的是什么,await
都会阻塞后面的代码(加入微任务列表)
下面代码执行顺序是什么:
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('settimeout')
})
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
分析过程:
console.log('script start')
直接打印结果,输出 script start
async1()
,执行 async1
函数,先打印 async1 start
,下面遇到await
怎么办?先执行 async2
,打印 async2
,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码new Promise
这里,直接执行,打印 promise1
,下面遇到 .then()
,它是微任务,放到微任务列表等待执行script end
,现在同步代码执行完了,开始执行微任务,即 await
下面的代码,打印 async1 end
then
的回调,打印 promise2
settimeout
所以最后的结果是:script start
、async1 start
、async2
、promise1
、script end
、async1 end
、promise2
、settimeout
this
指向立即执行
,bind 不会,而是返回一个函数多个参数
,apply
只能接受两个,第二个是数组
继承的是 属性 和 原型方法
ES6:https://es6.ruanyifeng.com/#docs/class-extends
Class 可以通过
extends
关键字实现继承,让子类继承父类的属性和方法子类如果写
constructor()
就必须要写super()
,且要写在最前面,否则报错,只有super()
方法才能让子类实例继承父类。
class Parent {
constructor(x,y) {
this.x = x
this.y = y
...
}
toString() {
...
}
}
class Son extends Parent {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
js 中有很多中继承的方式,不过每一种继承方式都有优缺点,重点掌握 ES5 继承,别的继承方式基本都是 ES5 继承的语法糖
先创造子类实例,通过
Parent.call(this, arg1, arg2...)
将父类的属性方法添加到this
上,继承了父类的属性再通过
Son.prototype = Object.create( Father.prototype )
将父类的原型继承过来最后可以通过
Son.prototype.constructor = Son
将子类的原型指到子类身上
function Father(name) {
this.name = name;
}
Father.prototype.get = function () {
return "黑马";
};
// 继承的是 属性 和 原型方法
function Son(name) {
Father.call(this, name);
}
// Son.prototype = new Father() // 相互影响 会存在一个 {name:undefined}
// Object.create 创造出一个空对象
// 让当前对象的__proto__ 指向传入的对象
Son.prototype = Object.create(Father.prototype);
Son.prototype.constructor = Son;
const son = new Son("程序员");
console.log(son);
console.log(son.get() + son.name); // 黑马程序员
什么是原型原型链?
每个函数都有一个 prototype 原型(原型就是对象),原型对象有一个 constructor 属性,指向的是构造函数
访问对象的某一个属性或者方法时,会从对象自身查找,如果查找不到,就会去原型链上去找,原型的最终目的就是让所有的实例能够共享其属性和方法
查找顺序:
自身 >
__proto__
> 构造函数的原型对象 >__proto__
> Object 的原型对象 >__proto__
> null
https://juejin.cn/post/6844903618999500808
includes()
方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回 false
语法
arr.includes(valueToFind[, fromIndex])
valueToFind
需要查找的元素值。Note: 使用 includes()
比较字符串和字符时是区分大小写。
fromIndex
可选
从fromIndex
索引处开始查找 valueToFind
。如果为负值,则按升序从 array.length + fromIndex
的索引开始搜 (即使从末尾开始往前跳 fromIndex
的绝对值个索引,然后往后搜寻)。默认为 0。
[1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false
[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true
[1, 2, NaN].includes(NaN); // true
find()
方法返回数组中满足提供的测试函数的第一个元素的值。否则返回undefined
arr.find(callback[, thisArg])
callback
在数组每一项上执行的函数,接收 3 个参数:element
当前遍历到的元素。index
可选当前遍历到的索引。array
可选数组本身。
thisArg
可选
执行回调时用作 this
的对象。
git rm 删除工作区文件,并将这次删除放入暂存区
git add 增加指定文件到暂存区
git init 新建初始化 git 代码库
git status 显示有变更的文件
git branch 列出所有分支
git commit -m [message] 提交暂存区到仓库区,可选填备注信息 message
git checkout -b [branch] 新建分支,并切换到该分支
/* html代码: */
/* CSS代码: */
.wrap{
/* 设置为弹性布局 */
display: flex;
/* 子元素在主轴对齐方式为居中 */
justify-content: center;
/* 交叉轴在y轴对齐 */
align-items: center;
}
/* html代码: */
/* CSS代码: */
.wrap{
width: 600px;
height: 600px;
border: 2px solid black;
/* 设置为相对定位,用来作为绝对定位元素的容器块。 */
position: relative;
}
.box{
width:50px;
height: 50px;
/* 设置为绝对定位,位置在父容器的中心 */
position: absolute;
margin: auto;
left: 50%;
top:50%;
/* 向回移动自身一半的长宽 */
transform: translateX(-50%) translateY(-50%);
}
<style>
.father {
margin: 100px auto;
width: 500px;
height: 300px;
border: 1px solid #0a3b98;
position: relative;
}
.son {
width: 100px;
height: 40px;
background: #f0a238;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}
</style>
<div class="father">
<div class="son"></div>
</div>
/* html代码: */
/* CSS代码: */
.wrap{
width: 600px;
border: 2px solid black;
/* 设置行高为600px */
line-height: 600px;
/* 让子盒子水平居中 */
text-align: center;
}
.box{
height: 50px;
width: 50px;
/* 设置为块级元素 */
display: inline-block;
/* 设置为垂直居中 */
vertical-align: middle;
background-color: red;
}
<style>
.father {
display: table-cell;
width: 200px;
height: 200px;
background: skyblue;
vertical-align: middle;
text-align: center;
}
.son {
display: inline-block;
width: 100px;
height: 100px;
background: red;
}
</style>
<div class="father">
<div class="son"></div>
</div>
根据元素标签的性质,可以分为:
水平居中
垂直居中
水平居中
垂直居中
概念:
Block Formatting Context,翻译过来就是块级格式化上下文
bfc 实际是一种属性,拥有这种属性后,就会让该渲染区域独立,并且该渲染区域中的内容布局不会影响到外界
如何触发:
根元素(html)
float 属性不为 none
position 为 absolute 或 fixed
display 为 inline-block, table-cell, table-caption, flex, inline-flex
overflow 不为 visible
解决什么问题
- 外边距重叠
- 外边距重叠,要注意这不是 bug,规范就是这样的,当两个盒子上下同时拥有上下间距,会取最大值
- 清除浮动
- 当子盒子开启 float 后会影响后面的布局以及盒子高度
- 浮动覆盖
- 由于浮动导致盒子被覆盖
盒模型主要分为 4 部分:内容、外边距、内边距、边框
Css3 盒子模型可以通过 box-sizing 来改变
标准盒模型(W3C 标准):content-box
盒子实际宽度加上 padding 和 border
ie 盒模型/怪异盒模型/c3 盒模型:box-sizing: border-box;
设置 width 后,实际盒子的宽度就固定为该宽度,包含了 内容 + padding + border
flex:1 → {
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0%;
}
https://juejin.cn/post/6844904153043435533#heading-1
此类错误状态代码指向客户端
500:内部服务器错误(Internal Server Error)— 通用未处理的服务器错误
503:服务不可用(Service Unavailable)— 服务器暂时无法处理请求
1.浏览器缓存机制
2.get 和 post 的区别
3.$ nextTick(),原理
4.防抖节流
5.组件通信方式
6.生命周期
7.promise
8.promise.all 方法什么时候走成功什么时候走失败;
9.v-if、v-for
10、对象和数组的响应式区别;
11、keep-alive;
12、组件通信的方式;
13.vue 数据响应式如何实现
14.vue for 循环的时候 key 的作用;
15、call、apply、bind 区别
15、vue.use 做了什么东西
16、插槽 什么场景下使用插槽;
17、平时工作中封装过哪些组件,怎么封装这些组件;
18、响应拦截器里面都做什么事情;响应拦截器里面的回调什么时候调用;
19、 r o u t e 和 route 和 route和router
20、单页和多页面的缺点;
21、keepalive 的作用原理;
22、http 和 https 的区别;
23、状态码有哪些;
24、如何实现实时更新,比如股票数据;websocket
25、如何判断一个数据是数组还是对象
26、路由懒加载原理
27、data 为什么是对象而不是函数;
28、.catch 能捕获到 return 一个 reject 吗
29、如何做移动端的 rem 适配,除了 flexible,还要装 pxtorem;
30、如何解决跨域问题,生产环境如何解决跨域问题?什么是反向代理;跨是浏览器的机制;
31、回流和重绘
32、图片懒加载的原理
33、项目优化的方式各个方式:打包,发请求;
34、观察者模式和发布订阅模式区别
的宽度,父盒子的剩余空间可以利用 flex-grow 来设置子盒子增大的占比
flex-shrink: 1
flex-basis: 0%
https://juejin.cn/post/6844904153043435533#heading-1
[外链图片转存中…(img-J96pnFCj-1705469393226)]
此类错误状态代码指向客户端
500:内部服务器错误(Internal Server Error)— 通用未处理的服务器错误
503:服务不可用(Service Unavailable)— 服务器暂时无法处理请求
1.浏览器缓存机制
2.get 和 post 的区别
3.$ nextTick(),原理
4.防抖节流
5.组件通信方式
6.生命周期
7.promise
8.promise.all 方法什么时候走成功什么时候走失败;
9.v-if、v-for
10、对象和数组的响应式区别;
11、keep-alive;
12、组件通信的方式;
13.vue 数据响应式如何实现
14.vue for 循环的时候 key 的作用;
15、call、apply、bind 区别
15、vue.use 做了什么东西
16、插槽 什么场景下使用插槽;
17、平时工作中封装过哪些组件,怎么封装这些组件;
18、响应拦截器里面都做什么事情;响应拦截器里面的回调什么时候调用;
19、 r o u t e 和 route 和 route和router
20、单页和多页面的缺点;
21、keepalive 的作用原理;
22、http 和 https 的区别;
23、状态码有哪些;
24、如何实现实时更新,比如股票数据;websocket
25、如何判断一个数据是数组还是对象
26、路由懒加载原理
27、data 为什么是对象而不是函数;
28、.catch 能捕获到 return 一个 reject 吗
29、如何做移动端的 rem 适配,除了 flexible,还要装 pxtorem;
30、如何解决跨域问题,生产环境如何解决跨域问题?什么是反向代理;跨是浏览器的机制;
31、回流和重绘
32、图片懒加载的原理
33、项目优化的方式各个方式:打包,发请求;
34、观察者模式和发布订阅模式区别