1. this

  1. 箭头函数没有作用域,this 指向上一级

2. 闭包

为什么使用闭包?

  1. 避免变量被污染
  2. 私有化
  3. 保存变量,常驻内存
;(function fn () {
let a = 10
let b = 30
function a () {}

return {}
})()

// 当前的a与闭包里的a不冲突
let a = 15

使用场景

  1. 防抖和节流中也会用到闭包
  2. 库的封装(保证数据私有性)

例子

// 计数器
// 闭包应用 => 处理私有数据
let makeCounter = function () {
let privateCounter = 0
function changeBy (val) {
privateCounter += val
}
return {
increment: function () {
changeBy(1)
},
decrement: function () {
changeBy(-1)
},
value: function () {
return privateCounter
}
}
}

2.1 匿名自执行函数

特点

  1. 自执行 => 单例模式
  2. 防止变量污染
;(function (window) {})(window)

应用

jQuery

以下代码尝试封装 jQuery

(function (window){
window.$ = jquery = function(nodeSelector){
let nodes = {};
if(typeof nodeSelector === 'string'){
let temp = document.querySelectorAll(nodeSelector);
for(let i = 0; i < temp.length; i++){
nodes[i] = temp[i];
}

// 类数组
nodes.length = temp.length;
}else{
throw new Error("必须输入字符串")
}

// 模拟添加css类方法
nodes.addClass = function (classes){
let className = class.split(" ");
className.forEach(value => {
for(let i = 0; i < nodes.length; i++){
nodes[i].classList.add(value)
}
})
};

// 模拟设置text方法
nodes.setText = function (text){
for(let i = 0; i < nodes.length; i++){
nodes[i].textContent = text;
}
}
}

})(window)

优化上述代码

let $ = (jquery = (function (window) {
let jquery = function (nodeSelector) {
this.nodes = document.querySelectorAll(nodeSelector)
}
jquery.prototype = {
each: function (callback) {
for (let i = 0; i < this.nodes.length; i++) {
callback.call(this, i, this.nodes[i])
}
},
addClass: function (classes) {
let className = classes.split(' ')
className.forEach(value => {
// for(let i = 0; i < nodes.length; i++){
// nodes[i].classList.add(value)
// }
this.each(function (index, obj) {
obj.classList.add(value)
})
})
},
setText: function (text) {
// for(let i = 0; i < nodes.length; i++){
// nodes[i].textContent = text;
// }
this.each(function (index, obj) {
obj.textContent = text
})
}
}

return function (nodeSelector) {
return new jquery(nodeSelector)
}
})())

代码解释:这段代码采用了一个立即执行函数表达式(IIFE),它返回一个函数,这个函数又返回一个新的对象。这里的第一个 return 语句是为了返回这个函数,以便在代码中使用 $jquery 来创建一个的对象。第二个 return 语句是为了确保每次调用 $jquery 时都会返回一个新的对象,而不是返回同一个对象的引用。这样可以避免多个地方同时对同一个对象进行操作时产生的副作用。

再次优化

let $ = (jQuery = (function (window) {
function Query (dom, selector) {
let i,
len = dom ? dom.length : 0
for (i = 0; i < len; i++) {
this[i] = dom[i]
}
this.length = len
this.selector = selector || ''
return this
}

function Z (elements, selector) {
return Query.call(this, elements, selector)
}

function qsa (element, selector) {
return element.querySelectorAll(selector)
}

Z.prototype = {
each (callback) {
;[].every.call(this, function (el, index) {
return callback.call(el, index, el)
})
},
find (selector) {
let doms = []
this.each(function (index, el) {
let childs = this.querySelectorAll(selector)
doms.push(...childs)
})
return new Z(doms, selector)
},
eq (i) {
let doms = []
this.each(function (index, el) {
if (i == index) {
doms.push(this)
}
})
return new Z(doms, this.selector)
},
remove () {
this.each(function (index, el) {
this.remove()
})
}
}

function isFunction (value) {
return typeof value == 'function'
}

$.isFunction = isFunction

return $
})())

