赞
踩
ES5:
ES6:
其中Number类型是基于IEEE 754标准实现的64位浮点数,也就是说,JavaScript中的所有数字都是以浮点数的形式存在的
无自定义类型数据
相同点:
都是基本数据类型,都有唯一的值,分别为null和undefined,在相等运算符中都会转为false即两值相等
不同点:
undefined表示未定义:
定义了形参,没有传实参,返回undefined
如:function A(a){console.log(a)} A() 执行后输出undefined
变量声明了但没有定义时返回undefined
如:a=a+1; console.log(a) 返回undefined
对象属性名不存在时返回undefined
如:class A(){} console.log(A.name)返回undefined
函数没有写返回值,拿到的是undefined
如:function A(){} console.log(A()) 返回undefined
null表示空对象,也作为原型链的终点
null主要用于复制一些可能会返回对象的变量,作为初始化
当定义变量却没赋值时,返回null
如:let a; console.log(a)返回null
BigInt表示任意大的整数,整数溢出就不是问题。在整数末尾加上n,即表示时BigInt类型数据,不会发生溢出。
如:console.log(9007199254740095n);console.log(9007199254740095)
前者直接输出,后者发生溢出,数值+1
也可以使用BigInt()构造函数,如:
BigInt(“9007199254740995”); // → 9007199254740995n
基本数据类型 直接存储在栈内存中,占据空间小,大小固定,被频繁使用。指的是保存在栈内存中的简单数据段。
引用数据类型 类型将指针存在栈中,值存在堆中。变量中保存的时指针,指向内存堆中实际的值,数组或对象。
栈是连续储存的数据结构,具有先进后出,后进先出的性质。通常操作有入栈,出栈,栈顶元素。
堆是非连续的树形储存数据结构,具有队列优先,先进先出;每个节点有一个值,整棵树是经过排序的。特点是根结点的值最小或最大,且根节点的两个子树也是一个堆。常用来实现优先队列,存取随意。
所有基本类型中Boolean值为false的只有6个,分别是0 NaN ‘’ null undefined false ; 引用类型中Boolean值全是true
typeof 判断基本数据类型
console.log(typeof 2) //number
instanceof 判断引用数据类型,在原型链中找该类型的原型
console.log([] instanceof Array) //true
constructor 都可判断但是当判断一个原型指向Array的构造函数时无法进行判断
console.log((‘str’).constructor === String) //true
Object.prototype.toString.call() 完美判断
Object.prototype.toString.call(2)//number
在JavaScript中类型转换有三种情况:
转换为数字(调用Number(),parseInt(),parseFloat()方法)
转换为字符串(调用.toString()或String()方法)
转换为布尔值(调用Boolean()方法) 还有隐式转换 注意:null、undefined没有.toString方法
Number是一个全局函数,可以将任何数据类型转换为数字类型
let a = Number('123'); // 返回123,类型为数字类型
let b = Number(true); // 返回1,类型为数字类型
let c = Number(null); // 返回0,类型为数字类型
let d = Number(undefined); // 返回NaN,类型为数字类型
需要注意的是,在将非数字类型转换为数字类型时,Number()
方法比较宽容,可以将布尔类型、空类型、未定义类型等转换为数字类型,并且在转换失败时返回NaN。
而parseInt()
和parseFloat()
方法则主要用于将字符串转换为数字类型。其中,parseInt()
方法将字符串转换为整数类型,如果字符串中有非数字字符,会忽略这些字符。而**parseFloat()
方法将字符串转换为浮点数类型,如果字符串中有非数字字符,会尝试将其解析为小数点等符号**。例如:
let a = parseInt('123'); // 返回123,类型为数字类型
let b = parseInt('123abc'); // 返回123,类型为数字类型
let c = parseFloat('3.14'); // 返回3.14,类型为数字类型
let d = parseFloat('3.14abc'); // 返回3.14,类型为数字类型
需要注意的是,当字符串无法转换为数字时,parseInt()
和parseFloat()
方法会返回NaN。此外,parseInt()
方法还可以接收第二个参数,用于指定字符串所表示的进制。例如:
let a = parseInt('11', 2); // 将'11'视为二进制数,返回3
let b = parseInt('11', 8); // 将'11'视为八进制数,返回9
let c = parseInt('11', 16); // 将'11'视为十六进制数,返回17
综上所述,Number()
、parseInt()
和parseFloat()
方法在具体的转换方式和适用场景上有所不同,需要根据具体情况选择合适的方法进行转换。
.toString()
和String()
都是将非字符串类型转换为字符串类型的方法,它们的主要区别在于使用方式和适用场景上。
首先,.toString()
是JavaScript内置对象原型的方法,在使用时需要调用某个对象的.toString()
方法,该方法会返回该对象的字符串表示。例如:
let a = 123;
let b = a.toString(); // 返回字符串'123'
let c = true;
let d = c.toString(); // 返回字符串'true'
需要注意的是,.toString()
方法只能被调用在基本数据类型、数组、日期、数学等内置对象上,如果该对象是自定义对象,则需要自行定义.toString()
方法才能使用。
而**String()
方法则是将任何数据类型转换为字符串类型的全局函数,可以直接传入一个参数进行转换**,例如:
let a = String(123); // 返回字符串'123'
let b = String(true); // 返回字符串'true'
let c = String(null); // 返回字符串'null'
let d = String(undefined); // 返回字符串'undefined'
需要注意的是,当使用String()
方法将null
和undefined
转换为字符串时,它们会分别转换为字符串'null'
和'undefined'
。
综上所述,.toString()
和String()
都是将非字符串类型转换为字符串类型的方法,但是.toString()
是一种基于对象的方法,通常用于将内置对象或自定义对象转换为字符串;而String()
则是一种全局函数,适用于将任何类型的数据转换为字符串。
: 双等于表示值相等。判断操作符两边对象或值是否相等,类型可以不同;当两边类型不一致时会进行强制类型转换后在进行比较,使用Number()转换成Number类型进行判断。例外规则:nullundefined,null/undefiner进行运算时不进行隐式类型转换,通常把值转换为Boolean值进行条件判断。Boolean(null)===Boolean(undefined) > false=false 结果为true.
===:表示严格判断,直接判断两者类型是否相同,如果两边的类型不一致时,不会做强制类型转换,直接返回false;如果相同时则比较大小,不会做任何隐式转换。对于引用类型来说,比较的都是引用内存地址,当两者存储的内存地址相同时才相等,否则结果为false。
objected.is:在===基础上特别处理了NaN,-0,+0**,保证-0与+0不相等,但NaN与NaN相等**。
==操作符的强制类型转换规则 字符串和数字之间的相等比较,将字符串转换为数字之后再进行比较。 其他类型和布尔类型之间的相等比较,先将布尔值转换为数字后,再应用其他规则进行比较。 null 和 undefined 之间的相等比较,结果为真。其他值和它们进行比较都返回假值。 对象和非对象之间的相等比较,对象先调用 ToPrimitive 抽象操作后,再进行比较。 如果一个操作值为 NaN ,则相等比较返回 false( NaN 本身也不等于 NaN )。 如果两个操作值都是对象,则比较它们是不是指向同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回true,否则,返回 false。 '1' == 1 // true '1' === 1 // false NaN == NaN //false +0 == -0 //true +0 === -0 // true Object.is(+0,-0) //false Object.is(NaN,NaN) //true
typeof null的结果是object。在js第一个版本中,所有值都存储在32位的单元中,每个单元包含一个小的类型标签(1-3bits)以及当前要存储值的真实数据。类型标签存储在每个单元的低位,其中null的值时机器码Null指针,即null指针的值全是0.
而000表示的是当前存储的数据指向一个对象,因而typeof null的结果会被判定为Object
000: object - 当前存储的数据指向一个对象。
1: int - 当前存储的数据是一个 31 位的有符号整数。
010: double - 当前存储的数据指向一个双精度的浮点数。
100: string - 当前存储的数据指向一个字符串。
110: boolean - 当前存储的数据是布尔值。
事件指的是 文档和浏览器窗口中发生的特定的交互瞬间,事件就发生了。
事件类型分两种:事件捕获, 事件冒泡
事件捕获指的是:当事件发生时,由外到内,从事件发生的顶点开始,逐渐往下查找,一直到目标元素,该事件流叫做事件捕获流。
事件冒泡指的是:当事件发生时,由内到外,从具体的目标节点元素触发,逐级向上传递,直到根节点,该事件流就是事件冒泡。
事件流指的是:页面接受事件的先后顺序就形成了事件流
事件委托,又名事件代理。事件委托就是利用事件冒泡,就是把子元素的事件都绑定到父元素上。如果子元素阻止了事件冒泡,那么委托也就没法实现了。
阻止事件冒泡:event.stopPropagation() .stop修饰符
addEventListener(‘click’,函数名,true/false) 默认值为false(即 使用事件冒泡,true 则为事件捕获)
好处:减少事件绑定的数量,从而提高性能,并且可以动态地添加或删除子元素而不必重新绑定事件处理器。同时,通过事件冒泡,也可以很方便地处理新增或动态生成的元素所触发的事件。
应用场景 在vue中事件委托:
我们经常遇到vue中v-for一个列表,列表的每一项都绑定了@click处理事件。我们都知道绑定这么多监听,从性能方面来说是不太好的。那我们我们可以通过把每个item的click事件委托给父元素的形式来实现
代码示例:
<templetes> <div id="app"> <ul @click="handleClick"> <li v-for="(item, index) in list" :key="index">{{ item }}</li> </ul> </div> </templetes> <script> ... new Vue({ el: '#app', data: { list: ['列表项1', '列表项2', '列表项3', '列表项4'] }, methods: { handleClick(event) { // 判断是否是列表项元素被点击 if (event.target && event.target.nodeName === 'LI') { // 获取列表项的索引 const index = Array.from(event.target.parentNode.children).indexOf(event.target); // 执行对应列表项的逻辑代码 console.log(`执行列表项${index + 1}的逻辑`); } } } }); </script>
在上面的代码中,我们在 ul
元素上绑定了一个点击事件监听器,并通过 event.target
属性来判断点击的是哪个列表项。然后根据列表项的索引执行对应的逻辑代码。注意,我们在渲染列表项时使用了 v-for
指令,并为每个列表项设置了一个 key
属性,以便 Vue 可以正确地跟踪每个列表项的变化。
概念:
同步:上一件事情没有完成,继续处理上一件事情,只有上一件事情完成了,才会做下一件事情
异步: 规划要做一件事情,如果是异步事情,不是当前立马去执行这件事情,需要等一定的时间,这样的话,我们不会等着他执行,而是继续执行下面的操作
对于写程序,同步往往会阻塞,没有数据过来,我就等着,异步则不会阻塞,没数据来我干别的事,有数据来去处理这些数据。
同步案例:for循环语句,alert(),console.log()等 js大部分都是同步编程
异步案例:所有定时器,ajax异步请求,所有的事件绑定都是异步;
举例子
同步,就是实时处理(如打电话),比如服务器一接收客户端请求,马上响应,这样客户端可以在最短的时间内得到结果,但是如果多个客户端,或者一个客户端发出的请求很频繁,服务器无法同步处理,就会造成涌塞。
同步如打电话,通信双方不能断(我们是同时进行,同步),你一句我一句,这样的好处是,对方想表达的信息我马上能收到,但是,我在打着电话,我无法做别的事情。
异步,就是分时处理(如收发短信),服务器接收到客户端请求后并不是立即处理,而是等待服务器比较空闲的时候加以处理,可以避免涌塞。
BOM(浏览器对象模型)和 DOM(文档对象模型)是 JavaScript 中两个重要的 API。BOM 和 DOM 都是由浏览器提供的,用于操作浏览器窗口、文档等内容。它们之间的主要区别如下:
1.对象层次结构不同
BOM 和 DOM 分别代表了浏览器的不同部分:
2.对象所处的作用域不同
BOM 和 DOM 的对象都有自己的作用域:
3.对象属性和方法的不同
BOM 和 DOM 对象都有自己的属性和方法:
4.对象兼容性的不同
由于不同浏览器的引擎实现可能存在差异,因此在使用 BOM 和 DOM 对象时需要注意兼容性问题:
setTimeout()和setInterval()经常被用来处理延时和定时任务。
setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式
setInterval()则可以在每隔指定的毫秒数循环调用函数或表达式,直到clearInterval把它清除。
window.onload:当一个资源及其依赖资源已完成加载时,将触发onload事件。
document.onDOMContentLoaded:当初始的HTML文档被完全加载和解析完成之后, DOMContentLoaded事件被触发,而无需等待样式表、图像和子框架的完成加载。
它们分别对应着页面中所有资源(包括图片、样式表等)都加载完成和仅仅是文档内容加载完成这两个状态.
区别:
1.触发时间不同
window.onload
事件会在页面中所有的资源都加载完成后触发,包括图片、样式表、脚本等外部资源以及文档中的所有元素。
document.onDOMContentLoaded
事件会在文档内容加载完成后触发,此时外部资源的加载可能还未完成。
2.页面渲染不同
window.onload
事件触发时,页面中所有资源都已加载完毕,包括外部资源和文档中的所有元素,此时页面处于完全渲染完成的状态。
document.onDOMContentLoaded
事件触发时,仅仅是文档内容加载完成,而外部资源(例如图片、样式表、脚本等)可能还未加载完成,此时页面的渲染仍然可能出现闪烁、错位等情况。
3.绑定方法不同
window.onload
事件通常使用 window.addEventListener("load", handler)
或 window.onload = handler
的方式绑定方法。
document.onDOMContentLoaded
事件可以使用 document.addEventListener("DOMContentLoaded", handler)
或 document.onreadystatechange = function() { if (document.readyState === "interactive") { handler(); } }
的方式绑定方法。
如果需要等待页面上所有资源加载完成后再执行一段 JavaScript 代码,就需要使用 window.onload
事件;如果只需要等待文档内容加载完成后再执行一段 JavaScript 代码,就可以使用document.onDOMContentLoaded
事件。
cookie:一个大小不超过4K的小型文本数据,一般由服务器生成,可以设置失效时间;若没有设置时间,关闭浏览器cookie失效,若设置了 时间,cookie就会存放在硬盘里,过期才失效,每次http请求,header都携带cookie
localStorage:5M或者更大,永久有效,窗口或者浏览器关闭也会一直保存,除非手动永久清除或者js代码清除,因此用作持久数据,不参与和服务器的通信
sessionStorage关闭页面或浏览器后被清除。存 放数据大小为一般为 5MB,而且它仅在客户端(即浏览器)中保存,不参与和服务器的通信。
location 对象存储了当前文档位置(URL)相关的信息,简单地说就是网页地址字符串,包括 URL 的各个部分(比如协议、主机名、路径名、查询字符串和哈希值等)。使用 window 对象的 location 属性可以访问。
href会重新定位到一个URL,hash会跳到当前页面中的anchor名字的标记(如果有),而且页面不会被重新加载
常用的属性包括:
href
: 返回当前页面完整的 URL。protocol
: 返回 URL 的协议部分,通常是 http 或 https。host
: 返回 URL 的主机名部分,包括域名和端口号。pathname
: 返回 URL 的路径部分。search
: 返回 URL 的查询部分,即问号后面的内容。hash
: 返回 URL 的片段部分,即井号后面的内容。window 对象给我们提供了一个 history 对象,与浏览器历史记录进行交互。该对象包含用户(在浏览器窗口中) 访问过的 URL。常用的属性包括:
history.back 可以后退一个网页
history.go 可以前进后退 1前进 -1 后退
history.forward 前进到下一个历史记录。
window.navigator`对象包含有关浏览器的信息,可以用它来查询一些关于运行当前脚本的应用程序的相关信息,包括浏览器的类型、版本、操作系统、屏幕大小等。常用的属性包括:
navigator.appCodeName 只读,任何浏览器中,总是返回 ‘Gecko’。该属性仅仅是为了保持兼容性。
navigator.appName 只读,返回浏览器的官方名称。不要指望该属性返回正确的值。
navigator.appVersion 只读,返回一个字符串,表示浏览器的版本。不要指望该属性返回正确的值。
navigator.platform 只读,返回一个字符串,表示浏览器的所在系统平台。
navigator.product 只读,返回当前浏览器的产品名称(如,“Gecko”)。
navigator.userAgent 只读,返回当前浏览器的用户代理字符串(user agent string)
作用域表示一个变量可以使用的范围,主要分为全局作用域和函数作用域;全局作用域是JS中最外层的作用域,在哪里都可以访问;函数作用域即函数中的一个独立作用域,只能在函数内部访问,函数可嵌套故作用域也可以嵌套。
ES6中新增了块级作用域,由大括号包括,如:if(){},for(){}等
1、作用域
作用域就是一个变量可以使用的范围,主要分为全局作用域和函数作用域
全局作用域就是Js中最外层的作用域
函数作用域是js通过函数创建的一个独立作用域,函数可以嵌套,所以作用域也可以嵌套
Es6中新增了块级作用域(由大括号包裹,比如:if(){},for(){}等)
2、自由变量
当前作用域外的变量都是自由变量,一个变量在当前作用域没有定义,但是被使用了,就会向上级作用域,一层一层依次查找,直至找到为止,如果全局作用域都没有找到这个变量就会报错。这个自由变量查找的过程就是作用域链。
3、变量提升
每个var声明的变量,function声明的函数存在变量提升。let const不存在变量提升
在js中声明之前未定义,会在js的最上方会形成一个预解析池,用来存储声明了但没有先定义的变量名
4、作用域链:
作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和 函数 , 简单来说:内部函数访问外部函数的变量这种链式查找的机制被称为作用域链
JavaScript 是一门单线程语言。这意味着在 JavaScript 程序中,只有一个主线程(也称为事件循环)来执行所有的代码,并处理所有的事件和回调函数。因此,在 JavaScript 中任何时候都只有一个任务(或代码片段)能够被执行。
这种设计有其优点和缺点。优点是编写 JavaScript 代码相对简单,并且能够避免许多常见的并发问题,例如死锁、竞态条件等。缺点是在某些情况下,如果程序中存在耗时的操作或者阻塞了主线程的代码,会导致整个应用的响应变慢或者出现假死现象。
尽管 JavaScript 是单线程的,但浏览器和 Node.js 提供了一些机制,使得 JavaScript 可以实现非阻塞 I/O 和异步编程,例如 Promise、async/await、事件驱动等方式。这些机制允许我们将耗时的操作放到后台线程中执行,同时不会阻塞主线程,从而提高 Web 应用的响应能力和用户体验。
此外,HTML5 的 Web Workers API 也提供了一种在 JavaScript 中创建多线程的方式,可以让我们在不阻塞主线程的情况下进行计算密集型的运算。但需要注意的是,Web Workers 中的线程与主线程之间是相互独立的,不能直接进行数据共享,需要通过消息传递来进行通信。
js代码执行过程中会有很多任务,这些任务总的分成两类:
需要注意的是除了同步任务和异步任务,任务还可以更加细分为macrotask(宏任务)和microtask(微任务),js引擎会优先执行微任务
宏任务macrotask: 可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
常见的宏任务:script, setTimeout, setInterval, setImmediate, I/O, UI rendering。
微任务microtask(异步): 可以理解是在当前task执行结束后立即执行的任务。
常见的微任务:process.nextTick(Nodejs),Promise.then(), MutationObserver。
线程,进程?
线程是最小的执行单元,进程是最小的资源管理单元一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程
事件循环(Event Loop)是 JavaScript 实现异步编程的核心机制之一。它规定了 JavaScript 引擎如何从任务队列中取出待处理的事件,并将其分派给适当的回调函数进行处理。
JavaScript 代码的执行过程可以分为两个阶段:同步执行阶段和异步执行阶段。同步执行阶段是指按照代码的顺序,逐行执行 JavaScript 代码所形成的线性任务序列。异步执行阶段是指在任务队列中等待处理的其他事件,例如 DOM 事件、定时器事件、网络请求等。
事件循环机制涉及到三个主要的组件:任务队列、事件循环和回调函数。通常,当某个事件发生时(例如点击按钮或者定时器到期),对应的回调函数会被添加到任务队列中。事件循环会不断地从任务队列中取出一个事件并将其分派给适当的回调函数进行处理,直到任务队列为空为止。
事件循环的工作方式可以通俗来说就是:
需要注意的是,不同类型的任务具有不同的优先级,事件循环会按照一定的顺序来执行任务队列中的事件。例如,微任务(Microtasks)例如 Promise、MutationObserver 会优先于宏任务(Macrotasks)例如 setTimeout、setInterval 执行。
个人理解:
首先js 是单线程运行的,在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。
在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务
当同步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行。
任务队列可以分为宏任务对列和微任务对列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。
当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。
因为js是单线程的,代码执行的时候,将不同的函数执行上下文压入到栈中进行有序的执行,
在执行同步代码的时候,如果遇到了异步事件,js引擎并不会一直等待其返回结果,就是将它挂起,继续执行栈中其他的任务
当同步任务执行完了,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行。
任务队列分为的宏任务队列和微任务队列,当前的执行栈中执行完,js引擎会首先判断微任务队列是否有任务可以执行有的话,放到栈中执行。
当微任务队列中的任务执行完了再去判断宏任务中的队列。
JavaScript 是单线程(js不走完下面不会走是因为同步)会阻塞DOM的解析,因此也就会阻塞DOM的加载。所以有时候我们希望延迟JS的加载来提高页面的加载速度。
有以下多种方式:
1.把JS放在页面的最底部
2.script标签的defer属性:脚本会立即下载但延迟到整个页面加载完毕再执行。该属性对于内联脚本无作用 (即没有 「src」 属性的脚本)。
3.是在外部JS加载完成后,浏览器空闲时,Load事件触发前执行,标记为async的脚本并不保证按照指定他们的先后顺序执行, 该属性对于内联脚本无作用 (即没有 「src」 属性的脚本)。
4.动态创建script标签,监听dom加载完毕再引入js文件
在 JavaScript 中,function函数的声明和var变量的声明都会被提升到作用域的顶部,但是函数表达式和变量赋值不会被提升。
JS代码在执行前,浏览器会对js代码进行扫描,默认的把所有带var和function声明的变量进行提前的声明或者定义,遵循先解析后使用的原则。 变量提升的表现是,在代码执行前,JavaScript 引擎会将变量和函数的声明提升到作用域顶部。这意味着变量和函数可以在声明之前使用。
具体来说,变量提升包含两种类型的声明:变量声明和函数声明。
对于变量声明,变量提升的原理是将所有变量的声明提前到当前作用域的顶部,但是并不会为这些变量赋初值,因此在使用这些变量之前,它们的值都是 undefined。例如:
console.log(a); // undefined
var a = 1;
上面的代码中,变量 a 的声明会被提升到作用域的顶部,但是在赋值之前,它的值为 undefined。
对于函数声明,变量提升的原理是将函数声明的整个定义提前到当前作用域的顶部,因此在声明位置之前就可以使用这个函数。例如:
foo(); // "bar"
function foo() {
console.log("bar");
}
上面的代码中,函数 foo 的声明被提升到作用域的顶部,因此在调用函数之前就可以使用它。这也是为什么在 JavaScript 中可以先调用一个函数,再定义它的原因。
实现原理方面,变量提升是由 JavaScript 引擎在编译阶段自动完成的。在编译阶段,JavaScript 引擎会将所有的变量声明和函数声明都建立在内存中的作用域链上。
作用域链可以理解为是一种包含多个执行环境的链式结构,每个执行环境都会持有一个对其父级执行环境的引用,从而形成了一个层层嵌套的执行环境体系。当需要访问一个变量时,JavaScript 引擎会从当前作用域开始,沿着作用域链不断向上查找,直到找到对应的变量为止。
通过将变量和函数声明放在作用域链的顶部,JavaScript 引擎会先将变量和函数声明提前到当前作用域链的顶部,并且在访问这些变量和函数时,会按照作用域链的查找规则去查找它们,从而保证了变量和函数的可用性。
原因 JavaScript引擎在代码执行前有一个解析的过程(预编译),创建执行上线文,初始化一些代码执行时需要用到的对象。 当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性, 它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。
首先要知道,JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行。
1.在解析阶段 JS会检查语法,并对函数进行预编译。
解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来, 变量先赋值为undefined,函数先声明好可使用。
在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似, 不过函数执行上下文会多出this、arguments和函数的参数。
全局上下文:变量定义,函数声明
函数上下文:变量定义,函数声明,this,arguments
2.在执行阶段,就是按照代码的顺序依次执行。让开发者更加方便地编写 JavaScript 代码,并且避免一些难以排查的错误。
1、提高性能
2、容错性更好
(1)提高性能 在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,让函数可以在执行时预先为变量分配栈空间。
在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、
不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),
并且因为代码压缩的原因,代码执行也更快了。
(2)容错性更好 变量提升可以在一定程度上提高JS的容错性
当函数声明与变量声明冲突时,变量提升时函数优先级更高,会忽略同名的变量声明
mouseenter
仅在鼠标进入当前元素时触发一次,而 mouseover
在鼠标进入当前元素以及当前元素的子元素时都会触发。mouseenter
不存在冒泡行为,即事件不会向父元素传递,而 mouseover
存在冒泡行为,即事件会向父元素传递。Event
),但是它们传递给回调函数的事件对象是不同的。在 mouseover
事件中,事件对象的 relatedTarget
属性表示鼠标从哪个元素移入当前元素;在 mouseenter
事件中,事件对象的 fromElement
属性表示鼠标从哪个元素移入当前元素。解释:服务端渲染的模式下,当用户第一次请求页面时,由服务器把需要的组件或页面渲染成 HTML 字符串,然后把它返回给客户端。客户端拿到手的,是可以直接渲染然后呈现给用户的 HTML 内容,不需要为了生成 DOM 内容自己再去跑一遍 JS 代码。使用服务端渲染的网站,可以说是“所见即所得”,页面上呈现的内容,我们在 html 源文件里也能找到。有了服务端渲染,当请求用户页面时,返回的body里已经有了首屏的html结构,之后结合css显示出来。
优点:
①首屏渲染快(关键性问题):相比于加载单页应用,我只需要加载当前页面的内容,而不需要像 React 或者 Vue 一样加载全部的 js 文件;
②SEO(搜索引擎)优化:不同爬虫工作原理类似,只会爬取源码,不会执行网站的任何脚本
③可以生成缓存片段、节能;
缺点:用户体验较差,不容易维护、通常前端改了部分html或者css,后端也需要改;
使用场景:vue全家桶或者react全家桶,都是推荐通过服务端渲染来实现路由的。
函数声明会将那个函数提升到最前面(即使你写代码的时候在代码块最后才写这个函数),成为全局函数。
函数声明要指定函数名,而函数表达式不用,可以用作匿名函数。
函数声明
function 函数名(参数1,参数2,...){ //要执行的语句 }
函数表达式
var func2=function(b){}//函数表达式
var func3=function func4(c){}//命名式函数表达式
var func5=(function(n1,n2){})();//立即执行的函数表达式
return function(){ };//作为返回值的函数表达式
Function构造器
var 变量名 = new Function("参数1","参数2",...,"参数n","函数体");
立即执行函数
var func5=(function(n1,n2){})();//立即执行的函数表达式 ()()
函数的length
属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length
属性将失真。
立即执行函数:( function( ){ })( ) 返回值可以为基本数据类型,也能返会任何类型的值。
写法原因:因为在 javascript 里,括号内部不能包含语句,当解析器对代码进行解释的时候,先碰到了(), 然后碰到function关键字就会自动将()里面的代码识别为函数表达式而不是函数声明。
作用:立即执行函数会形成一个单独的作用域,我们可以封装一些临时变量或者局部变量,避免污染全局变量。
使用场景: ①代码在页面加载完成之后,不得不执行一些设置工作,比如时间处理器,创建对象等等。 ②所有的这些工作只需要执行一次,比如只需要显示一个时间。
③需要一些临时的变量,但是初始化过程结束之后,就再也不会被用到,我们可以用立即执行函数——去将我们所有的代码包裹在它的局部作用域中, 不会让任何变量泄露成全局变量。
new实际上是在堆内存中开辟一个空间。
①创建一个空对象,构造函数中的this指向这个空对象;
②这个新对象被执行[ [ 原型 ] ]连接;
③执行构造函数方法,属性和方法被添加到this引用的对象中;
④如果构造函数中没有返回其它对象,那么返回this,即创建的这个的新对象,否则,返回构造函数中返回的对象。
function _new(){
let target = {}; //创建的新对象
let [constructor,...args] = [...arguments];
//执行[[原型]]连接,target是constructor的实例
target.__proto__ = constructor.prototype;
//执行构造函数,将属性或方法添加到创建的空对象上
let result = constructor.prototype;
if(result && (typeof (result) == "object" || typeof (result) == "function")){
//如果构造函数执行的结构返回的是一个对象,那么返回这个对象
return result;
}
//如果构造函数返回的不是一个对象,返回创建的对象
return target;
}
自己理解的new:
new实际上就是在堆内存中开辟一个空间,创建一个空对象,并将this指向这个空对象,将该对象的原型和构造函数的原型对象进行连接,然后执行构造函数方法,将属性和方法添加到新对象中,最后判断其返回值,返回新对象或者构造函数返回的对象。
arguments 当我们不知道有多少个参数传进来的时候就用 arguments 来接收,是一个类似于数组的对象,他有length属性,可以arguments[ i ]来访问对象中的元素, 但是它不能用数组的一些方法。 例如push、pop、slice等。arguments虽然不是一个数组,但是它可以转成一个真正的数组。
取之可以用 展开运算符来 数组和类数组类数组: ①拥有length属性,其它属性(索引)为非负整数;箭头函数里没有arguments ②不具有数组所具有的方法; ③类数组是一个普通对象,而真实的数组是Array类型。
常见的类数组:arguments,document.querySelectorAll得到的列表,jQuery对象($(“div”));
在全局的环境下this是指向window 的
普通函数调用直接调用中的this 会指向 window, 严格模式下this会指向 undefined,自执行函数 this 指向 window,定时器中的 this 指向 window
在对象里调用的this,指向调用函数的那个对象,
在构造函数以及类中的this,构造函数配合 new 使用, 而 new 关键字会将构造函数中的 this 指向实例化对象,所以构造函数中的 this 指向 当前实例化的对象
方法中的this谁调用就指向谁。
箭头函数没有自己的 this,箭头函数的this在定义的时候,会继承自外层第一个普通函数的this
浏览器的js具有自动垃圾回收机制,垃圾回收机制也就是自动内存管理机制,垃圾收集器会定期的找出那些不在继续使用的变量,然后释放内存。但是这个过程不是实时的,因为GC开销比较大并且时停止响应其他操作,所以垃圾回收器会按照固定的时间间隔周期性的执行。
如果 那些不再使用的变量,它们所占用的内存 不去清除的话就会造成内存泄漏
内存泄露其实就是我们的程序中已经动态分配的堆内存,由于某些原因没有得到释放,造成系统内存的浪费导致程序运行速度减慢甚至系统崩溃等严重后果。
比如说:
1、闭包:在闭包中引入闭包外部的变量时,当闭包结束时此对象无法被垃圾回收(GC)。
2、DOM:当原有的DOM被移除时,子结点引用没有被移除则无法回收
3、Times计时器泄露
都是来改变this指向和函数的调⽤,实际上call与apply的功能是相同的,只是两者的传参方式不一样,
call⽅法跟的是⼀系列参数,需要将参数按顺序依次传入
apply跟⼀个 数组作为参数,call⽅法和apply使⽤后就直接调⽤
function sum(a, b, c) {
return a + b + c;
}
let result1 = sum.call(null, 1, 2, 3); // 返回6
let result2 = sum.apply(null, [1, 2, 3]); // 返回6
需要注意的是,在使用.call()和.apply()时,第一个参数为null或undefined时,默认会使用全局对象window或者global作为this指向对象。
bind 传参后不会立即执行,而是返回一个改变了this指向的函数,这个函数可以继续传参,且执行,需要类似于bind()()两个括号才能调⽤。
function sum(a, b, c) {
return a + b + c;
}
let add = sum.bind(null, 1, 2); // 返回一个新函数,this指向null,第一、二个参数分别为1、2
let result = add(3); // 调用新函数,返回6
.bind()方法返回一个新函数是它与.call()和.apply()最明显的不同之处。此外,.bind()方法还可以通过在返回的新函数上再次调用.bind()方法来实现对this值的多次绑定。
call 的性能要比apply好一点(尤其是当函数传递参数超过3个的时候)
call 用扩展运算符就可以把 apply 来代替了
函数式编程是一种强调以函数为主的软件开发风格。通过组合纯函数,避免共享状态、可变作用和副作用来构建软件的过程。 目的:使用函数来抽象作用在数据之上的控制流和操作,从而在系统中消除副作用并减少对状态的改变。常见的函数式编程语言包括 Haskell、Lisp、Clojure 等。
声明式编程:专注于”做什么”而不是”如何去做”。在更高层面写代码,更关心的是目标,而不是底层算法实现的过程。 如:css, 正则表达式,sql 语句,html, xml…
命令式编程(过程式编程) : 专注于”如何去做”,这样不管”做什么”,都会按照你的命令去做。解决某一问题的具体算法实现。常见的命令式编程语言包括 C、Java、Python 等。
如: for()
函数式编程:把运算过程尽量写成一系列嵌套的函数调用。
如 : forEach()
柯里化是指将一个接收多个参数的函数转化为一个接收单个参数的函数序列的技术,而偏应用则是指将一个多参数函数变换成只接受部分参数并返回一个新函数的技术。
例如,将求和函数实现柯里化:
function sum(a, b, c) {
return a + b + c;
}
let currySum = function(a) {
return function(b) {
return function(c) {
return sum(a, b, c);
};
};
};
let result1 = currySum(1)(2)(3); // 返回6
let addOne = currySum(1); // 返回一个接收1个参数的新函数
let result2 = addOne(2)(3); // 返回6
需要注意的是,手动实现函数的柯里化和偏应用可以让函数更加灵活和复用性更高,但需要花费一定的时间和精力。
特点:
①接收单一参数,将更多的参数通过回调函数来搞定;
②返回一个新函数,用于处理所有的想要传入的参数;
③需要利用call/apply与arguments对象收集参数;
④返回的这个函数正是用来处理收集起来的参数。
作用:能进行部分传值,而传统函数调用则需要预先确定所有实参。如果你在代码某一处只获取了部分实参,然后在另一处确定另一部分实参,这个时候柯里化和偏应用就能派上用场。
用途:我认为函数柯里化是对闭包的一种应用形式,延迟计算、参数复用、动态生成函数(都是闭包的用途)。
偏应用(Partial Application)指的是将多参数函数转换为只接受部分参数,并返回一个新函数的过程。与柯里化不同的是,偏应用通常是先预先给定部分参数,再在后面进行调用。偏应用的好处包括:
对象是一种复合数据类型,它可以包含属性和方法,并且可以通过点号(.
)或中括号([]
)操作符来访问和操作。
在 JavaScript 中,对象可以通过对象字面量、构造函数等方式来创建。对象字面量是一种简便的表示方式,可以用 {}
包裹多个属性和方法来创建对象;构造函数则是一种使用 new
关键字创建对象的方式,它包括了构造函数本身以及原型对象,并且可以通过 prototype
属性和 this
关键字来定义对象的属性和方法。
const person = {
name: "张三",
age: 20,
gender: "男",
sayHi() {
console.log(`你好,我是${this.name},今年${this.age}岁,${this.gender}。`);
},
};
console.log(person.name); // 输出 "张三"
console.log(person["age"]); // 输出 20
person.sayHi(); // 输出 "你好,我是张三,今年20岁,男。"
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.sayHi = function () {
console.log(`你好,我是${this.name},今年${this.age}岁,${this.gender}。`);
};
}
const person1 = new Person("李四", 25, "女");
console.log(person1.name); // 输出 "李四"
console.log(person1.age); // 输出 25
person1.sayHi(); // 输出 "你好,我是李四,今年25岁,女。"
上述代码定义了一个名为 Person
的构造函数,它有三个参数:name
、age
和 gender
,以及一个属性和一个方法,用于存储对象的状态和行为。可以使用 new
关键字来调用构造函数,并创建一个新的 Person
对象。需要注意的是,构造函数内部的属性和方法都要使用 this
关键字,这样创建出来的对象才能正确访问和调用。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHi() {
console.log(`你好,我是${this.name},今年${this.age}岁。`);
}
}
const person = new Person("张三", 20);
person.sayHi(); // 输出 "你好,我是张三,今年20岁。"
上面的代码中,使用class关键字定义了一个名为Person的类,它有一个constructor方法,用于初始化对象的属性,以及一个sayHi方法用于输出对象信息。使用new关键字创建Person类的实例对象,然后调用sayHi方法输出信息。
面向对象编程 (Object-oriented Programming,OOP) 是一种编程范式,它将真实世界中的事物抽象成一个个对象,并将对象作为程序的基本单元来进行设计和开发。面向对象编程的核心思想是“封装、继承、多态”。
封装指的是将对象的属性和方法进行包装,使得外部只能通过公共接口访问和操作对象,从而保证了对象的安全性和可维护性。在 JavaScript 中,可以通过闭包、工厂函数等方式实现封装。下面是一个使用闭包实现封装的示例:
function createPerson(name, age, gender) { // 定义私有变量和方法 const hobbies = ["吃饭", "睡觉", "打游戏"]; function printHobbies() { console.log(`${name} 的爱好是:${hobbies.join(", ")}`); } // 返回公共接口 return { getName() { return name; }, getAge() { return age; }, getGender() { return gender; }, sayHi() { console.log(`你好,我是${name},今年${age}岁,${gender}。`); }, addHobby(hobby) { hobbies.push(hobby); printHobbies(); }, }; } const person = createPerson("张三", 20, "男"); console.log(person.getName()); // 输出 "张三" console.log(person.getAge()); // 输出 20 person.sayHi(); // 输出 "你好,我是张三,今年20岁,男。" person.addHobby("看电影"); // 输出 "张三 的爱好是:吃饭, 睡觉, 打游戏, 看电影"
上述代码定义了一个名为 createPerson
的工厂函数,它通过闭包实现了对 name
、age
和 gender
等私有变量的封装。在返回的公共接口中,定义了一些公共方法,如 getName
、getAge
、sayHi
等,外部只能通过这些方法来访问和操作对象,从而保证了对象的安全性和可维护性。
继承指的是在已有类的基础上创建一个新类,在新类中可以重用已有类的属性和方法,并且可以进行扩展,增加新的属性和方法。
在ES6之前 JavaScript 中的继承机制不像传统的面向对象语言那么完善,但是可以使用原型链实现继承。下面是一个使用原型链实现继承的示例:
function Animal(name) { this.name = name; } Animal.prototype.sayHi = function () { console.log(`你好,我是${this.name}。`); }; function Cat(name, color) { Animal.call(this, name); // 调用父类构造函数初始化属性 this.color = color; } Cat.prototype = Object.create(Animal.prototype); // 将 Cat 的原型设置为 Animal 的实例 Cat.prototype.constructor = Cat; // 修复 Cat 的构造函数指向 Cat.prototype.sayHi = function () { console.log(`你好,我是${this.color}色的${this.name}。`); }; const cat = new Cat("小花", "白"); cat.sayHi(); // 输出 "你好,我是白色的小花。"
上述代码定义了一个名为 Animal
的父类和一个名为 Cat
的子类,通过在子类中设置原型来实现了继承。在子类中,使用 call
方法调用父类的构造函数来初始化子类的属性,并且覆盖了父类的 sayHi
方法来实现多态。
在ES6之后可以直接通过extends来实现继承。
class Animal { constructor(name) { this.name = name; } sayHi() { console.log(`你好,我是${this.name}。`); } } class Cat extends Animal { constructor(name, color) { super(name); // 调用父类的构造函数 this.color = color; } sayHi() { console.log(`你好,我是${this.color}色的${this.name}。`); } } const cat = new Cat("小花", "白"); cat.sayHi(); // 输出 "你好,我是白色的小花。"
在上面的示例中,使用extends关键字让Cat类继承自Animal类,从而实现了类继承。注意,在子类constructor函数中必须调用super方法来调用父类的构造函数,否则会报错。
多态指的是同一个方法可以根据对象的不同类型表现出不同的行为。在 JavaScript 中,由于函数参数的灵活性,实现多态比较容易。下面是一个使用多态的示例:
function calculate(area) { if (area instanceof Circle) { return Math.PI * area.radius ** 2; } else if (area instanceof Rectangle) { return area.width * area.height; } else { throw new Error("不支持的类型!"); } } function Circle(radius) { this.radius = radius; } function Rectangle(width, height) { this.width = width; this.height = height; } const circle = new Circle(5); console.log(calculate(circle)); // 输出 78.53981633974483 const rect = new Rectangle(3, 4); console.log(calculate(rect)); // 输出 12 const triangle = { base: 5, height: 8 }; console.log(calculate(triangle)); // 抛出异常 "不支持的类型!"
上述代码定义了两个表示圆形和矩形的类 Circle
和 Rectangle
,以及一个函数 calculate
,根据传入的对象来计算面积。在 calculate
函数中,通过判断对象的类型来实现多态,在传入不支持的类型时会抛出异常。
使变量能够长时间驻留在内存中,不被垃圾回收器回收
也继承了函数的特性
闭包表现形式:
例如:
普通函数:
let a=1;
function fn(){
a++;
console.log(a)
}
fn() //2
fn() //3
但这样可能会遇到a全局变量被污染的情况
函数:
function fn(){
let a=1;
a++;
console.log(a)}
fn() //2
fn() //2
但每次调用完函数之后,函数都会被垃圾回收,导致a每次都没有更改
因此使用闭包,将变量长时间驻留在内存中
function fn(){ let a=1; return function(){ a++; console.log(a) } } let f = fn() f() //2 f() //3
使用闭包能够使变量长时间存储在内存中而不会被垃圾回收,但使用不当时会导致内存泄漏的情况
因此可以将函数清除来解决内存泄露的问题
例如在上面的例子中再加上
f = null
即可解决内存泄漏的问题
深拷贝和浅拷贝是针对复杂数据类型来说的,浅拷贝只拷贝一层,而深拷贝是层层拷贝。
1.浅拷贝:
将原对象或原数组的引用直接赋给新对象,新数组,新对象只是对原对象的一个引用,而不复制对象本身,新旧对象还是共享同一块内存
如果属性是一个基本数据类型,拷贝就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,
2.深拷贝:
创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”
深拷贝就是把一个对象,从内存中完整的拷贝出来,从堆内存中开辟了新区域,用来存新对象,并且修改新对象不会影响原对象
3、赋值:
当我们把一个对象赋值给一个新的变量时,赋的是该对象在栈中的内存地址,而不是堆中的数据。也就是两个对象
基本数据类型之间的变量赋值操作中,拷贝传递的就是值
但在引用数据类型之中,变量赋值分浅拷贝与深拷贝两种形式
浅拷贝即拷贝传递的是变量的内存地址
如:
let a= [1,2,3]
let b = a
a[0] = 5
console.log(b) // [5,2,3]
在这里改变了a中的变量,但b中的变量也跟着改变
因为b存的是引用数据类型在堆内存中的地址
深拷贝指的是将引用数据类型中所有的值拷贝到另一个变量中
但也分深拷贝一层和深拷贝多层
例如:
let a=[1,2,3]
let b=[...a]
a[0]=5
console.log(b) //[1,2,3]
在这里改变了a中的变量,b没有发生改变
即表示b完全拷贝了a的值而不是地址
但遇到多维数组的情况下,依旧是存储地址
如:
let a=[1,2,3,[1,2,3]]
let b=[...a]
a[3] [0] = 5
console.log(b) //[1,2,3,[5,2,3]]
即仅拷贝一层,而没有进行更深层次的拷贝
但在一般情况下也称之为深层拷贝
防抖指的是:
在一定时间内,只能执行一次操作,当每执行一次操作后,都会将前面的操作给销毁;即只执行最后的一次操作
节流指的是:
在一定时间内,只能执行一次操作,当执行一次操作后,不能再做后续操作;即只执行最开始的一次操作
代码如下:
let clickDom = document.querySelector("#buttonOnClick") clickDom.addEventListener("click",clickEvent,false) function clickEvent(){ let eventClick = null; return args => { if(eventClick){ clearTimeout(eventClick)} else setTimeout(Click,1000) } } function Click(){ console.log("click") }
当执行点击事件后一秒内再次执行点击事件,会将之前的事件销毁清除,即一定时间内只执行最后一次的事件,该代码展示的是防抖操作。
let clickDom = document.querySelector("#buttonClick") clickDom.addEventListener("click",clickEvent,false) function clickEvent(){ let timer = null; return args => { if(!timer){ timer = setTimeout(()=>{ click(); timer=null; },1000); } } }
当执行一次点击事件一秒后执行click事件,在该段时间执行的任何操作都被无视,即在一定时间内只执行第一次的事件,该代码展示的是节流操作。
File
用来表示一组文件,使用选择文件时,这些文件被存储在File对象中。也可以通过new File()创建一个新的File对象。
Blob
Blob对象表示二进制数据,常用来表示大型数据对象(如图片,音频等)。File对象是Blob对象的一个子类,它继承了Blob对象的所有属性和方法
FormData
前端将文件存储在formdata对象中,才能传给后端
slice方法
File对象的slice方法是从父类Blob对象继承而来,用于从文件中提取一段范围的数据。
.slice(start,end)
start:开始提取数据的字节偏移量,默认为0
end:结束提取数据的字节偏移量,默认为文件或blob对象的总字节数
slice方法返回值是一个新的blob对象,包含从原始文件或blob对象中提取的指定字节范围的数据
大文件分片上传流程:
代码如下:
document .getElementById("upload") .addEventListener("click",async function(){ const fileInput = document,getElementById("fileInput") const file = fileInput.files[0] const chunkSize = 100 * 1024 //1024kb const totalChunks = Math.ceil(file.size/chunkSize) for(let i=0;i<totalChunks;i++){ const start = i*chunksize const end = Math.min(start+chunkSize,file.size) const chunk = file.slice(start,end) const result = await uploadChunk(chunk) } }) async function uploadChunk(chunk){ const formData = new FormData() formData.append("file",chunk) const response = await fetch("https://file.io",{ method:"POST", body:formData }); const result = await response.json() return result }
每个上传的文件都新增一个md5签名,由此来判断文件是否被上传。
部分存在的校验:
通过比较文件的size来判断切片上传到哪一步,进而对后续切片进行切片上传
" == "操作符在左右数据不一致的时候,会先进行隐式转换,该值意味着不是基本数据类型,
因为如果a是null或者undefined、bool类型都不可能返回true;可以推测a是复杂数据类型。
方法一:数组的 toString 接口默认调用数组的 join 方法,重新 join 方法
也可以重写toString接口,让每次调用都+1
let a = [1,2,3];
a.join = a.shift;
console.log(a == 1 && a == 2 && a == 3) //true
方法二:利用数据劫持(Proxy/Object.definedProperty)
let i = 1;
let a = new Proxy({},{
i:1,
get:function(){
return () => this.i++
}
});
console.log(a == 1 && a == 2 && a == 3);
0.1 和 0.2 都是用二进制表示的无限循环小数,而在计算机中,存储浮点数是有精度限制的,因此会出现精度误差。
可以使用以下两种方式解决:
使用 toFixed()
方法将浮点数转换为指定小数位数的字符串,再进行比较。
const a = 0.1;
const b = 0.2;
const c = 0.3;
console.log((a + b).toFixed(1) === c.toFixed(1)); // true
定义一个误差范围,在误差范围内就认为两个浮点数相等。
const a = 0.1;
const b = 0.2;
const c = 0.3;
console.log(Math.abs(a + b - c) < 0.000001); // true
新增了块级作用域(let,const)
提供了定义类的语法糖(class)
新增了一种基本数据类型(Symbol)
新增了变量的解构赋值
函数参数允许设置默认值,引入了 rest 参数,新增了箭头函数
数组新增了一些 API,如 isArray / from / of 方法;数组实例新增了entries(),keys() 和 values() 等方法
对象和数组新增了扩展运算符
ES6 新增了模块化(import/export)
ES6 新增了 Set 和 Map 数据结构
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例
ES6 新增了生成器(Generator)和遍历器(Iterator)
let 和 const 取代了 var,用于声明块级作用域的变量和常量,其作用域只在 {} 内部,比如 if、for、while 或者任意代码块内。let 声明的变量可以被重新赋值,而 const 声明的常量是不可修改的。这样做的好处是可以避免 var 的变量提升和作用域问题,保证代码更加简洁和可读性更高。
例如:
let a = 1;
const b = 2;
if (true) {
let a = 3;
const b = 4;
}
console.log(a); // 1
console.log(b); // 2
箭头函数语法更加简洁,不需要写 function 关键字,同时还解决了函数内 this 丢失的问题。箭头函数中的 this 指向定义时所在的作用域(词法作用域),而不是运行时所在的作用域。另外,当只有一个参数时,也可以省略参数的小括号。
例如:
const arr = [1, 2, 3];
const res = arr.map(item => item * 2);
console.log(res); // [2, 4, 6]
const obj = {
name: 'Tom',
sayHello: () => {
console.log(`Hello, ${this.name}`); // this 指向全局对象 window,输出 "Hello, undefined"
}
};
注意:
箭头函数是匿名函数不能作为构造函数,不能使用new
箭头函数不绑定arguments,取而代之用rest参数…解决,
this指向不同,箭头函数的this在定义的时候继承自外层第一个普通函数的this
箭头函数通过call()或apply()调用一个函数,只传入了一个参数,对this并没有影响.
箭头函数没有prototype(原型),所以箭头函数本身没有this
箭头函数不能当做Generator函数,不能使用yield关键字、
写法不同,箭头函数把function省略掉了 ()=> 也可以吧return 省略调 写法更简洁
箭头函数不能通过call()、apply()、bind()方法直接修改它的this指向。
模板字符串使用反引号“`”表示,在其中可以直接插入变量或表达式,同时支持多行字符串和标签模板等功能。模板字符串中可以使用 ${} 来插入变量或表达式,这样使得代码更加简洁并且易读。
例如:
const name = 'Tom';
const str = `My name is ${name}`;
console.log(str); // "My name is Tom"
const tpl = `<ul>
<li>111</li>
<li>222</li>
<li>333</li>
</ul>`;
console.log(tpl);
解构赋值允许从数组或对象中提取值并赋给变量,更加简化了代码,提高了可读性。使用方括号“[]”来表示数组结构,使用花括号“{}”来表示对象结构。
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构赋值
常见的几种方式有
1.默认值
2.交换变量
3.将剩余数组赋给一个变量
结构数组和对象字符串区别
对象的解构与数组类似,但有所不同。数组的元素是按次序排列的,变量的取值由它的位置决定;
而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。字符串也是可以解构赋值的。字符串被转换成了一个类似数组的对象.
我在项目中:就是从目标对象或数组中提取自己想要的变量。最常用的场景是:element-ui,vant-ui按需引入,请求接口返回数据,提取想要数据。
例如:
const arr = [1, 2, 3];
const [a, b, c] = arr;
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
const obj = { x: 1, y: 2, z: 3 };
const { x, y, z } = obj;
console.log(x); // 1
console.log(y); // 2
console.log(z); // 3
ES6 中引入了许多新的对象和数组方法,使得开发者可以更加方便地操作对象和数组。
例如:
// 对象扩展
const obj1 = { x: 1 };
const obj2 = { y: 2 };
const newObj = Object.assign(obj1, obj2);
console.log(newObj); // {x: 1, y: 2}
// 数组扩展
const arr1 = Array.of(1, 2, 3, 4);
const arr2 = arr1.find(item => item > 2);
const arr3 = arr1.findIndex(item => item > 2);
console.log(arr1); // [1, 2, 3, 4]
console.log(arr2); // 3
console.log(arr3); // 2
ES6 引入了 class 表示类和 extends 表示继承,更加符合面向对象编程的习惯。类中支持定义构造函数、实例方法、静态方法等,extends 关键字用于继承父类,同时也支持 super 关键字来调用父类的方法。
例如:
class Animal { constructor(name) { this.name = name; } sayHello() { console.log(`My name is ${this.name}`); } } class Dog extends Animal { constructor(name) { super(name); } sayHello() { super.sayHello(); console.log('Wang!'); } } const dog = new Dog('Lucy'); dog.sayHello(); // "My name is Lucy","Wang!"
Promise 是一种用于异步编程的解决方案,它代表一个异步操作的最终完成或失败,可以更加方便地处理异步操作。Promise 支持 then()、catch()、finally() 等方法来处理异步回调,可以避免回调函数嵌套带来的可读性和维护性问题。
1、Promise 是异步编程的一种解决方案,主要用于异步计算,支持链式调用,可以解决回调地狱 的问题,自己身上有all、reject、resolve、race 等方法,原型上有then、catch等方法。
2、可以将异步操作队列化,按照期望的顺序执行,返回符合预期的结果,可以在对象之间传递和操作 promise,帮助我们处理队列
3、promise 有三个状态:pending[待定]初始状态,fulfilled[实现]操作成功,rejected[被否决]操作失败
4、Promise 对象状态改变:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了
5、如果不设置回调函数,Promise内部抛出的错误,不会反应到外部,但是写了then 和 catch ,会被then的第二个参数 或 catch所捕获
例如:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('Success');
} else {
reject('Error');
}
}, 1000);
});
promise.then(res => {
console.log(res);
}).catch(err => {
console.log(err);
});
注意:
promise 的then会返回一个新的 promise 对象,能保证 then 方 可以进行链式调用
Promise.all哪怕一个请求失败了也能得到其余正确的请求结果的解决方案
ES6 中引入了模块化的概念,可以通过 export 和 import 语句来导出和导入模块中的变量、函数和类等。模块化可以使得代码更加模块化和可维护性更高,并且可以让不同的模块之间互相独立。
例如:
// 模块 a.js
export const a = 1;
export function sayHello() {
console.log('Hello!');
}
// 模块 b.js
import { a, sayHello } from './a.js';
console.log(a); // 1
sayHello(); // "Hello!"
CommonJS 是一种用于 JavaScript 模块化的规范,它主要是为了解决浏览器端 JavaScript 缺少模块化机制的问题而提出的。CommonJS 规范的主要目标是使 JavaScript 代码可以像其他语言一样具有模块化功能,以便于更好地管理和复用代码。
CommonJS 规范定义了一个称为模块(module)的基本单位,每个模块都是一个单独的文件,文件内部的所有变量、函数、对象等成员都是私有的,对外暴露的只有指定的接口,从而避免了命名冲突和代码污染的问题。模块之间可以相互引用,从而实现了代码的复用和管理。
CommonJS 规范中定义了两个与模块相关的核心方法 require() 和 module.exports,其中 require() 用于加载模块,module.exports 用于向外部输出模块接口,并且支持循环引用的模块依赖关系。
例如,在 CommonJS 规范下,我们可以创建一个模块 hello.js:
const message = 'Hello World!';
function sayHello() {
console.log(message);
}
module.exports = {
sayHello: sayHello
};
然后在另一个模块中使用 require() 方法加载该模块:
const hello = require('./hello');
hello.sayHello(); // 输出 "Hello World!"
在 Node.js 等支持 CommonJS 规范的环境中,可以方便地使用模块化编程,提高代码的可维护性和复用性。
ES6(ECMAScript 2015)是 JavaScript 的一个更新的主要版本和一种新的规范,它在2015年推出,并包括了一系列重要的改进和增强特性。ES6 的目标是使 JavaScript 更加强大、更易读写、更可靠,从而进一步提高开发效率。
ES6 规范引入了很多新特性,其中包括:
这些新特性使得 ES6 在代码编写、调试、调用上都有了显著的提高,极大地提升了代码的可读性、可维护性和可扩展性。虽然目前还有一些浏览器并不支持 ES6 的全部特性,但是随着时间的推移,ES6 已经成为了 JavaScript 开发的重要标准。
CommonJS 和 ES6 都是用于 JavaScript 模块化的规范,有各自的特点和用途。在 Node.js 环境下,还是使用 CommonJS 规范来编写模块更为常见;在浏览器端,则使用 ES6 规范更为普遍。
虽然 CommonJS 和 ES6 都是用于 JavaScript 模块化的规范,但是在实现方式、编译时机、作用域、支持环境等方面存在明显的区别。
实现方式不同:
CommonJS 是一种模块化规范,它的实现是基于 Node.js 的 require() 和 module.exports 等方法,这些方法只在 Node.js 运行时环境中存在。而 ES6 模块化规范则是在语言层面上定义的,与具体实现平台无关。
编译时机不同:
CommonJS 模块化规范是在运行时加载模块,即需要哪个模块就立即通过 require() 加载哪个模块,而 ES6 模块化规范则是在编译时确定模块的依赖及其输出的接口,在运行时进行按需加载。
作用域不同:
CommonJS 中的模块是运行时加载和执行的,所以模块中的变量和函数是在模块内部私有的,不会对外暴露,即每个模块都有自己的作用域。ES6 模块化中的模块则是静态加载和编译的,每个模块都是独立的,没有办法修改模块内部的变量。
支持的环境不同:
CommonJS 规范主要应用于后端服务器端编程,而 ES6 规范主要应用于前端浏览器端编程,逐渐成为新一代的 JavaScript 标准。
1、import是ES6中的语法标准也是用来加载模块文件的,import函数可以读取并执行一个JavaScript文件,然后返回该模块的export命令指定输出的代码。export与export default均可用于导出常量、函数、文件、模块,export可以有多个,export default只能有一个。
2、require 定义模块:module变量代表当前模块,它的exports属性是对外的接口。通过exports可以将模块从模块中导出,其他文件加载该模块实际上就是读取module.exports变量,他们可以是变量、函数、对象等。在node中如果用exports进行导出的话系统会系统帮您转成module.exports的,只是导出需要定义导出名。
require与import的区别
1,require是CommonJS规范的模块化语法,import是ECMAScript 6规范的模块化语法;
2,require是运行时加载,import是编译时加载;
3,require可以写在代码的任意位置,import只能写在文件的最顶端且不可在条件语句或函数作用域中使用;
4,require通过module.exports导出的值就不能再变化,import通过export导出的值可以改变;
5;require通过module.exports导出的是exports对象,import通过export导出是指定输出的代码;
6,require运行时才引入模块的属性所以性能相对较低,import编译时引入模块的属性所所以性能稍高。
相同点
都是循环遍历数组中的每一项 forEach和map方法里每次执行匿名函数都支持3个参数,参数分别是item(当前每一项)、index(索引值)、arr(原数组),需要用哪个的时候就写哪个 匿名函数中的this都是指向window 只能遍历数组
注意:forEach对于空数组是不会调用回调函数的。
不同点
map方法返回一个新的数组,数组中的元素为原始数组调用函数处理后的值。(原数组进行处理之后对应的一个新的数组。) map()方法不会改变原始数组 map()方法不会对空数组进行检测 forEach()方法用于调用数组的每个元素,将元素传给回调函数.(没有return,返回值是undefined)
Async 和 await 是一种同步的写法,但还是异步的操作,两个必须配合一起使用
函数前面的async关键字,表明该函数内部有异步操作。调用该函数时,会立即返回一个Promise对象。
await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西,如果是promise则会等待promaise 返回结果,接普通函数直接进行链式调用.
await 能够获取promise执行的结果 await必须和async一起使用才行,async配合await使用是一个阻塞的异步方法
如果await后面不是Promise对象, 就直接返回对应的值,只能在async函数中出现, 普通函数直接使用会报错
await语句后的Promise对象变成reject状态时,那么整个async函数会中断,后面的程序不会继续执行
使用场景:
我在项目中: 需求:执行第一步,将执行第一步的结果返回给第二步使用。在ajax中先拿到一个接口的返回数据,然后使用第一步返回的数据执行第 二步操作的接口调用,达到异步操作。
1、 推荐在循环对象属性的时候,使用 for…in,在遍历数组的时候的时候使用for…of。
2、 for in遍历的是数组的索引,而for of遍历的是数组元素值
3、for…of 不能循环普通的对象,需要通过和 Object.keys()搭配使用
4、for…in 遍历顺序以数字为先 无法遍历 symbol 属性 可以遍历到公有中可枚举的
5、从遍历对象的角度来说,for···in会遍历出来的为对象的key,但for···of会直接报错。
Generator 生成器 也是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同 function *(){}
Generator 函数是一个状态机,封装了多个内部状态,除了状态机,还是一个遍历器对象生成函数。
Generator 是分段执行的, yield (又得)可暂停,next方法可启动。每次返回的是yield后的表达式结果,这使得Generator函数非常适合将异步任务同步化
Generator 并不是为异步而设计出来的,它还有其他功能(对象迭代、控制输出、部署Interator`接口…)
Generator函数返回Iterator对象,因此我们还可以通过for…of进行遍历,原生对象没有遍历接口,通过Generator函数为它加上这个接口,就能使用for…of进行遍历了
promise和async/await是专门用于处理异步操作的
Generator并不是为异步而设计出来的,它还有其他功能(对象迭代、控制输出、部署Interator接口…)
promise编写代码相比Generator、async更为复杂化,且可读性也稍差
Generator、async需要与promise对象搭配处理异步情况
async实质是Generator的语法糖,相当于会自动执行Generator函数
async使用上更为简洁,将异步代码以同步的形式进行编写,是处理异步编程的最终方案
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
symbol 是es6 加入的,是一个基本数据类型,它代表的是一个独一无二的值,SYMBOL 值是由 SYMBOL函数生成,也就是说现在我们定义对象的属性名字可以是原有的字符串 也可以是 symbol 类型的,symbol 可以保证不与其他属性名冲突,减少了bug的产生,
如果那 symbol 对比的话 就是会返回 false
symbol 他是一个原始类型的值就,不可以使用 new 关键字,symbol不是对象 没有迭代器的接口 不能去添加属性值,他是类似于字符串的一种类型
symbol 不能用来四则运算,否则会报错,只能用显示的方式转为字符串
symbol 参数里的 a 表示一种修饰符 对当前创建的 symbol 的一种修饰,作为区分 ,否则会混淆
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作
Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for…of循环,Iterator 接口主要供for…of消费。
其实iteration == iterator 有三个作用:
为各种数据结构,提供一个统一的、简便的访问接口;
使得数据结构的成员能够按某种次序排列;
主要供for…of消费
我对模块的理解是,一个模块是实现一个特定功能的一组方法。在最开始的时候,js 只实现一些简单的功能,所以并没有模块的概念 ,但随着程序越来越复杂,代码的模块化开发变得越来越重要。
由于函数具有独立作用域的特点,最原始的写法是使用函数来作为模块,几个函数作为一个模块,但是这种方式容易造成全局变量的污 染,并且模块间没有联系。
后面提出了对象写法,通过将函数作为一个对象的方法来实现,这样解决了直接使用函数作为模块的一些缺点,但是这种办法会暴露所 有的所有的模块成员,外部代码可以修改内部属性的值。
现在最常用的是立即执行函数的写法,通过利用闭包来实现模块私有作用域的建立,同时不会对全局作用域造成污染。
js 中现在比较成熟的有四种模块加载方案:
第一种是 CommonJS 方案,它通过 require 来引入模块,通过 module.exports 定义模块的输出接口。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。
第二种是 AMD 方案,这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。require.js 实现了 AMD 规范。
第三种是 CMD 方案,这种方案和 AMD 方案都是为了解决异步模块加载的问题,sea.js 实现了 CMD 规范。它和require.js的区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。
第四种方案是 ES6 提出的方案,使用 import 和 export 的形式来导入导出模块。
MVVM 就是 Model-View-ViewModel 的缩写,MVVM 将视图和业务逻辑分开。
View:视图层,Model 数据模型,而 ViewModel 是把两者建立通信的桥梁。
在 MVVM 框架下,View 和 Model 之间没有直接的联系,而是通过 ViewModel 进行交互。View 和 ViewModel 之间以及 Model 和 ViewModel 之间的交互都是双向的,因此 view 数据的变化会同步到 Model 中,而 Model 数据的变化也会立即反映到 View 上。可以说它们两者是实时更新的,互相影响。 ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而 View 和 Model 之间的同步工作完全是自动的,因此开发者只需要关注业务逻辑,不需要手动操作 DOM,也不需要关注数据状态的同步问题,这些都由 MVVM 统一管理,
整体看来,MVVM比MVC精简很多,不仅简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器操作DOM元素。因为在MVVM中,View不知道Model的存在,Model和ViewModel也观察不到View,这种低耦合模式提高代码的可重用性。
【优点】
数据源和视图实现了双向绑定,很好的做到了数据的一致性 相比于mvp各层的耦合度更低,一个viewmodel层可以给多个view层共用。
【缺点】
因为使用了dataBinding,增加了大量的内存开销,增加了程序的编译时间,项目越大内存开销越大。 数据绑定使得 Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题
MVC 全名是 Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范
【优点】
耦合性低,方便维护,可以利于分工协作 重用性高
【缺点】
使得项目架构变得复杂,对开发人员要求高
MVP MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示,在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的
vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter和getter,在数据变动时发布消息给订阅者,触发相应的监听回调 Vue是一个典型的MVVM框架,模型(Model)只是普通的javascript对象,修改它则-视图(View)会自动更新。这种设计让状态管理变得非常简单而直观
Observer(数据监听器) : Observer的核心是通过Object.defineProprtty()来监听数据的变动,这个函数内部可以定义setter和getter,每当数据发生变化,就会触发setter。这时候Observer就要通知订阅者,订阅者就是Watcher
Compile(指令解析器) : Compile主要做的事情是解析模板指令,将模板中变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加鉴定数据的订阅者,一旦数据有变动,收到通知,更新-视图
Watcher(订阅者) : Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:
在自身实例化时往属性订阅器(dep)里面添加自己
自身必须有一个update()方法
待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调
vue中的模板template无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的HTML语法,所有需要将template转化成一个JavaScript函数,这样浏览器就可以执行这一个函数并渲染出对应的HTML元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。
Vue 的编译过程就是将 template 转化为 render 函数的过程 分为以下三步
第一步是将 模板字符串 转换成 element ASTs(解析器)
第二步是对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)
第三步是 使用 element ASTs 生成 render 函数代码字符串(代码生成器)
其实就是用对象的方式取代真实的 DOM 操作,把真实的 DOM 操作放在内存当中,在内存中的对象里做模拟操作。当页面打开时浏览器会解析 HTML 元素,构建一颗 DOM 树,将状态全部保存起来,在内存当中模拟我们真实的 DOM 操作,操作完后又会生成一颗 dom 树,两颗 DOM 树进行比较,根据 diff 算法比较两颗 DOM 树不同的地方,只渲染一次不同的地方。
(个人理解)虚拟dom他不并不是真实的 dom ,是根据模板生成一个js对象(使用createElement,方法),根据这个js对象再去生成真实的dom,对复杂的文档DOM结构,提供一种方便的工具,进行最小化的DOM操作 ,是可以快速的渲染和高效的更新元素,提高浏览器的性能,
例如,一个 ul 标签下很多个 li 标签,其中只有一个 li 有变化,这种情况下如果使用新的 ul 去替代旧的 ul,因为这些不必要的 DOM 操作而造成了性能上的浪费,但是如果直接使用虚拟节点覆盖旧节点的话,减少了很多的不必要的 DOM 操作。
我们在渲染页面的时候 会对新的虚拟dom和旧的虚拟dom进行对比 只渲染不同的地方,而不再是像之前只要发生变化,全部的真实dom都要重新渲染,所以提高了渲染的效率。
缺点:首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢
diff 算法是一种通过同层的树节点进行比较的高效算法,比较方式:diff整体策略为:深度优先,同层比较
diff算法 当data发生改变 会根据新的数据生成一个新的虚拟dom ,新的虚拟dom和旧的虚拟dom进行对比,这个对比的过程就是diff算法,会找到不同地方,只去渲染不同的地方,总的来说就是减少DOM,重绘和回流。
为什么要用虚拟DOM来描述真实的DOM呢?
创建真实DOM成本比较高,如果用 js对象来描述一个dom节点,成本比较低,另外我们在频繁操作dom是一种比较大的开销。所以建议用虚拟dom来描述真实dom。
是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调从而达到数据和视图同步。
主要分为四部分
Vue3是通过Object.define.proxy 对对象进行代理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为 Proxy 是 ES6 的语法
Vue3.0 摒弃了 Object.defineProperty,改为基于 Proxy 的观察者机制探索。
首先说一下 Object.defineProperty 的缺点:
① Object.defineProperty 无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实施响应。 this.$set()解决
② Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue2.X 里,是通过递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象才是更好的选择。
而要取代它的 Proxy 有以下两个优点
可以劫持整个对象,并返回一个新对象。有多种劫持操作(13 种)
补充:
Proxy 用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。 mdn
Proxy 是 ES6 新增的一个属性,翻译过来的意思就是代理,用在这里表示由它来“代理”某些操作。Proxy 让我们能够以简洁易懂的方式控制外部对象的访问,其功能非常类似于设计模式中的代理模式。
Object.defineProperty 无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实施响应。 this.$set()解决
问题原因:因为 vue 的检查机制在进行视图更新时无法监测 数组中的对象的某个属性值的变化。解决方案如下
方案一:利用 this.set(this.obj,key,val)
例:this.set(this.obj,‘k1’,‘v1’)
方案二:就利用 Object.assign({},this.obj)创建新对象
如果是数组就 Object.assign([],this.obj)
如果是对象就 Object.assign({},this.obj)。
灵活的组件应用,高效的数据绑定
渐进式代表的含义是:主张最少——它是一个轻量级框架,只做了自己该做的事,没有做不该做的事
每个框架都不可避免会有自己的一些特点,从而会对使用者有一定的要求,这些要求就是主张,主张有强有弱,它的强势程度会影响在业务开发中的使用方式。
这里的vue数据驱动的是视图,也就是DOM元素,指的是让DOM的内容随着数据的改变而改变框架的理解
SSR全称Server Side Render
SSR的缺点:
1.性能提升
更小巧,更快速;支持摇树优化。支持 Fragments (支持多个根节点)和跨组件渲染;支持自定义渲染器。
2.API 变动
Vue2使用 选项类型API(Options API) 对比Vue3 合成型API(Composition API)
optionsApi 使用传统api中,新增一个需求,要在data,methods,computed中修改
compositionApi 我们可以更加优雅的组织我们的代码,函数,让我们的代码更加有序的组合在一起
3.重写虚拟 DOM (Virtual DOM Rewrite)
随着虚拟 DOM 重写,减少 运行时(runtime)开销。重写将包括更有效的代码来创建虚拟节点。
vue3 没有了过滤器
4.双向数据绑定 从 Object.defineProperty() 变成了 proxy,通过下标修改数组变化了视图数据没发生变化 this.$set() vue3不需要
双向数据绑定原理发生了改变,使用proxy替换Object.defineProerty
使用Proxy的优势:
- 可直接监听数组类型的数据变
- 监听的目标为对象本身,不需要像Object.defineProperty一样遍历每个属性,有一定的性能提升
- 可直接实现对象属性的新增/删除
setup 函数
3.0新加入了TypeScript以及PWA支持
默认使用懒加载
可以不用加上key
vue3 的watch监听可以进行终止监听
生命周期有了一定的区别 Vue2--------------vue3
beforeCreate -> setup() 开始创建组件之前,创建的是data和method
created -> setup()
beforeMount -> onBeforeMount 组件挂载到节点上之前执行的函数。
mounted -> onMounted 组件挂载完成后执行的函数
beforeUpdate -> onBeforeUpdate 组件更新之前执行的函数。
updated -> onUpdated 组件更新完成之后执行的函数。
beforeDestroy -> onBeforeUnmount 组件挂载到节点上之前执行的函数。
destroyed -> onUnmounted 组件卸载之前执行的函数。
activated -> onActivated 组件卸载完成后执行的函数
deactivated -> onDeactivated
相同点 :
都是使用了虚拟dom
组件化开发
父子之间通信单项数据流
都支持服务端渲染
不同点:
reacct 的jsx vue的是 template
数据变化,react 手动 setState vue自动响应式处理 proxy object.DefineProperty
react 单向数据流 ,vue双向数据流
react 的 redux mobx vue 的vuex 。pinia’
beforeCreate() 创建前,这个时候data中的数据,还未定义,所以不能使用
created()创建后 最早开始使用 data和methods中数据的钩子函数
beforeMount()挂载前 指令已经解析完毕内存中已经生成dom树,但是尚未挂载到页面中去,此时页面还是旧的。
mounted()挂载后 dom已经渲染完毕,此时页面和内存中都是最新的数据,最早可以操作DOM元素钩子函数
beforeUpdate()更新前 当视图层的数据发生改变会执行这个钩子 内存更新,但是DOM节点还未更新,数据没有与页面同步
updated()更新后 数据更新完成以后触发的方法,DOM节点已经更新
beforeDestroy()即将销毁 data和methods中的数据此时还是可以使用的,可以做一些释放内存的操作
destroyed()销毁完毕 组件已经全部销毁,Vue实例已经被销毁,Vue中的任何数据都不可用
其他三个:
activated 被 keep-alive 缓存的组件激活时调用。
deactivated 被 keep-alive 缓存的组件停用时调用。
errorCaptured 2.5.0+ 新增当捕获一个来自子孙组件的错误时被调用
Vue3.0中的生命周期做了一些改动:
beforeCreate -> setup() 开始创建组件之前,创建的是data和method
created -> setup()
beforeMount -> onBeforeMount 组件挂载到节点上之前执行的函数。
mounted -> onMounted 组件挂载完成后执行的函数
beforeUpdate -> onBeforeUpdate 组件更新之前执行的函数。
Update - > onUpdated组件更新完成之后执行的函数。
beforeDestroy -> onBeforeUnmount 组件挂载到节点上之前执行的函数。
destroyed -> onUnmounted 组件卸载之前执行的函数。
created 模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。
mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。
this. n e x t T i c k ( ) 将回调延迟到下次 D O M 更新循环之后执行。在修改数据之后立即使用它,然后等待 D O M 更新。它跟全局方法 V u e . n e x t T i c k 一样,不同的是回调的 t h i s 自动绑定到调用它的实例上。可以根据打印的顺序看到,在 c r e a t e d ( ) 钩子函数执行的时候 D O M 其实并未进行任何渲染,而此时进行 D O M 操作并无作用,而在 c r e a t e d ( ) 里使用 t h i s . nextTick()将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。 可以根据打印的顺序看到,在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作并无作用,而在created()里使用this. nextTick()将回调延迟到下次DOM更新循环之后执行。在修改数据之后立即使用它,然后等待DOM更新。它跟全局方法Vue.nextTick一样,不同的是回调的this自动绑定到调用它的实例上。可以根据打印的顺序看到,在created()钩子函数执行的时候DOM其实并未进行任何渲染,而此时进行DOM操作并无作用,而在created()里使用this.nextTick()可以等待dom生成以后再来获取dom对象,而通过this.$nextTick()获取到的值为dom更新之后的值
setTimeout(() => {
console.log(this.$refs.button);
});
setTimeout 将同步转换为异步 this.$nextTick
this.$nextTick 将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,
优先是Promise.then方法,是个微任务,这样可以避免多一次队列,进而少一次UI渲染,节省性能
页面a----beforeCreate undefined
页面a----created 1
页面a----beforeMount 1
页面a----mounted 1
页面b----beforeCreate undefined
页面b----created 1
页面b----beforeMount 1
页面a----beforeDestroy 1
页面a----destroyed 1
页面b----mounted 1
加载渲染过程
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
代码更新过程
父beforeUpdate->子beforeUpdate->子updated->父updated
代码销毁过程
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
代码常用钩子简易版
父create->子created->子mounted->父mounted
activated, deactivated 是组件keep-alive时独有的钩子
beforeCreate
created
beforeMount
mounted
beforeUpdate
updated
activated
deactivated
beforeDestroy
destroyed
errorCaptured
仅仅是数据发生改变的时候会侦听到;
只是会检测到你写在watch里的那些属性,没写的就不会触发。
执行到它的时候时候是数据发生变化且界面更新完毕;
不能监听到路由数据(例如网址中的参数);
所有的数据发生变化都会调用(消耗性能);
每次触发的代码都是同一个
1、监控自己定义的变量,不用再data里面声明,函数名就是变量名
2、适合多个变量或对象进行处理后返回一个值(结果)。若这多个变量发生只要有一个发生变化,结果都会变化。
3、计算的结果具有缓存,依赖响应式属性变化,响应式属性没有变化,直接从缓存中读取结果。
4、在内部函数调用的时候不用加()。
5、必须用return返回
6、不要在computed 中对data中的数据进行赋值操作,这会形成一个死循环。
methods不会被缓存:方法每次都会去重新计算结果。methods 方法表示一个具体的操作,主要书写业务逻辑;
使用 methods 方法编写的逻辑运算,在调用时 add() 一定要加“()”,methods 里面写的多位方法,调用方法一定要有()。methods方法页面刚加载时调用一次,以后只有被调用的时候才会被调用。我们在长度框和宽度框的值输入完以后,点击“+” methods 方法调用一次。这里很明显我们采用 methods 会更节省资源。
watch:
1、watch 函数是不需要调用的。
2、重点在于监控,监控数据发生变化的时候,执行回调函数操作。
3、当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch
4、函数名就是你要监听的数据名字
5、监控一些input框值的特殊处理,适合一个数据影响多个数据。
6、数据变化时,执行一些异步操作,或开销比较大的操作
computed:
在模板中放入太多的逻辑会让模板过重且难以维护,在需要对数据进行复杂处理,且可能多次使用的情况下,尽量采取计算属性的方式
一个需要的结果受多个数据影响的时候,比如购物车结算金额(受到很多处的价格结算)。
操作某个属性,执行一些复杂的逻辑,并在多处使用这个结果。
内部函数中多处要使用到这个结果的。
1、监控自己定义的变量,不用再data里面声明,函数名就是变量名
2、适合多个变量或对象进行处理后返回一个值(结果)。若这多个变量发生只要有一个发生变化,结果都会变化。
3、计算的结果具有缓存,依赖响应式属性变化,响应式属性没有变化,直接从缓存中读取结果。
4、在内部函数调用的时候不用加()。
5、必须用return返回
6、不要在computed 中对data中的数据进行赋值操作,这会形成一个死循环。
可以在钩子函数中的 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
在created中最好
能更快获取到服务端数据,减少页面加载时间,用户体验更好;
SSR不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性。
mounted 在请求完数据之后需要对 dom 进行操作的时候可以用到
computed 是vue中的计算属性,具有缓存性,当他的依赖于值,发生改变的时候才会重新调用
methods 是没有缓存的,只要调用,就会执行,一般结合事件来使用
watch 没有缓存性 监听data中的属性 属性值只要发生变化就会执行 可以利用他的特性做一些异步的操作
created:dom渲染前调用,即通常初始化某些属性值
mounted:在dom渲染后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作
Vue 的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)
开启严格模式,仅需在创建 store 的时候传入 strict: true:
const store = new Vuex.Store({
// …
strict: true
})
在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。
不要在发布环境下启用严格模式!严格模式会深度监测状态树来检测不合规的状态变更——请确保在发布环境下关闭严格模式,以避免性能损失。
类似于插件,我们可以让构建工具来处理这种情况:
const store = createStore({
// …
strict: process.env.NODE_ENV !== ‘production’
})
vuex是一个状态管理工具,所谓状态的是就是数据,采用集中式存储管所有组件的状态,是为了结局中大型项目一个数据共享的问题。vuex 他可以将数据保存到本地,数据是响应式的,能够保持数据页面的共享,提高开发效率。
好处:
能够在 vuex 中集中管理共享的数据,易于开发和后期维护 可以做状态管理、采用localstorage保存信息、数据一直存储在用户的客户端中 存储在 vuex 中的数据都是响应式的,能够实时保持数据与页面的同步,能够高效地实现组件之间的数据共享,提高开发 效率
vuex核心:
state:vuex的基本数据,数据源存放地,用于定义共享的数据。
getter:从基本数据派生的数据,相当于state的计算属性
mutation:提交更新数据的方法,唯一 一个可以操作state 中数据的方法,必须是同步的,第一个参数是state,第二个参数是cmmi传过来的数据
action:action是用来做异步操作的,一般用来发请求,在 action 中写入函数,然后在页面中用dispatch调用,然后在 action 中通过commit 去调用 mutation 通过 mutation 去操作state。
modules:模块化vuex,可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理
运行机制:Vuex提供数据(state),来驱动视图(这里指的是Vue组件),视图通过Dispatch派发Action,在Action中可以进一步做一些异步的操作(例如通过ajax请求后端的接口数据),然后通过Commit提交给Mutations,由Mutations去最终更改state。那么为什么要经过Mutations呢?这是因为我们要在Vue调试工具(Devtools)中记录数据的变化,这样可以通过插件去进行进一步的调试。所以说Mutations中只能是纯同步的操作,如果是有异步操作,那么就需要在Actions中进行处理。如果说没有异步操作,那么可以直接由组件进行Commit操作Mutations。
mapState,mapActions,mapMutations,mapGetters
辅助函数可以把vuex中的数据和方法映射到vue组件中。达到简化操作的目的
如何使用:
Import { mapActions, mapGetters, mapMutations, mapState } from ‘vuex’
computed(){ …mapState([‘数据名字’])}
需要做 vuex 数据持久化 一般使用本地存储的方案来保存数据 可以自己设计存储方案 也可以使用第三方插件
推荐使用 vuex-persist 插件,它就是为 Vuex 持久化存储而生的一个插件。不需要你手动存取 storage ,而是直接将状态保存至 cookie 或者 localStorage 中
模块:由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。方便管理
由于Vue在开发时对路由支持的不足,于是官方补充了vue-router插件。vue的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来。传统的页面应用,是用一些超链接来实现页面切换和跳转的。在vue-router单页面应用中,则是路径之间的切换,实际上就是组件的切换。路由就是SPA(单页应用)的路径管理器。再通俗的说,vue-router就是我们WebApp的链接路径管理系统。
原理 一般源码中,都会用到 window.history 和 location.hash 原理:通过改变浏览器地址URL,在不重新请求页面的情况下,更新页面视图,通过BOM中的location对象,其中对象中的location.hash储存的是路由的地址、可以赋值改变其URL的地址。而这会触发hashchange事件,而通过window.addEventListener监听hash值然后去匹配对应的路由、从而渲染页面的组件
1.一种是# hash,在地址中加入#以欺骗浏览器,地址的改变是由于正在进行页内导航
2.一种是h5的history,使用URL的Hash来模拟一个完整的URL
路由有两种模式 hash和history模式 默认是hash
1、hash ——即地址栏 URL 中的#符号,它的特点在 于:hash 虽然出现 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影 响,因此改变 hash 不会重新加载页面。
2、history ——利用了 HTML5 History api 在浏览器中没有# 有浏览器兼容问题
3、history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,否则返回 404 错误。
无论访问哪一个路径,都会触发全局的钩子函数,位置是调用router的方法 router/index.js
router.beforeEach 全局前置守卫 进入路由之前
router.beforeResolve 全局解析守卫,在beforeRouteEnter调用之后调用
同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被正确调用
router.afterEach 全局后置钩子 进入路由之后
你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:
beforeRouteEnter 进入路由前,此时实例还没创建,无法获取到zhis
beforeRouteUpdate (2.2) 路由复用同一个组件时
beforeRouteLeave 离开当前路由,此时可以用来保存数据,或数据初始化,或关闭定时器等等
//在组件内部进行配置,这里的函数用法也是和beforeEach一毛一样
const Foo = {
template: ...
,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 this
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 this
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 this
}
}
beforeEnter:(to,from,next)=>{ alert(“欢迎来到孙志豪的界面”) next() }
参数
to: Route: 即将要进入的目标 路由对象
from: Route: 当前导航正要离开的路由对象
next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
redirect:”/路径”
1、 2、this.$router.push()跳转到指定的url,并在history中添加记录,点击回退返回到上一个页面
3、this.$router.replace()跳转到指定的url,但是history中不会添加记录,点击回退到上上个页面
4、this.$touter.go(n)向前或者后跳转n个页面,n可以是正数也可以是负数
router.push:跳转,并向history栈中加一个记录,可以后退到上一个页面
router.replace:跳转,不会向history栈中加一个记录,不可以后退到上一个页面
router.go:传正数向前跳转,传负数向后跳转
router.back 返回到上一级页面
主要通过 query 和 params 来实现
(1) query可以使用name和path而params只能使用name
(2) 使用params传参刷新后不会保存,而query传参刷新后可以保存
(3) Params在地址栏中不会显示,query会显示
(4) Params可以和动态路由一起使用,query不可以
(5)to=”/goods?id=1001”this.然后在接收的页面通过 $route.query.id 来接收
route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数。
router 是“路由实例对象”,包括了路由的跳转方法(push、go),钩子函数等。
全局前置守卫beforeEach (路由器实例内的前置守卫)
路由独享守卫beforeEnter(激活的路由)
组件内守卫beforeRouteEnter(渲染的组件)
全局解析守卫beforeResolve(路由器实例内的解析守卫)
全局后置钩子afterEach(路由器实例内的后置钩子)
组件内守卫beforeRouteLeave(即将离开的组件)
全局前置守卫beforeEach (路由器实例内的前置守卫)
组件内守卫beforeRouteEnter(渲染的组件)
全局解析守卫beforeResolve(路由器实例内的解析守卫)
全局后置钩子afterEach(路由器实例内的后置钩子)
完整的导航解析流程
导航被触发。
在失活的组件里调用 beforeRouteLeave 守卫。
调用全局的 beforeEach 守卫。
在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
在路由配置里调用 beforeEnter。
解析异步路由组件。
在被激活的组件里调用 beforeRouteEnter。
调用全局的 beforeResolve 守卫 (2.5+)。
导航被确认。
调用全局的 afterEach 钩子。
触发 DOM 更新。
调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
动态路由是指路由器能够自动的建立自己的路由表,能够根据实际情况的变化实时地进行调整。用开头,后面跟的值是不确定的。这个值是我们要传递的参数 动态路由匹配本质上就是通过url进行传参
比如在写一个商品详情页面的时候,我们的页面结构都一样,只是渲染的数据不同而已,这时候就可以根据商品的不同id去设置动态路由,只需要写一个组件,就可以把每个商品的商品详情映射到同一个组件上去。
{
path: '/Jqxq/:id', // 路由配置拼接
name: 'Jqxq',
component: Jqxq
}
跳转 this.$router.push('/Jqxq/'+ item.id)
接收 : this.$route.params.id
vue项目中,界面通常由多个嵌套的组件构成, 必须先清楚这样一件事,一个对应展示的就是一个组件 因此实现嵌套路由有两个要点: 路由对象中定义子路由 用children实现嵌套路由 组件内的使用.
export default new Router({
mode: ‘history’, //路由模式,取值为history与hash
base: ‘/’, //打包路径,默认为/,可以修改
routes: [
{
path: string, //路径
ccomponent: Component; //页面组件
name: string; // 命名路由-路由名称
components: { [name: string]: Component }; // 命名视图组件
redirect: string | Location | Function; // 重定向
props: boolean | string | Function; // 路由组件传递参数
alias: string | Array; // 路由别名
children: Array; // 嵌套子路由
// 路由单独钩子
beforeEnter?: (to: Route, from: Route, next: Function) => void;
meta: any; // 自定义标签属性,比如:是否需要登录
icon: any; // 图标
// 2.6.0+
caseSensitive: boolean; // 匹配规则是否大小写敏感?(默认值:false)
pathToRegexpOptions: Object; // 编译正则的选项
}
]
})
在 router 目录下的 index.js 文件中,对 path 属性加上 /:id,使用 route 对象的 params.id 获取
有两种模式 hash和history模式 默认是hash
1、hash ——即地址栏 URL 中的#符号,它的特点在 于:hash 虽然出现 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影 响,因此改变 hash 不会重新加载页面。
2、history ——利用了 HTML5 History api 在浏览器中没有# 有浏览器兼容问题
history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 地址后加上/items/id。后端如果缺少对 /items/id 的路由处理,将返回 404 错误。
使用原因:在单页应用中,如果没有应用懒加载,运用 webpack 打包后的文件将会异常的大,造成进入首页时,需要加载的内容过多,延时过长,不利于用户体验,而运用懒加载则可以将页面进行划分,需要的时候加载页面,可以有效的分担首页所承担的加载压力,减少首页加载用时 原理:vue 异步组件技术:异步加载,vue-router 配置路由 , 使用 vue 的异步组件技术 , 实现按需加载。
{ path: ‘/home’, component: () => import(‘@/views/home/home.vue’) } // 懒加载
location.hash 的值实际就是 URL 中#后面的东西 它的特点在于:hash 虽然出现 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。
可以为 hash 的改变添加监听事件
window.addEventListener(“hashchange”, funcRef, false);
每一次改变 hash(window.location.hash),都会在浏览器的访问历史中增加一个记录利用 hash 的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了
特点:兼容性好但是不美观
利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。
这两个方法应用于浏览器的历史记录站,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。
.stop 阻止事件冒泡
.cpture 设置事件捕获
.self 只有当事件作用在元素本身才会触发
.prevent 阻止默认事件,比如超链接跳转
.once 事件只能触发一次
.native 触发js原生的事件
.number 把文本框的内容转换为数字
.trim 去除文本框左右空格
⑴v-bind:给元素绑定属性
⑵v-on:给元素绑定事件
⑶v-html:给元素绑定数据,且该指令可以解析 html 标签
⑷v-text:给元素绑定数据,不解析标签
⑸v-model:数据双向绑定
⑹v-for:遍历数组
⑺v-if:条件渲染指令,动态在 DOM 内添加或删除 DOM 元素
⑻v-else:条件渲染指令,必须跟 v-if 成对使用
⑼v-else-if:判断多层条件,必须跟 v-if 成对使用
⑽v-cloak:解决插值闪烁问题
⑾v-once:只渲染元素或组件一次
⑿v-pre:跳过这个元素以及子元素的编译过程,以此来加快整个项目的编译速度
⒀v-show:条件渲染指令,将不符合条件的数据隐藏(display:none)
v-for 比 v-if 优先,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候。
“key 值:用于管理可复用的元素。因为 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。这么做使 Vue 变得非常快,但是这样也不总是符合实际需求。 2.2.0+ 的版本里,当在组件中使用 v-for 时,key 是必须的。”
key是给每一个vnode的唯一id,也是diff的一种优化策略,可以根据key,更准确, 更快的找到对应的vnode节点,更高效的对比虚拟DOM中每个节点是否是相同节点,相同就复用,不相同就删除旧的创建新的
当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。key 的作用主要是为了高效的更新虚拟 DOM。
举例子:加入写一个带有复选框的列表
选中第一个节点的复选框,点击删除,vue中是这样操作的,删除后新的数据这时会进行比较,第一个节点的标签一样,值不一样,就会复用原来位置的标签,不会做删除和创建,在第一个节点中是将复选框选中的,当我们看见好像是把第一个删除了,但是点击后去看复选框的时候还是选中在第一个,如果是直接将第一个节点删除了那么复选框就不会选中。
能够解决插值表达式闪烁问题,需要在style中设置样式[v-clock]{display:none}
v-if 动态的创建或者销毁元素,为true的时候为显示,为false的时候不显示,要使用v-else必须和v-if紧挨着
v-show 是控制元素的显示或者隐藏,在我们的标签上会看到有display:block,none
v-if 有更高的切换消耗,而 v-show 有更高的初始化渲染消耗,一般推荐频繁切换的时候使用 v-show 更好,当我们的判断分支比较多的时候,和首次渲染的时候 使用v-if
出了vue自带的指定以外,我们如果需要对dom进行底层操作的时候这里就用到了自定义指令,分为一下
全局: vue.directive:{“”,{}} 局部:directives:{指令名:{钩子函数}}
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind:只调用一次,指令与元素解绑时调用。
参数:
el:指令所绑定的元素
binding:一个对象包含一下,
name:指令名,不包括 v- 前缀。
value:指令的绑定值
created - 自定义指令所在组件, 创建后
beforeMount - 就是Vue2.x中的 bind, 自定义指令绑定到 DOM 后调用. 只调用一次, 注意: 只是加入进了DOM, 但是渲染没有完成
mounted - 就是Vue2.x中的 inserted, 自定义指令所在DOM, 插入到父 DOM 后调用, 渲染已完成(最最重要)
beforeUpdate - 自定义指令所在 DOM, 更新之前调用
updated - 就是Vue2.x中的 componentUpdated
beforeUnmount - 销毁前
unmounted - 销毁后
1.在生成 ast 语法树时,遇到指令会给当前元素添加 directives 属性
2.通过 genDirectives 生成指令代码
3.在 patch 前将指令的钩子提取到 cbs 中,在 patch 过程中调用对应的钩子
4.当执行指令对应钩子函数时,调用对应指令定义的方法
过滤器是对 即将显示的数据做进一步的筛选处理,然后显示,过滤器并没有改变原来的数据,只是在原数据的基础上产生新的数据
全局:
Vue.filter(‘过滤器名’,funciton(val){})
局部过滤器,定义在组件内部 filters 属性上.它只能在此组件内部使用.
filters:{过滤器名:funciton(参数){//逻辑代码}}
使用: 过滤时间,过滤金钱
Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。 mixin 项目变得复杂的时候,多个组件间有重复的逻辑就会用到mixin
如果希望在多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以将其编写为 mixin,并在组件中简单的引用它。
然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。
在vue.js中mixin和页面执行顺序问题
mixin中的代码先执行,单文件中的后执行。
mixin的beforeCreate > 父beforeCreate > mixin的created > 父created > mixin的beforeMount > 父beforeMount > 子beforeCreate > 子created > 子beforeMount > 子mounted > mixin的mounted >父mounted
在下次DOM更新循环结束后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。使用场景是:可以在created钩子函数中拿到dom节点
nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法
删除数组
delete只是把数组元素的值变成empty/undefined,元素的键不变,数组长度不变。
Vue.delete直接删除数组,改变数组的键值和长度。
删除对象
两者相同,都会把键名(属性/字段)和键值删除。
Vue.extend 作用和原理
官方解释:Vue.extend 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
其实就是一个子类构造器 是 Vue 组件的核心 api 实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并 基础用法
/
/ 创建构造器 /* Vue.extend( options ) 参数:{Object} options 用法:使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象; data 选项是特例,需要注意: 在 Vue.extend() 中它必须是函数;*/ var Profile = Vue.extend({ template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>', data: function () { return { firstName: 'Walter', lastName: 'White', alias: 'Heisenberg' } } }) // 创建 Profile 实例,并挂载到一个元素上。 new Profile().$mount('#mount-point') // 结果如下: <p>Walter White aka Heisenberg</p> /* 可以看到,extend 创建的是 Vue 构造器,而不是我们平时常写的组件实例,所以不可以通过 new Vue({ components: testExtend }) 来直接使用,需要通过 new Profile().$mount(’#mount-point’) 来挂载到指定的元素上。 */
(1)在父组件的子组件标签上绑定一个属性,挂载要传输的变量 (2)在子组件中通过props来接受数据,props可以是数组也可以是对象,接受的数据可以直接使用 props: [“属性名”] props:{属性名:数据类型}
(1)在父组件的子组件标签上自定义一个事件,然后调用需要的方法 (2)在子组件的方法中通过 this.$emit(“事件”)来触发在父组件中定义的事件,数据是以参数的形式进行传递的
(1)找到min.js文件,给他vue挂载一个 公共的 b u s V u e . p r o t o t y p e . bus Vue.prototype. busVue.prototype.bus = new Vue() (2)传送数据的一方 用this. b u s . bus. bus.emit(‘事件名’,‘传送的数据’) (3)在 接收数据的一方用通过 Bus.$on(“事件名”,(data)=>{data是接受的数据})
prop 验证,和默认值
props:会接收不同的数据类型,常用的数据类型的设置默认值的写法,Number, String, Boolean, Array, Function, Object
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级prop 的更新流动到子组件中,但是反过来则不行。这样防止子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。若果在子组件中直接修改prop传递的值,Vue会发出警告,
具体实现:vuex 是一个状态管理工具,主要解决大中型复杂项目的数据共享问题,主要包括 state,actions,mutations,getters 和 modules 5 个要素,主要流程:组件通过 dispatch 到 actions,actions 是异步操作,再 actions中通过 commit 到 mutations,mutations 再通过逻辑操作改变 state,从而同步到组件,更新其数据状态
① 项目使用 keep-alive 时,可搭配组件的 name 进行缓存过滤。 ② DOM 做递归组件时需要调用自身 name ③ vue-devtools 调试工具里显示的组件名称是由 vue 中组件 name 决定的
自定义组件
在vue 中的 component 中新建组件,定义好视图层,
在vue中开发,都是用的组件化的思想开发的,组件封装的方式可以使我们的开发效率提高,把单页面的每个模块拆分为一个组件件,
组件封装的方式解决了我们传统项目,开发效率低,难以维护,复用性低等问题。
使用:比如说封装一个 swiper 首先我们要定义一个props要接受传递的数据,写入响应的逻辑,在通过import引入到页面注册作为标签使用即可。
keep-alive是Vue提供给我们一个内置组件,会缓存不活动的组件实例,而不是销毁它们, 作为标签使用 包裹在需要缓存的组件外
在组件切换过程中 把切换出去的组件保留在内存中,防止重复渲染DOM,减少加载时间及性能消耗,提高用户体验性
作用: 比如列表页面进入详情,我们想保存列表滚动的位置,我们就可以使用keep-alive保存列表页面的滚动位置。
组件使用keep-alive以后会新增两个生命周期 actived() deactived()
activated(组件激活时使用) 与 deactivated(组价离开时调用)
有两个参数: 允许组件有条件的进行缓存。
include - 包裹的组件名会被缓存
exclude 包裹的组件名都不会被缓存
首先,答案是不会的,准确的说是不会直接调用。 默认情况下,也就是没有设置keep-alive,当离开当前路由时,会直接调用beforeDestroy和destroyed来销毁。 当组件设置keep-alive后,不会直接调用这个销毁周期函数,而是会在生命周期函数新增两个,activated和deactivated; 当退出的时候会执行deactivated 函数
Object是引用数据类型,如果不用function返回,每个组件的data都是内存的同一个地址,一个数据改变了其他也改变了,这就造成了数据污染。如果data是一个函数,每个实例的data都在闭包中,就不会各自影响了
组件特性及好处、组件的基本组成
(1) 特性:重用性、可指定性、互操作性、高内聚性、低耦合度
(2) 好处:组件可以扩展HTML元素、封装可重用代码
template 结构(html代码)
script行为
style样式
插槽就是父组件往子组件中插入一些内容。
有三种方式,默认插槽,具名插槽,作用域插槽
默认插槽就是把父组件中的数据,显示在子组件中,子组件通过一个slot插槽标签显示父组件中的数据
具名插槽是在父组件中通过slot属性,给插槽命名,在子组件中通过slot标签,根据定义好的名字填充到对应的位置。这样就可以指定多个可区分的slot,在使用组件时灵活地进行插值。
作用域插槽是带数据的插槽,子组件提供给父组件的参数,父组件根据子组件传过来的插槽数据来进行不同的展现和填充内容。在标签中通过v-slot=""要穿过来的数据“来接受数据。
vue 中的 scoped 通过在 DOM 结构以及 css 样式上加唯一不重复的标记:data-v-hash 的方式,以保证唯一(通过 PostCSS 转译),达到样式私有模块化的目的。
scoped 的 3 条渲染规则: ① 给 HTML 的 DOM 节点加一个不重复的 data 属性,来表示它的唯一性; ② 在每句 css 选择器末尾(编译后的生成的 css 语句)加一个当前组件的 data 属性选择器来私有化样式; ③ 如果组件内部包含有其他组件,只会给其他组件的最外层标签加上 ddan 当前组件的 data 属性。 补充:
在做项目中,会遇到这么一个问题,即:引用了第三方组件,需要在组件中局部修改第三方组件的样式,而又不想去除scoped属性造成组件之间的样式污染。那么有哪些解决办法呢?
①不使用scopeds省略(不推荐);
② 在模板中使用两次style标签。
③scoped穿透:/deep/ >>>
1.函数式组件需要在声明组件是指定 functional:true
2.不需要实例化,所以没有this,this通过render函数的第二个参数context来代替
3.没有生命周期钩子函数,不能使用计算属性,watch
4.不能通过
e
m
i
t
对外暴露事件,调用事件只能通过
c
o
n
t
e
x
t
.
l
i
s
t
e
n
e
r
s
.
c
l
i
c
k
的方式调用外部传入的事件
5.
因为函数式组件是没有实例化的,所以在外部通过
r
e
f
去引用组件时,实际引用的是
H
T
M
L
E
l
e
m
e
n
t
6.
函数式组件的
p
r
o
p
s
可以不用显示声明,所以没有在
p
r
o
p
s
里面声明的属性都会被自动隐式解析为
p
r
o
p
,
而普通组件所有未声明的属性都解析到
emit 对外暴露事件,调用事件只能通过context.listeners.click的方式调用外部传入的事件 5.因为函数式组件是没有实例化的,所以在外部通过ref去引用组件时,实际引用的是HTMLElement 6.函数式组件的props可以不用显示声明,所以没有在props里面声明的属性都会被自动隐式解析为prop,而普通组件所有未声明的属性都解析到
emit对外暴露事件,调用事件只能通过context.listeners.click的方式调用外部传入的事件5.因为函数式组件是没有实例化的,所以在外部通过ref去引用组件时,实际引用的是HTMLElement6.函数式组件的props可以不用显示声明,所以没有在props里面声明的属性都会被自动隐式解析为prop,而普通组件所有未声明的属性都解析到attrs里面,并自动挂载到组件根元素上面(可以通过inheritAttrs属性禁止)
优点 1.由于函数式组件不需要实例化,无状态,没有生命周期,所以渲染性能要好于普通组件 2.函数式组件结构比较简单,代码结构更清晰
一个简单的展示组件,作为容器组件使用 比如 router-view 就是一个函数式组件
“高阶组件”——用于接收一个组件作为参数,返回一个被包装过的组件
单页面:只有一个html页面,跳转方式是组件之间的切换
优点:跳转流畅、组件化开发、组件可复用、开发便捷
缺点:首屏加载过慢
多页面:有多个页面,跳转方式是页面之间的跳转
优点:首屏加载块
缺点:跳转速度慢
为什么要使用脚手架
快速开始一个vue项目,不用手动配置,直接开发
首先要安装axios,一般我会在项目的src目录中,新建一个network文件夹,作为我们的网络请求模块,然后在里面新建一个http.js和一个api.js文件和一个reques.js。
http.js文件用来封装我们的axios basUrl Tiemout,
api.js用来统一管理我们的接口url,
request.js中添加请求拦截和响应拦截。在请求拦截中,会给请求头添加token字段,还有loading动画的开启。在响应拦截中,可以做一些loading动画的关闭,还有可以根据后端返回的状态码,做一些检验token是否有效或者过期的操作。
接着就是做一些axios进行的api接口的封装,这里我用到了async,await封装请求接口函数,这样可以将异步操作同步化操作,代码更加友好,避免回调地域的出现。
在vue开发中实现跨域:在vue项目根目录下找到vue.config.js文件(如果没有该文件则自己创建),在proxy中设置跨域
devServer: {
proxy: { //配置跨域
'/api': {
target: 'http://121.121.67.254:8185/', //这里后台的地址模拟的;应该填写你们真实的后台接口
changOrigin: true, //允许跨域
pathRewrite: {
/* 重写路径,当我们在浏览器中看到请求的地址为:http://localhost:8080/api/core/getData/userInfo 时
实际上访问的地址是:http://121.121.67.254:8185/core/getData/userInfo,因为重写了 /api
*/
'^/api': ''
}
},
}
}
assets中的文件会经过webpack打包,重新编译,推荐在assets存放js等需要打包编译的文件。
static中的文件,不会打包编译。static中的文件只是复制一遍。static中建议放一些外部第三方文件,自己的放assets里,别人的放static中。(图片推荐放在static里)
不会立即同步执行重新渲染。Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环tick中,Vue 刷新队列并执行实际(已去重的)工作。
首先是通过在根目录下创建.env.*(配置文件)文件,development 本地开发环境配置、staging 测试环境配置、production 正式环境配置(生产环境)。因为我在创建的文件中并没有定义很多变量,只定义了基础的env,所以需要在src目录下创建一个config文件夹,创建对应的环境变量文件,用来管理不同的环境。在config中创建对应的文件是为了后期修改起来方便,不需要重启项目,符合开发习惯。之后就是根据需要的环境,在封装的axios中通过解构赋值的方式导入,放在baseURL中就可以使用。
首先安装按需引入的插件,在babel.config.js中添加按需引入的配置,创建一个plugin文件夹,定义一个js文件用来存放按需引入的代码,之后在建好的js文件中首先导入vue,再导入需要的vant-ui插件,通过vue.use()全局注入。修改样式可以用样式穿透 /deep/
① 虚拟 dom:dom 操作时非常耗性能的,不再使用原生的 dom 操作节点,极大的解放 dom 操作,但具体操作的还是 dom,不过是换了一种方式。提供了很多指令当然需要 对 dom 进行底层操作的时候就用到自定义指令
② 视图、数据、结构分离:使数据的更改更为简单,只需要操作数据就能完成相关操作。
③ 组件化:把一个单页应用中的各种模块拆分到一个一个单独的组件中,便于开发,以及后期的维护
简洁:页面由 HTML 模板+Json 数据+Vue 实例组成 数据驱动:自动计算属性和追踪依赖的模板表达式
组件化:用可复用、解耦的组件来构造页面 轻量:代码量小,不依赖其他库 快速:精确有效批量 DOM 更新 模板友好:可通过 npm,bower 等多种方式安装,很容易融入 Vue 的核心库只关注视图层,并且非常容易学习
assets 文件夹是放静态资源;
components 是放组件;
router 是定义路由相关的配置;
view 视图;
app.vue 是一个应用主组件;
main.js 是入口文件
当 Vue 进入初始化阶段时,一方面 Vue 会遍历 data 中的属性,并用 Object.defineProperty 将它转化成 getter/setterd 的形式,实现数据劫持;
另一方面,Vue 的指令编译器 Compiler 对元素节点的各个指令进行解析,初始化视图,并订阅 Watcher 来更新视图,此时 Watcher 会将自己添加到消息订阅器 Dep 中,此时初始化完毕。
当数据发生变化时,触发 Observer 中 setter 方法,立即调用 Dep.notify( ),Dep 这个数组开始遍历所有的订阅者,并调用其 update 方法,Vue 内部再通过 diff 算法,patch 相应的更新完成对订阅者视图的改变。
使用 babel-polyfill 插件,和前缀名 hack
使用 Object.assign(),vm.data 可 以 获 取 当 前 状 态 下 的 data ,
Object.assign(this. d a t a , t h i s . data, this. data,this.options.data())
vue-router的登陆权限判断主要是在全局钩子函数中进行的,我们在router.js文件中的定义路由里,将需要登陆权限的页面加上meta属性,值是对象的形式,然后在该对象中自定义一个属性,属性值就是一个Boolean值,这时候在main.js文件的全局钩子函数中进行判断,如果需要跳转的页面的自定义属性值为true,那么将进行判断其是否登录,如果没有登录,则告诉用户登录,如果有登录,那么进行页面跳转。
vue-cli 是基于 Vue.js 进行快速开发的完整系统,也可以理解成是很多 npm 包的集合。
.vue 文件 --> .js 文件 ES6 语法 --> ES5 语法 Sass,Less,Stylus --> CSS 对 jpg,png,font 等静态资源的处理 热更新 定义环境变量,区分 dev 和 production 模式 如果开发者需要补充或修改默认设置,需要在 package.json 同级下新建一个 vue.config.js 文件
数组考虑性能原因没有用 defineProperty 对数组的每一项进行拦截,而是选择对 7 种数组(push,shift,pop,splice,unshift,sort,reverse)方法进行重写(AOP 切片思想)
所以在 Vue 中修改,数组的索引和长度是无法监控到的。需要通过以上 7 种变异方法修改数组才会触发数组对应的 watcher 进行更新
1.工厂模式 - 传入参数即可创建实例 虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
2.单例模式 - 整个程序有且仅有一个实例 vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
3.发布-订阅模式 (vue 事件机制)
4.观察者模式 (响应式数据原理)
5.策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略
① 把不常改变的库放到 index.html 中,通过 cdn 引入
然后找到 build/webpack.base.conf.js 文件,在 module.exports = { } 中添加以下代码:
externals: {
‘vue’: ‘Vue’,
‘vue-router’: ‘VueRouter’,
‘element-ui’: ‘ELEMENT’,
}
②vue 路由懒加载,图片懒加载,使用异步组件,按需加载
③ 不生成 map 文件,找到 config/index.js 文件,修改为 productionSourcceMap:false
④vue 组件尽量不要全局引入
⑤ 使用更轻量级的工具库
⑥ 开启 gzip 压缩:这个优化是两方面的,前端将文件打包成.gz 文件,然后通过 nginx 的配置,让浏览器直接解析.gz 文件。
⑦ 首页单独做服务端渲染:如果首页真的有瓶颈,可以考虑用 node 单独做服务端渲染,而下面的子页面仍用 spa 单页的方式交互。这里不推荐直接用 nuxt.js 服务端渲染方案,因为这样一来增加了学习成本,二来服务端的维护成本也会上升,有时在本机测试没问题,在服务端跑就有问题,为了省心,还是最大限度的使用静态页面较好。
jQuery是直接操作DOM,Vue不直接操作DOM,Vue的数据与视图是分开的,Vue只需要操作数据就行它是个框架
jQuery的操作DOM行为是频繁的,而Vue利用虚拟DOM的技术,大大提高了更新DOM时的性能它是个库
Vue中不倡导直接操作DOM,开发者只需要把大部分精力放在数据层面上
Vue集成了一些库,大大提高开发效率,例如Route、Vuex等等
尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher v-if和v-for不能连用 v-if 和 v-show 区分使用场景 v-for 遍历必须加 key,key 最好是 id 值,且避免同时使用 v-if 如果需要使用v-for给每项元素绑定事件时使用事件代理
SPA 页面采用keep-alive缓存组件 在更多的情况下,使用v-if替代v-show 使用路由懒加载、异步组件
防抖、节流 第三方模块按需导入
长列表滚动到可视区域动态加载,不需要响应式的数据不要放到 data 中(可以Object.freeze() 冻结数据)
图片懒加载
SEO优化 预渲染
服务端渲染SSR 打包优化,
压缩代码 Tree Shaking/Scope Hoisting
使用cdn加载第三方模块 多线程打包happypack splitChunks抽离公共文件 sourceMap优化 骨架屏
PWA 还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。防止内部泄漏,组件销毁后把全局变量和事件销毁
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。