JavaScript题
1.JavaScript中的数据类型?
在JavaScript中,分两种类型:
- 基本类型
- 引用类型
基本类型主要有以下6种:Number、String、Boolean、Undefined、null、symbol。
引用类型主要有Object、Array、Function。其它的有Date、RegExp、Map等。
2.DOM
文档对象模型(DOM)是HTML和XML文档的编程接口。
日常开发离不开DOM的操作,对节点的增删改查等操作。在以前,使用Jquery,zepto等库来操作DOM,之后在vue,Angular,React等框架出现后,通过操作数据来控制DOM(多数情况下),越来越少的直接去操作DOM。
3.BOM
3.1 BOM是什么?
BOM(Browser Object Model),浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象。其作用就是跟浏览器做一些交互效果,比如:进行页面的后退、前进、刷新、浏览器窗口发生变化,滚动条滚动等。
3.2 window
Bom的核心对象是window,它表示浏览器的一个实例。
在浏览器中,window即是浏览器窗口的一个接口,又是全局对象。
4 == 和 === 区别,分别在什么情况使用
等于操作符用俩个等于号(==)表示,如果操作数相等,则会返回true。
等于操作符(==)在比较中会先进行类型转换,再确定操作数是否相等。
全等操作符由3个等于号(===)表示,只有俩个操作数在不转换的前提下相等才返回true,即类型相同,值也相同。
区别:等于操作符(==)会做类型转换再进行值的比较,全等操作符不会做类型转换。
null 和undefined 比较,相等操作符为true 全等为false。
5 typeof 和 instanceof 的区别
typeof 操作符返回一个字符串,表示未经计算的操作数的类型。
instanceof 运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。
区别:
typeof会返回一个变量的基本类型,instanceof返回的是一个Boolean.instanceof可以准确的判断复杂引用数据类型,但是不能正确判断基础数据类型。- 如果需要通用检测数据类型,可以通过
Object.prototype.toString,调用该方法,统一返回格式[object XXX]的字符串。
6 JavaScript 原型,原型链?有什么特点?
原型
JavaScript常被描述为一种基于原型的语言---每个对象拥有一个原型对象。访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或达到原型链的末尾。
原型链
原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链,它解释了为何一个对象会拥有定义在其他对象中的属性和方法。
在对象实例和它的构造器之间建立一个链接(它是_proto_属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。
- 一切对象都是继承自
Object对象,object对象直接继承根源对象null - 一切的函数对象(包括
object对象),都是继承自Function对象 Object对象直接继承自Function对象Function对象的_proto_会指向自己的原型对象,最终还是继承自Object对象
7.对作用域链的理解
作用域,即变量和函数生效的区域或集合,作用域决定了代码区块中变量和其他资源的可见性。
- 全局作用域:任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在任意位置访问。
- 函数作用域:函数作用域也叫局部作用域,如果一个变量是在函数内部声明的,它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问。
- 块级作用域:ES6引入了
let和const关键字,和var关键字不同,在大括号中使用let和const声明的变量存在于块级作用域中。在大括号外面不能访问这些变量。
作用域链
当在JavaScript中使用一个变量的时候,首先JavaScript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。
8. 谈谈对this对象的理解
8.1定义
函数的this关键字在JavaScript中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别。在绝大数情况下,函数的调用方式决定了this的值。
this关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象。
8.2 new绑定
通过构造函数new 关键字生成一个实例对象,此时this指向这个实例对象。
apply()、call()、bind()、是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时this指的就是这个第一个参数。
8.3 箭头函数
在ES6的语法中,提供了箭头函数法,让我们在代码书写时就能确定this的指向。
9.new操作符具体干了什么
- 创建一个新的对象
- 将对象与构建函数通过原型链链接起来
- 将构建函数中的
this绑定到新建的对象上 - 根据构建函数返回类型做判断,如果原始值则被忽略,如果是返回对象,需要正常处理。
10.bind、call、apply区别?
bind、call、apply、作用是改变函数执行时的上下文,改变函数运行时的this指向。
区别:
- 三者都可以改变函数的
this指向 - 三者第一个参数都是this要指向的对象,如果没有这个参数或者参数为
undefined或null,则默认指向全局window - 三者都可以传参,但是
apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分多次传入 bind是返回绑定this之后的函数,apply、call则是立即执行
11.闭包的理解?闭包使用场景?
11.1 闭包是什么?
一个函数和对其周围状态的引用捆绑在一起,这样的组合就是闭包。闭包让你可以在一个内层函数中访问到其外层函数的作用域。
11.2 闭包使用场景
- 创建私有变量
- 延长变量的生命周期
典型的闭包问题:
for(var i = 1; i <= 5; i ++){
setTimeout(() => {
console.log(i)
}, 0)
} // 五个 6
1、为什么会全部输出 6 ?
因为 setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行,当输出 i 时,同步任务中的 i 已经执行完所有的 ++,同步循环任务已经结束,i 变成了6,所以全部输出 6 。
2、怎么让这个循环依次输出 1 2 3 4 5 6 ?
3、使用ES6中的 let 关键字生成块级作用域
for (let i = 1; i <= 5; i++) {
setTimeout(() => {
console.log(i)
}, 0)
}
11.3 柯里化函数
柯里化的目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用。
11.4 闭包的缺点
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能有负面影响。
12.深拷贝浅拷贝的区别?实现一个深拷贝?
12.1 浅拷贝
Object.assign、Array.prototype.slice()、Array.prototype.concat()、拓展运算符实现复制。
var obj = {
name: 'xxx',
age: 17
}
var newObj = Object.assign({}, obj);
const a = [1,2,3];
const b = a.slice(0);
b[1] = 4;
console.log(a, b);// [1,2,3] [1,4,3]
const a = [1,2,3];
const b = [...a];
b[1] = 4;
console.log(a, b);// [1,2,3] [1,4,3]
12.2 深拷贝
常见深拷贝方式:
- _.cloneDeep()
- jQuery.extend()
- JSON.stringify()
- 手写循环递归
const _ = require('lodash');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f); // false
JSON.stringify()
// 有缺点 会忽略undefined、symbol、函数
const obj2=JSON.parse(JSON.stringify(obj1));
循环递归
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj; //null或者undefined就不拷贝
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话不拷贝
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
12.3 区别
浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享一块内存,修改对象属性会影响原对象。
深拷贝会另外创建一个一模一样的对象,不共享内存,不影响原对象。
13. JavaScript字符串的常用方法
let stringValue = "hello world";
console.log(stringValue.slice(3, 7)); // "lo w"
console.log(stringValue.substring(3,7)); // "lo w"
console.log(stringValue.substr(3, 7)); // "lo worl"
// 删除前、后或者前后所有空格符,再返回新的字符串
let stringValue = " hello world ";
let trimmedStringValue = stringValue.trim();
console.log(stringValue); // " hello world "
console.log(trimmedStringValue); // "hello world"
// 接收一个整数参数,将字符串复制多少次,返回拼接所有副本后的结果
let stringValue = "na ";
let copyResult = stringValue.repeat(2) // na na
- toUpperCase()、toLowerCase() 大小写转化
- indexOf() 从字符串开头去搜索传入的字符串,并返回位置(没找到返回-1)
- includes() 字符串是否包含传入的字符串
- split() 把字符串按照指定分隔符,拆分成数组
- replace() 接收俩个参数,第一个参数为匹配的内容,第二个参数为替换的元素
14.数组常用方法
增
- push() 添加到数组末尾
- unshift() 在数组开头添加
- splice() 传入3个参数,开始位置、0(要删除的元素数量)、插入的元素
- concat() 合并数组,返回一个新数组
删
- pop() 删除数组最后一项,返回被删除的项。
- shift() 删除数组的第一项,返回被删除的项。
- splice()传入两个参数,开始位置,删除元素的数量,返回包含删除元素的数组。
- slice() 用于创建一个包含原有数组中一个或多个元素的新数组,不会影响原始数组。
查
- indexOf() 返回要查找元素在数组中的位置,如果没找到则返回 -1.
- includes() 返回查找元素是否在数组中,有返回true,否则false.
- find() 返回第一个匹配的元素。
排序方法
- reverse() 将数组元素方向反转
- sort() 接受一个比较函数,用于判断那个值在前面
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
let values = [0, 1, 5, 10, 15];
values.sort(compare);
alert(values); // 0,1,5,10,15
转换方法
join() 方法接收一个参数,即字符串分隔符,返回包含所有项的字符串。
循环方法
some() 和 every() 方法一样
对数组每一项都运行传入的测试函数,如果至少有一个元素返回true,则这个方法返回true.
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let someResult = numbers.some((item, index, array) => item > 2);
console.log(someResult) // true
forEach()
对数组每一项都运行传入的函数,没有返回值
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.forEach((item, index, array) => {
//执行操作
});
filter()
函数返回true 的项会组成数组之后返回。
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let filterResult = numbers.filter((item, index, array) => item > 2);
console.log(filterResult); // 3,4,5,4,3
map()
返回由每次函数调用的结果构成的数组。
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let mapResult = numbers.map((item, index, array) => item * 2);
console.log(mapResult) // 2,4,6,8,10,8,6,4,2
15.事件循环的理解?
事件循环
JavaScript是一门单线程的语言,实现单线程非阻塞的方法就是事件循环。
在JavaScript中,所有的任务都可以分为:
- 同步任务:立即执行的任务,同步任务一般会直接进入到主线程执行。
- 异步任务:异步的比如
ajax网络请求,setTimeout定时函数等。
同步任务进入主线程,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程不断重复就是事件循环。
宏任务与微任务
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)
- 遇到 console.log(1),直接打印1
- 遇到定时器,属于新的宏任务,留着后面执行
- 遇到 new Promise,这个是直接执行的,打印'newPromise
- .then 属于微任务,放入微任务队列,后面再执行
- 遇到 console.log(3)直接打印 3
- 好了本轮宏任务执行完毕,现在去微任务列表查看是否有微任务,发现.then 的回调,执行它打印'then'
- 当一次宏任务执行完,再去执行新的宏任务,这里就剩一个定时器的宏任务了,执行它,打印 2
结果是:1=>'new Promise'=> 3 => 'then' => 2
异步任务执行顺序,事件队列其实是一个“先进先出”的数据结构,排在前面的事件会优先被主线程读取。
微任务
常见的微任务有:
- Promise.then
- MutaionObserver
- process.nextTice(node.js)
宏任务
宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合.
常见的宏任务有:
- script(可以理解为外层同步代码)
- setTimeout/setInterval
- Ul rendering/Ul事件
- postMessage、MessageChannel
- setlmmediate、1/0(Node.is) 这时候,事件循环,宏任务,微任务的关系如图所示
它的执行机制是:
- 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中
- 当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完
async 与 await
async就是用来声明一个异步方法,await是用来等待异步方法执行。
async函数返回一个promise对象,下面代码是等效的:
function f() {
return Promise.resolve('TEST');
}
async function asyncF() {
return 'TEST';
}
正常情况下, await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
async function f() {
// 等同于 return 123
return await 123
}
f().then(i => console.log(i)) // 123
不管 await 后面跟着的是什么,await 都会阻塞后面的代码。
async function fn1 (){
console.log(1)
await fn2()
console.log(2) // 阻塞
}
async function fn2 (){
console.log('fn2')
}
fn1()
console.log(3)
await 会阻塞下面的代码(即加入微任务队列),上面的例子中,先执行 async 外面的同步代码同步代码执行完,再回到 async函数中,再执行之前阻塞的代码
输出:1,fn2,3,2
async function async1() {
console.log('1')
await async2()
console.log('2')
}
async function async2() {
console.log('3')
}
console.log('4')
setTimeout(function () {
console.log('settimeout')
})
async1()
new Promise(function (resolve) {
console.log('5')
resolve()
}).then(function () {
console.log('6')
})
console.log('7');
// 输出结果: 4 1 3 5 7 2 6 settimeout
分析过程:
- 1.执行整段代码,遇到
console.log('4')直接打印结果,输出4; - 2.遇到定时器了,它是宏任务,先放着不执行;
- 3.遇到
async1(),执行async1函数,先打印1,下面遇到await怎么办?先执行async2,打印3,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码 - 4.跳到
new Promise这里,直接执行,打印5,下面遇到.then(),它是微任务,放到微任务列表等待执行; - 5.最后一行直接打印
7,现在同步代码执行完了,开始执行微任务,即await下面的代码,打印2; - 6.继续执行下一个微任务,即执行
then的回调,6; - 7.上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印
settimeout所以最后的结果是:4 1 3 5 7 2 6 settimeout。
16.JavaScript本地存储方式有哪些?区别及应用场景?
16.1 方式
javaScript 本地缓存的方法主要讲述以下四种:
- cookie
- sessionStorage
- localStorage
- indexedDB
16.1.1.cookie
Cookie ,类型为「小型文本文件」,指某些网站为了辨别用户身份而储存在用户本地终端上的数据。是为了解决 HTTP 无状态导致的问题。
作为一段一般不超过 4KB 的小型文本数据,它由一个名称(Name)、一个值(Value)和其它几个用于控制 cookie 有效期、安全性、使用范围的可选属性组成。
但是 cookie 在每次请求中都会被发送,如果不使用 HTTPS 并对其加密,其保存的信息很容易被窃取,导致安全风险。
16.1.2 localStorage
- 生命周期:持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。
- 存储的信息在同一域中是共享的。
- 当本页操作(新增、修改、删除)了 localStorage 的时候,本页面不会触发 storage 事件,但是别的页面会触发 storage 事件。
- 大小:5M(跟浏览器厂商有关系)。
localstorage本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡。- 受同源策略的限制。
localStorage.setItem('username','你的名字');
localStorage.getItem('username');
localStorage.key(0) // 获取第一个键名
localStorage.removeItem('username');
localStorage.clear(); // 清空localStorage
16.1.3 sessionStorage
sessionStorage 和 localstorage 使用方法基本一致,唯一不同的是生命周期,一旦页面(会话)关闭,sessionStorage 将会删除数据。
16.1.4 indexedDB
indexedDB 是一种低级AP,用于客户端存储大量结构化数据(包括,文件/blobs)。该API使用索引来
实现对该数据的高性能搜索。
虽然 Web Storage 对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用。
优点:
- 储存量理论上没有上限
- 所有操作都是异步的,相比LocalStorage 同步操作性能更高,尤其是数据量较大时
- 原生支持储存 JS 的对象
- 是个正经的数据库,意味着数据库能干的事它都能干
缺点:
- 操作非常繁琐
- 本身有一定门槛
区别
- 存储大小:
cookie数据大小不能超过4k,sessionStorage 和 localStorage虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大。 - 有效时间:
localStorage存储持久数据,浏览器关闭后数据不丢失除非主动删除数据;sessionStorage数据在当前浏览器窗口关闭后自动删除;cookie设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭。 - 数据与服务器之间的交互方式,
cookie的数据会自动的传递到服务器,服务器端也可以写cookie到客户端;sessionStorage 和 localStorage不会自动把数据发给服务器,仅在本地保存
17.Ajax 原理是什么?如何实现?
Ajax 的原理简单来说通过 XmlHttpRequest 对象来向服务器发异步请求,从服务器获得数据,然后用 JavaScript 来操作 DOM 而更新页面。
简单封装一个ajax请求:
function ajax(options) {
//创建XMLHttpRequest对象
const xhr = new XMLHttpRequest();
//初始化参数的内容
options = options || {};
options.type = (options.type || 'GET').toUpperCase();
options.dataType = options.dataType 'json';
const params = options.data;
// 发送请求
if (options.type === 'GET') {
xhr.open('GET', options.url + '?' + params, true) xhr.send(null)
} else if (options.type === 'POST') {
xhr.open('POST', options.url, true) xhr.send(params)
// 接收请求
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
let status = xhr.status;
if (status >= 200 && status < 300) {
options.success && options.success(xhr.responseText, xhr.responseXML)
} else {
options.fail && options.fail(status)
}
}
}
}
}
// 调用
ajax({
type: 'post',
dataType: 'json',
data: {},
url: 'https://xxxx',
success: function(valse, xml){
console.log(valse)
},
fail: function(status){
console.log(status)
}
})
18. 防抖和节流?区别?如何实现?
- 节流: n秒内只运行一次,若在n秒内重复触发,只有一次生效。
- 防抖: n 秒后在执行该事件,若在n秒内被重复触发,则重新计时
应用场景:
防抖在连续的事件,只需触发一次回调的场景有:
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求
- 手机号、邮箱验证输入检测
- 窗口大小 resize 。只需窗口调整完成后,计算窗口大小。防止重复渲染。
节流在间隔一段时间执行一次回调的场景有:
- 滚动加载,加载更多或滚到底部监听
- 搜索框,搜索联想功能
节流
function throttled(fn, delay) {
let timer = null;
let starttime = Date.now();
return function () {
let curTime = Date.now(); // 当前时间
let remaining = delay - (curTime - starttime); // 从上一次到现在,还剩下多少多余事件
let context = this; // 保存this指向
let args = arguments; // 拿到event对象
clearTimeout(timer);
if (remaining <= 0) {
fn.apply(context, args);
starttime = Date.now();
} else {
timer = setTimeout(fn, remaining);
}
}
}
防抖
function debounce(func, wait) {
let timeout;
return function () {
let context = this; // 保存this指向
let args = arguments; // 拿到event对象
clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(context, args)
}, wait);
}
}
如果需要立即执行防抖,可加入第三个参数
function debounce(func, wait, immediate) {
let timeout;
return function() {
let context = this; // 保存this指向
let args = arguments; // 拿到event对象
if (timeout) clearTimeout(timeout); // timeout 不为 null
if (immediate) {
let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会触发
timeout = setTimeout(function() {
timeout = null;
},
wait);
if (callNow) {
func.apply(context, args)
}
} else {
timeout = setTimeout(function() {
func.apply(context, args)
},
wait);
}
}
}
区别
相同点
- 都可以通过使用 setTimeout 实现
- 目的都是,降低回调执行频率。节省计算资源
不同点
- 函数防抖,在一段连续操作结束后,处理回调,利用clearTimeout和 setTimeout 实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能。
- 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次例如,都设置时间频率为500ms,在2秒时间内,频繁触发函数,节流,每隔500ms 就执行一次。防抖,则不管调动多少次方法,在2s后,只会执行一次。
19. web常见的攻击方式有哪些?如何防御?
常见的有:
- XSS 跨站脚本攻击
- CSRF 跨站请求伪造
- SQL 注入攻击
防止csrf常用方案如下:
- 阻止不明外域的访问,
同源检测,Samesite Coolkie。 - 提交时要求附加本域才能获取信息
CSRF Token, 双重Cookie验证。
预防SQL如下:
- 严格检查输入变量的类型和格式
- 过滤和转义特殊字符
- 对访问数据库的web应用程序采用web应用防火墙
20.JavaScript内存泄露的几种情况?
内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存。
Javascript 具有自动垃圾回收机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。
常见的内存泄露情况:
- 意外的全局变量。
a='我是未声明的变量'. - 定时器
21. JavaScript数字精度丢失的问题?如何解决?
0.1 + 0.2 === 0.3; // false
可以使用parseFloat解决
22.JavaScript 脚本延迟加载的方式有哪些?
延迟加载就是等页面加载完成之后再加载 JavaScript 文件。js 延迟加载有助于提高页面加载速度。
一般有以下几种方式:
- defer 属性:给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
- async 属性:给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
- 动态创建 DOM 方式:动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入js 脚本。使用 setTimeout 延迟方法:设置一个定时器来延迟加载 js 脚本文件让 JS 最后加载:将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。
CSS题型整理
1.盒模型
盒模型:由4个部分组成,content,padding,border,margin.
2.BFC的理解
BFC:即块级格式化上下文。
常见页面情况有:
- 元素高度没了
- 俩栏布局没法自适应
- 元素间距奇怪
2.1清除内部浮动
元素添加overflow: hidden;
3.元素水平垂直居中的方法有哪些?
实现方式如下:
- 利用定位+margin:auto
- 利用定位+margin: 负值
- 利用定位+transform
- flex布局等
4.实现两栏布局,右侧自适应?三栏布局中间自适应?
两栏布局的话:
- 使用float左浮动布局
- 右边模块使用margin-left 撑出内容块做内容展示
- 为父级元素添加BFC,防止下方元素跟上方内容重叠。
flex布局:
- 简单易用代码少
三栏布局:
- 两边用float,中间用margin
- 两边用absolute,中间用margin
- display: table
- flex
- grid网格布局
5.css中,有哪些方式隐藏页面元素?
例如:
display: none; 最常用,页面彻底消失,会导致浏览器重排和重绘visibility: hidden; dom存在,不会重排,但是会重绘opacity: 0; 元素透明 元素不可见,可以响应点击事件position: absolute; 将元素移出可视区域,不影响页面布局
6.如何实现单行/多行文本溢出的省略样式
单行:
<style>
p {
overflow: hidden;
line-height: 40px;
width:400px;
height:40px;
border:1px solid red;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
<p>文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本</p>
多行
<style>
.demo {
position: relative;
line-height: 20px;
height: 40px;
overflow: hidden;
}
.demo::after {
content: "...";
position: absolute;
bottom: 0;
right: 0;
padding: 0 20px 0 10px;
}
</style>
<body>
<div class="demo">文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本</div>
</body>
css实现
<style>
p {
width: 400px;
border-radius: 1px solid red;
-webkit-line-clamp: 2;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
</styl
7.CSS3新增了哪些新特性?
选择器:
nth-child(n)nth-last-child(n)last-child
新样式:
border-radius; 创建圆角边框box-shadow; 为元素添加阴影border-image; 图片绘制边框background-clip; 确定背景画区background-size; 调整背景图大小
文字:
word-wrap: normal|break-word; 使用浏览器默认的换行 | 允许在单词内换行;text-overflow; clip | ellipsis; 修剪文本 | 显示省略符号来代表被修剪的文本;text-decoration; text-fill-color| text-stroke-color | text-stroke-width;
transition 过渡、transform 转换、animatin动画、渐变、等
8.CSS提高性能的方法有哪些?
如下:
- 内联首屏关键
css - 异步加载
css - 资源压缩(
webpack/gulp/grunt)压缩代码 - 合理的使用选择器
- 不要使用
@import icon图片合成等
ES6
1.var,let, const的区别?
var声明的变量会提升为全局变量,多次生成,会覆盖。letlet声明的变量只在代码块内有效。const声明一个只读常量,常量的值不能改变。
区别:
- 变量提升,
var会提升变量到全局。let, const直接报错 - 暂时性死区
- 块级作用域
- 重复声明
- 修改声明的变量
2.ES6中数组新增了哪些扩展?
- 扩展运算符
... - 构造函数新增的方法
Array.from(),Array.of() - 数组实例新增方法有:copyWithin(),find(),findIndex(),fill(),includes(),keys(),values()等
1、map
-
接受两个参数,一个是回调函数,一个是回调函数的this值(可选):其中回调函数被默认传入三个值,依次为当前元素、当前索引、整个数组。
-
创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
-
对原来的数组没有影响。
-
后面的参数都是可选的 ,不用的话可以省略。
let nums = [1, 2, 3]; let obj = {val: 5}; let newNums = nums.map(function(item,index,array) { return item + index + array[index] + this.val; //对第一个元素,1 + 0 + 1 + 5 = 7 //对第二个元素,2 + 1 + 2 + 5 = 10 //对第三个元素,3 + 2 + 3 + 5 = 13 }, obj); console.log(newNums);//[7, 10, 13]
2、 reduce
-
参数: 接收两个参数,一个为回调函数,另一个为初始值。回调函数中三个默认参数,依次为积累值、当前值、整个数组。
-
不传默认值会自动以第一个元素为初始值,然后从第二个元素开始依次累计。
let nums = [1, 2, 3]; // 多个数的加和 let newNums = nums.reduce(function(preSum,curVal,array) { return preSum + curVal; }, 0); console.log(newNums);//6
3、filter
-
参数: 一个函数参数。这个函数接受一个默认参数,就是当前元素。这个作为参数的函数返回值为一个布尔类型,决定元素是否保留。
-
filter方法返回值为一个新的数组,这个数组里面包含参数里面所有被保留的项。
let nums = [1, 2, 3]; // 保留奇数项 let oddNums = nums.filter(item => item % 2); console.log(oddNums);
4、sort
- 参数: 一个用于比较的函数,它有两个默认参数,分别是代表比较的两个元素。
let nums = [2, 3, 1];
//两个比较的元素分别为a, b
nums.sort(function(a, b) {
if(a > b) return 1;
else if(a < b) return -1;
else if(a == b) return 0;
})
当比较函数返回值大于0,则 a 在 b 的后面,即a的下标应该比b大。
反之,则 a 在 b 的后面,即 a 的下标比 b 小。
整个过程就完成了一次升序的排列。
当然还有一个需要注意的情况,就是比较函数不传的时候,是如何进行排序的?
答案是将数字转换为字符串,然后根据字母
unicode值进行升序排序,也就是根据字符串的比较规则进行升序排序。
5、forEach
return 终止不了 ForEach 循环,可以用 try catch 终止
let num = [1, 2, 3]
try {
num.forEach((item) => {
if (item === 2) {
throw new Error('error1')
}
console.log(item) // 1
})
} catch (e) {
console.log(e) // Error: error1
}
6、every
方法用于检测数组所有元素是否都符合指定条件
every() 方法使用指定函数检测数组中的所有元素:
- 如果数组中检测到有一个元素不满足,则整个表达式返回 false ,且剩余的元素不会再进行检测。
- 如果所有元素都满足条件,则返回 true。
注意: every() 不会对空数组进行检测。
注意: every() 不会改变原始数组。
var arr = [11, 12, 13, 14, 5]
// 检测数组所有元素是否都符合指定条件(有一个不满足就返回 false)
var bool = arr.every(item => item > 10)
console.log(bool) // false
7、some
some() 方法用于检测数组中的元素是否满足指定条件
some() 方法会依次执行数组的每个元素:
- 如果有一个元素满足条件,则表达式返回true , 剩余的元素不会再执行检测。
- 如果没有满足条件的元素,则返回false。
注意: some() 不会对空数组进行检测。
注意: some() 不会改变原始数组。
var arr = [11, 12, 13, 14, 5]
// 检测数组中的元素是否满足指定条件(有一个满足就返回 true)
var bool = arr.some(item => item > 10)
console.log(bool) // true
3.对象新增了哪些扩展
对象名跟对应值名相等的时候,可以简写。 const a = {foo: foo} == const a = {foo}
属性的遍历:
- for...in:循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)
- Object.keys(obj):返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)的键名
- Object.getOwnPropertyNames(obj):回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)的键名
- Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有Symbol属性的键名----- Reflect.ownKeys(obj):返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是Symbol或字符串,也不管是否可枚举.
对象新增的方法
- Object.is();
- Object.assign();
- Object.getOwnPropertyDescriptors() ;
- Object.keys(), Object.values(),Object.entries();
- Object.fromEntries();
4.理解ES6中Promise的?
优点:
- 链式操作减低了编码难度
- 代码可读性增强
promise对象仅有三种状态,pending(进行中),fulfilled(已成功),rejected(已失败)。一旦状态改变(从 pending变为 fulfilled和从 pending变为 rejected),就不会再变,任何时候都可以得到这个结果。
使用方法
const promise = new Promise(function(resolve, reject) {});
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和 reject
resolve函数的作用是,将Promise对象的状态从"未完成"变为"成功"reject函数的作用是,将Promise对象的状态从"未完成"变为"失败"
实例方法:
then()是实例状态发生改变时的回调函数。catch()指定发生错误时的回调函数。finally()不管Prosime对象最后状态如何,都会执行。
构造函数方法 Promise构造函数存在以下方法:
- all() 将多个
Promise实例包装成一个新的Promise实例。 - race() 将多个
Promise实例包装成一个新的Promise实例。 - allSettled() 接受一组Promise实例作为参数,只有等所有这些参数返回结果,实例才会结束。
- resolve() 将现有对象转为Promise对象。
- reject() 返回一个新的Promise实例,状态为rejected。
Vue2面试题
1.生命周期?
beforeCreate->created->beforeMount->mounted->beforeUpdate->updated->beforeDestroy->destroyed。
1.4 数据请求在created和mouted的区别
created 是在组件实例一旦创建完成的时候立刻调用,这时候页面 dom 节点并未生成; mounted是在页面dom节点渲染完毕之后就立刻执行的。触发时机上created是比mounted要更早的,
两者的相同点:都能拿到实例对象的属性和方法。讨论这个问题本质就是触发的时机,放在mounted中的请求有可能导致页面闪动(因为此时页面dom结构已经生成),但如果在页面加载前完成请求,则不会出现此情况。建议对页面内容的改动放在 created 生命周期当中。
2.双向数据绑定是什么?
释义:当js代码更新Model时,view也会自动更新,用户更新view,Model的数据也会自动被更新,就是双向绑定。
3.Vue组件之间的通信方式有哪些?
- 1.通过props传递 (父给子组件传递)
- 2.通过$emit触发自定义事件 (子传父)
- 3.使用ref (父组件使用子组件的时候)
-
- EventBus (兄弟组件传值)
-
- attrs 与 listeners (祖先传递给子孙)
-
- Provide 与 Inject (在祖先组件定义provide)返回传递的值,在后代组件通过inject 接收组件传递过来的值。
-
- Vuex (复杂关系组件数据传递,存放共享变量)
4.v-if和v-for的优先级是什么?
v-for的优先级比v-if的高
注意
不能把v-if 和 v-for 同时在同一个元素上,带来性能方面的浪费。必须使用的话可以在外层套一个template
5、单组件的data为什么是一个对象?
data 必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果 data 仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象!简言之,组件复用下,不会造成数据同时指向一处,造出牵一发而动全身的问题。
6、跨域问题怎么破
1: CORS , 前后端都要对应去配置,IE10+
2: nginx 反向代理,一劳永逸 <-- 线上环境可以用这个
3: 依旧坚挺的jsonp大法!
线下开发模式,比如你用了vue-cli, 里面的 webpack 有引入了proxyTable这么个玩意, 也可以做接口反向代理。
// 在 config 目录下的index.js
proxyTable: {
"/bp-api": {
target: "http://new.d.st.cn",
changeOrigin: true,
// pathRewrite: {
// "^/bp-api": "/"
// }
}
}
// target : 就是 api 的代理的实际路径
// changeOrigin: 就是是变源,必须是...
// pathRewrite : 就是路径重定向,一看就知道
7、路由模式改为history后,首次启动首页没报错,刷新访问路由都报错!
必须给对应的服务端配置查询的主页面..也可以认为是主路由入口的引导。
8、组件的通讯有哪几种啊!
父传子:
props子传父:
emit兄弟通讯:
event bus: 就是找一个中间组件来作为信息传递中介vuex: 信息树
9、组件可以缓存么?
可以,用keep-alive;
不过是有代码的..占有内存会多了...所以无脑的缓存所有组件!!!别说性能好了..切换几次,有些硬件 hold不住的,浏览器直接崩溃或者卡死..
所以keep-alive一般缓存都是一些列表页,不会有太多的操作,更多的只是结果集的更换..给路由的组件meta增加一个标志位,结合v-if就可以按需加上缓存了!
10、computed 和 watch 的区别和运用的场景?
computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
-
当我们需要进行数值计算,并且依赖于其它数据时,应该使用
computed,因为可以利用computed的缓存特性,避免每次获取值时,都要重新计算; -
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用
watch选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
11、在哪个生命周期内调用异步请求?
可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面
loading时间; ssr不支持beforeMount、mounted钩子函数,所以放在created中有助于一致性.
12、在什么阶段才能访问操作DOM?
在钩子函数 mounted 被调用前,Vue 已经将编译好的模板挂载到页面上,所以在 mounted 中可以访问操作 DOM。
13、父组件可以监听到子组件的生命周期吗?
可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:
// Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},
// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
},
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...
当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created,updated 等都可以监听。
14、谈谈你对 keep-alive 的了解?
keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:
- 一般结合路由和动态组件一起使用,用于缓存组件;
- 提供
include和exclude属性,两者都支持字符串或正则表达式,include表示只有名称匹配的组件会被缓存,exclude表示任何名称匹配的组件都不会被缓存 ,其中exclude的优先级比include高; - 对应两个钩子函数
activated和deactivated,当组件被激活时,触发钩子函数activated,当组件被移除时,触发钩子函数deactivated。
15、v-model 的原理?
数据变化会引起 DOM 的变化之外,还会在操作 DOM 改变后反过来影响数据的变化。
是 v-value 和 v-on:input 事件的语法糖
v-model 并不是随便使用,只作用于固定的表单标签:
inputselecttextarea自定义组件- 原理:通过
prop向自定义组件传递数据,监听自定义事件接收组件反传的数据并更新
16、Vue 组件间通信有哪几种方式?
1、props / $emit 适用 父子组件通信
父传子 子调用父组件方法
2、ref 与 children 适用 父子组件通信
ref:如果在普通的DOM元素上使用,引用指向的就是DOM元素;如果用在子组件上,引用就指向组件实例$parent / $children:访问父 / 子实例
3、Vuex 适用于 父子、隔代、兄弟组件通信
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
Vuex的状态存储是响应式的。当Vue 组件从 store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会相应地得到高效更新。- 改变
store中的状态的唯一途径就是显式地提交 (commit)mutation。这样使得我们可以方便地跟踪每一个状态的变化。
17、你使用过 Vuex 吗?
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
(1)Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
(2)改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
主要包括以下几个模块:
State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。
18、vue-router 路由模式有几种?
vue-router 有 3 种路由模式:hash、history、abstract,对应的源码如下所示:
hash: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器; history : 依赖 HTML5 History API 和服务器配置。具体可以查看 HTML5 History 模式; abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.
19、什么是 MVVM?
MVVM 框架实现了双向绑定,这样 ViewModel 的内容会实时展现在 View 层,前端开发者再也不必低效又麻烦地通过操纵 DOM 去更新视图
20、Vue 是如何实现数据双向绑定的?
Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示:
- 输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化。
- Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化。
17、虚拟 DOM 实现原理?
虚拟 DOM 的实现原理主要包括以下 3 部分:
1.用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象; 2.diff 算法 — 比较两棵虚拟 DOM 树的差异; 3.pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。
21、Vue 中的 key 有什么作用?
key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速
更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
19、你有对 Vue 项目进行哪些优化?
1、代码层面的优化
- v-if 和 v-show 区分使用场景
- computed 和 watch 区分使用场景
- v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
- 图片资源懒加载
- 路由懒加载
- 第三方插件的按需引入
2、Webpack 层面的优化
- Webpack 对图片进行压缩
- 减少 ES6 转为 ES5 的冗余代码
- 提取公共代码
3、基础的 Web 技术的优化
- 开启 gzip 压缩
- 浏览器缓存
- CDN 的使用
22、父子生命周期顺序
父beforeCreate => 父created => 父beforeMount =>
子beforeCreate => 子created =>子beforeMount => 子Mounted => 父Mounted
子组件先挂载 然后到父组件,更新也类似
父beforeUpdate =>子beforeUpdate => 子updated => 父updated
21、es6的知识点:
- 声明和表达式:
let、const、解构赋值、Symbol - 内置对象:
Map和Set proxy和reflect - 字符串模板
- 函数:
参数扩展、箭头函数、迭代器、for of - class类
export和import模块promiseasync await和generator
22、axios
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
axios有什么特性
- 从浏览器中创建
XMLHttpRequests - 从
node.js创建http请求 - 支持
Promise API - 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换 JSON 数据
- 客户端支持防御 XSRF
23、diff 算法
同层级对比新旧虚拟DOM,把变化的节点更新在真实的 DOM 上。
24、路由懒加载原理
- 第一次渲染是占位节点
- 然后发出下载请求
- 等import() api返回的组件代码拿到以后生成构造器
- 再$forceUpdate重新渲染
- 挂载完毕
Vue-Router 的生命周期
router.beforeEach注册一个全局前置守卫router.beforeResolve注册一个全局守卫afterEach全局后置钩子- 路由独享的守卫
beforeEnter - 组件内的守卫
beforeRouterEnter、beforeRouterUpdate、beforeRouterLeave
25、Vuex
Vuex 是一个专门为 Vue.js 框架设计的、专门用来对于 Vue.js 应用进行状态管理的库。
Vuex 内部采用了 new Vue 来将 Store 内的数据进行「响应式化」,所以 Vuex 是一款利用 Vue 内部机制的库。
每一个 Vuex 应用的核心是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
Vue.js 提供了一个 Vue.use 的方法来安装插件,内部会调用插件提供的 install 方法。
Vue.use(Vuex)
采用 Vue.mixin 方法将 vuexInit 方法混淆进 beforeCreate 钩子中,把 store 中的数据 state 挂载到全局 Vue 实例的 data 中,再利用 Object.defineProperty 对 data 数据的依赖收集和响应式,对 state 里的数据也做了同样的依赖收集,所以 store 里存储的数据也是响应式数据。
主要包括以下几个模块:
-
State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。 -
Getter:允许组件从Store中获取数据,mapGetters辅助函数仅仅是将store中的getter映射到局部计算属性。 -
Mutation:是唯一更改store中状态的方法,且必须是同步函数。 -
Action:用于提交mutation,而不是直接变更状态,可以包含任意异步操作。 -
Module:允许将单一的Store拆分为多个store且同时保存在单一的状态树中。 -
Getters最终在组件里映射的是一个computed计算属性。 -
Getters也可以当做计算属性使用。 -
组件也可以直接触发
mutation,修改state。
Action 两个作用:
- 1、处理异步操作
- 2、对 mutation 做进一步封装
Vue3
1.Proxy 可以实现什么功能?
在 Vue3.0 中通过 Proxy 来替换原本的 Object.defineProperty来实现数据响应式。
Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。
let p = new Proxy(target, handler);
代表需要添加代理的对象,handler用来自定义对象中的操作,比如可以用来自定义set或者get函数。下面来通过Proxy来实现一个数据响应式:
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(targer, property)
return Reflect.get(target, property, receiver)
},
set(target, property, value, receiver) {
setBind(valer, propert)
return Reflect.set(target, property,value)
}
}
return new Proxy(obj, hander)
}
let obj = {a: 1};
let p = onWatch(obj, (v, property) => {
console.log(`监听到属性${propery}改变为${v}`)
}, (target, property) => {
console.log(`'${property}' = ${target[property]}`)
})
p.a = 2; // 监听到属性a改变
p.a // 'a' = 2
在上述代码中,通过自定义set和get函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。
当然这是简单版的响应式实现,如果需要实现一个Vue中的响应式,需要在get中收集依赖,在set派发更新,之所以Vue3.0要使用Proxy替换原本的API原因在于Proxy无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是Proxy可以完美监听到任何方式的数据改变,唯一缺陷就是浏览器的兼容性不好。