3. 空对象

let obj = Object.create(null)let obj2 = {}的区别

  1. obj没有原型链,obj2__proto__
  2. 使用obj来存数据,运行效率比obj2更快,因为obj没有原型链,找不到数据不用向上变量原型链

4. 事件委托

解释:如果要作用到当前节点,可以作用到上一层节点中,用上层节点进行委托。比如<li>标签有点击事件,可以使用<ul>标签(当前<li>标签的上层)接收点击事件,作用时,判断是不是<li>标签即可。

例子

let ul = document.getElementById('ul')
ul.onclick = function (event) {
event = event || window.event
let target = event.target
// 可以通过判断event target的nodeName是不是li标签,nodeName规定大写
if (target.nodeName == 'LI') {
alert(target.innerHTML)
}
}

5. 原型链

5c486425000107c811760468[1].jpg

822e1844346f4694a2e04e22f6fa9fbc[1].png

6. 对象

可以在对象中使用方括号获取变量的值作为键

let user1 = 'John'
let obj = {
user1: { js: 100, css: 60 }
}
// 输出 {user1: {…}}

let obj = {
[user1]: { js: 100, css: 60 }
}
// 输出 {John: {…}}

7. ??与||的区别

const x = undefined ?? 'default' // x = 'default'
const y = null ?? 'default' // y = 'default'
const z = 'value' ?? 'default' // z = 'value'
const a = '' ?? 'default' // a = ''
const b = '' || 'default' // b = 'default'

|| 只会在左边的值为假值时返回右边的值 (0, ‘’, undefined, null, false 等都为假值)

?? 是在左边的值为 undefined 或者 null 时才会返回右边的值

8. 前端性能优化

8.1 路由懒加载

// 通过webpackChunkName设置分割后代码块的名字
const Home = () =>
import(/* webpackChunkName: "home" */ '@/views/home/index.vue')
const MetricGroup = () =>
import(/* webpackChunkName: "metricGroup" */ '@/views/metricGroup/index.vue')

const routes = [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/metricGroup',
name: 'metricGroup',
component: MetricGroup
}
]

懒加载前提的实现:ES6 的动态地加载模块——import()

8.2 组件懒加载

只有使用到该组件时,才会加载对应的文件

<script>
const dialogInfo = () => import(/* webpackChunkName: "dialogInfo" */ '@/components/dialogInfo');
export default {
name: 'homeView',
components: {
dialogInfo
}
}
</script>

使用场景

  1. 该页面的 JS 文件体积大,导致页面打开慢,可以通过组件懒加载进行资源拆分,利用浏览器并行下载资源,提升下载速度(比如首页)
  2. 该组件不是一进入页面就展示,需要一定条件下才触发(比如弹框组件)
  3. 该组件复用性高,很多页面都有引入,利用组件懒加载抽离出该组件,一方面可以很好利用缓存,同时也可以减少页面的 JS 文件大小(比如表格组件、图形组件等)

8.3 合理使用 Tree shaking

Tree shaking 的理解:是一种用于优化 JavaScript 代码的技术,消除无用的 JS 代码,减少代码体积。

如果代码存在副作用(如修改全局变量或调用接口等),那么这部分代码是不会被删除的。

// util.js
export function targetType (target) {
return Object.prototype.toString
.call(target)
.slice(8, -1)
.toLowerCase()
}
export function deepClone (target) {
return JSON.parse(JSON.stringify(target))
}

项目中只使用了 targetType 方法,但未使用 deepClone 方法,项目打包后,deepClone 方法不会被打包到项目里

// util.js
export default {
targetType (target) {
return Object.prototype.toString
.call(target)
.slice(8, -1)
.toLowerCase()
},
deepClone (target) {
return JSON.parse(JSON.stringify(target))
}
}

// 引入并使用
import util from '../util'
util.targetType(null)

无法通过静态分析判断出一个对象的哪些变量未被使用,所以 tree-shaking 只对使用 export 导出的变量生效

8.4 骨架屏优化白屏时长

使用骨架屏优化白屏时间

骨架屏插件:vue-skeleton-webpack-plugin

8.5 长列表虚拟滚动

通过虚拟滚动列表,即数据滚动,dom 元素固定个数不再新增

虚拟滚动的插件:vue-virtual-scroller、vue-virtual-scroll-list、react-tiny-virtual-list、react-virtualized

8.6 Web Worker 优化长任务

GUI 渲染线程与 JS 引擎线程是互斥的关系,当页面中有很多长任务时,会造成页面 UI 阻塞,出现界面卡顿、掉帧等情况

(通信时长:新建一个 web worker 时, 浏览器会加载对应的 worker.js 资源)

当任务的运算时长 - 通信时长 > 50ms,推荐使用 Web Worker

8.7 requestAnimationFrame 制作动画

requestAnimationFrame 是浏览器专门为动画提供的 API,它的刷新频率与显示器的频率保持一致,使用该 api 可以解决用 setTimeout/setInterval 制作动画卡顿的情况

setTimeout/setInterval、requestAnimationFrame 三者的区别

1)引擎层面

setTimeout/setInterval 属于 JS引擎,requestAnimationFrame 属于 GUI引擎

JS引擎与GUI引擎是互斥的,也就是说 GUI 引擎在渲染时会阻塞 JS 引擎的计算

2)时间是否准确

requestAnimationFrame 刷新频率是固定且准确的,但 setTimeout/setInterval 是宏任务,根据事件轮询机制,其他任务会阻塞或延迟 js 任务的执行,会出现定时器不准的情况

3)性能层面

当页面被隐藏或最小化时,setTimeout/setInterval 定时器仍会在后台执行动画任务,而使用 requestAnimationFrame 当页面处于未激活的状态下,屏幕刷新任务会被系统暂停

8.8 JS 的 6 种加载方式

8.8.1 正常模式

<script src='index.js'></script>

这种情况下 JS 会阻塞 dom 渲染,浏览器必须等待 index.js 加载和执行完成后才能去做其它事情

8.8.2 async 模式

<script async src='index.js'></script>

async 模式下,它的加载是异步的,JS 不会阻塞 DOM 的渲染,async 加载是无顺序的,当它加载结束,JS 会立即执行

使用场景:若该 JS 资源与 DOM 元素没有依赖关系,也不会产生其他资源所需要的数据时,可以使用 async 模式,比如埋点统计

(埋点统计:通过一定的方式记录用户行为和操作,将这些数据发送到服务器进行处理和分析,从而了解用户的行为习惯、流量来源、使用习惯等信息。页面 PV(Page View)统计、点击事件统计、表单提交统计、Ajax 请求统计、错误信息统计)

8.8.3 defer 模式

<script defer src='index.js'></script>

defer 模式下,JS 的加载也是异步的,defer 资源会在 DOMContentLoaded 执行之前,并且 defer 是有顺序的加载

如果有多个设置了 defer 的 script 标签存在,则会按照引入的前后顺序执行,即便是后面的 script 资源先返回

应用场景

控制资源加载顺序

<script defer src="vue.js"></script>
<script defer src="element-ui.js"></script>

8.8.4 module 模式

<script type='module'>import {a} from './a.js'</script>

浏览器会对其内部的import引用发起 HTTP 请求,获取模块内容。这时 script 的行为会像是 defer 一样,在后台下载,并且等待 DOM 解析

8.8.5 preload

link 标签的 preload 属性:用于提前加载一些需要的依赖,这些资源会优先加载

<link rel="preload" as="script" href="index.js">

特点

  1. preload 加载的资源是在浏览器渲染机制之前进行处理的,并且不会阻塞 onload 事件;
  2. preload 加载的 JS 脚本其加载和执行的过程是分离的,即 preload 会预加载相应的脚本代码,待到需要时自行调用;

8.8.6 prefetch

<link rel="prefetch" as="script" href="index.js">

prefetch 是利用浏览器的空闲时间,加载页面将来可能用到的资源的一种机制;通常可以用于加载其他页面(非首页)所需要的资源,以便加快后续页面的打开速度

8.9 图片的优化

8.9.1 图片的动态裁剪

只需在图片的 url 上动态添加参数,就可以得到需要的尺寸大小

8.9.2 图片的懒加载

<script>
let n = document.getElementsByTagName("img").length; // 获取图片数量
let imgs = document.getElementsByTagName("img"); // 获取图片dom元素
let count = 0; // 图片计数,防止加载过的图片重新加载

lazyload(); // 执行第一遍,先把看到的图片先加载
window.onscroll = lazyload; // 为网页滑动事件绑定函数

function lazyload() {
let deviceHeight = document.documentElement.clientHeight; // 获取设备页面的高度
let scrollTop =
document.documentElement.scrollTop || document.body.scrollTop; // 获取当前滑动的距离

for (let i = count; i < n; i++) {
// 从count开始遍历,避免多次执行
if (imgs[i].offsetTop < deviceHeight + scrollTop) {
// 若当前图片距离页面顶部的高度小于滑条滑过的距离和页面高度的和,证明该图片已经在可视区域内,执行加载
if (imgs[i].getAttribute("src") == "./img/loading.gif") {
imgs[i].src = imgs[i].getAttribute("data-src");
}
count = i + 1;
}
}
}
</script>

插件推荐:vue-lazyload

8.9.3 使用字体图标

字体图标的优点

  1. 轻量级:一个图标字体要比一系列的图像要小。一旦字体加载了,图标就会马上渲染出来,减少了 http 请求
  2. 灵活性:可以随意的改变颜色、产生阴影、透明效果、旋转等
  3. 兼容性:几乎支持所有的浏览器,请放心使用

8.9.4 图片转 base64 格式

将小图片转换为 base64 编码字符串,并写入 HTML 或者 CSS 中,减少 http 请求

优缺点

1)它处理的往往是非常小的图片,因为 Base64 编码后,图片大小会膨胀为原文件的 4/3,如果对大图也使用 Base64 编码,后者的体积会明显增加,即便减少了 http 请求,也无法弥补这庞大的体积带来的性能开销,得不偿失;(空间换时间)

2)在传输非常小的图片的时候,Base64 带来的文件体积膨胀、以及浏览器解析 Base64 的时间开销,与它节省掉的 http 请求开销相比,可以忽略不计,这时候才能真正体现出它在性能方面的优势

9. 函数表达式、函数声明

  1. 变量提升
  2. 函数声明提升
var foo = function () {
console.log("foo1")
}
foo()

var foo = function () {
console.log("foo2")
}
foo()


function foo() {
console.log("foo1")
}
foo()

function foo() {
console.log("foo2")
}
foo()

// foo1
// foo2
// foo2
// foo2

表达式提升:无论在哪个位置使用var声明一个变量,该变量都会在整段程序最上端,因此在使用var声明他之前调用他,会显示undefined

foo()
var foo = function () {
console.log("foo1")
}

// Uncaught TypeError: foo is not a function

函数声明提升:函数声明会在任何代码执行之前先被读取并添加到执行上下文,如果出现重复的函数声明,最后的声明会替换前面的声明,因此无论哪个位置执行该函数都会执行最后声明的函数内容

function foo() {
console.log("foo1")
}
foo() // foo2

function foo() {
console.log("foo2")
}

10. valueOf与toString的区别

valueOf()方法返回对象的原始值。例如,当使用一个对象进行数学运算时,JavaScript会自动调用valueOf()方法来获取对象的原始值进行计算。

toString()方法返回对象的字符串表示。它将对象转换为字符串,并返回该字符串。

var num = 10;

var numObj = new Number(10);

console.log(num.valueOf()); // 输出:10
console.log(num.toString()); // 输出:"10"

console.log(numObj.valueOf()); // 输出:10
console.log(numObj.toString()); // 输出:"10"