基础知识整理-3-JavaScript
JS 是一种高级的、解释型的编程语言;
JS 是一门基于原型、头等函数的语言,是一门多范式的语言,它支持面向对象程序设计,指令式编程,以及函数式编程。
历史:
1994 年,网景公司(Netscape)发布了 Navigator 浏览器 0.9 版
这是历史上第一个比较成熟的网络浏览器,但这个版本的浏览器只能浏览,不具备与访问者互动的能力。
网景公司急需一种网页脚本语言,使得浏览器可以与网页互动。
1995 年网景公司招募了程序员 Brendan Eich 希望将 Scheme 语言作为网页脚本语言;
就在这时,Sun 公司将 Oak 语言改名为 Java,正式向市场推出。Java 推出后立马在市场引起轰动,使得网景公司动了心,决定与 Sun 公司结盟,希望将 Java 嵌入到网页。Brendan Eich 本人热衷于 Scheme,并对 Java 不感兴趣,用 10 天时间设计出来了 JavaScript。
最初这门语言的名字是 Mocha(摩卡),在 Navigator2.0 beta 版本更名为 LiveScript,在 Navigator2.0 beta 3 版本正式重命名为 JavaScript,为了蹭 Java 的热度。
但是这门语言当时更像是一个多种语言的大杂烩:
借鉴 C 语言的基本语法;
借鉴 Java 语言的数据类型和内存管理;
借鉴 Scheme 语言,将函数提升为一等公民(first class)的地位;
借鉴 Self 语言,使用基于原型(prototype)的继承机制。
微软公司于 1995 年首次推出 Internet Explorer 从而引发了与 Netscape 的浏览器大战
微软对 Netscape Navigator 解释器进行了逆向工程,创建了 JScript,与网景公司竞争。
这时候对开发者来说是一场噩耗,因为需要针对不同的浏览器进行适配
1996 年 11 月,网景公司正式向 ECMA(欧洲计算机制造商协会)提交语言标准
1997 年 6 月,ECMA 以 JavaScript 语言为基础制定了 ECMAScript 标准规范 ECMA-262
ECMA-262 是一份标准,定义了 ECMAScript
JavaScript 成为了 ECMAScript 最著名的实现之一
除此之外,ActionScript 和 JScript 也都是 ECMAScript 规范的实现语言
所以说,ECMAScript 是一种规范,而 JavaScript 是这种规范的一种实现。
1997 - ES1;1998 - ES2; 1999 - ES3; 2009 - ES5; 2015 - ES6
<noscript>
元素被用于给不支持 javascript 的浏览器提供替代内容
<script>
元素是双标签元素,在引用外部 js 文件时,标签内不能再写代码,推荐将元素放置在<body>
元素内容底部。
JavaScript 代码严格区分大小写
变量命名规则:变量可由字母、下划线、美元符号、数字组成,但不能以数字开头,不能使用关键字、保留字。
多个单词使用小驼峰的方式表示
不使用 var 声明变量也可以创建变量,并挂在 window 对象中,不管在哪个作用域。
var 声明的变量只要在全局作用域都会挂到 window 对象。
语句是向浏览器发出的指令,通常表达一个操作或者行为,每条语句后面通常加分号来表示语句的结束。当存在换行符时,JS 会将其理解为隐式的分号,所以也可以省去手动添加的操作。
// 单行注释
, /* 多行注释 */
, /** 文档注释(不常用)*/
vscode 插件推荐:ES7+ react (console.log 可通过 clg 快速生成)
配置方法大括号颜色和连线:Bracket Pairs(控制是否启用括号对指南):active、Bracket Pair Colorization(控制是否启用括号对着色):true
在 JavaScript 中有 8 种基本的数据类型(7 种原始类型和 1 种复杂类型)
原始(基本):Number String Boolean Undefined Null BigInt Symbol
复杂(引用):Object
typeof 操作符 用来判断 undefined、boolean、string、number、symbol、function、object(null)
特殊的数值:Infinity 无穷大 , NaN 不是一个数字 , 0x 代表 16 进制 , 0o 开头表示 8 进制 , 0b 开头表示 2 进制;
JS 中数字可以表示的范围:Number.MAX_VALUE 最大值 , Number.MIN_VALUE
isNaN()方法判断值是否是 NaN
当对一个对象类型进行初始化时,不建议初始化为{}
建议初始化为 null。{}
转换为 boolean 时是 true。
变量声明但没赋值时,它的默认值就是 undefined,一般不会主动设置 undefined。
可以用 Array.isArray()来检测是否是数组,因为 typeof 数组会显示 object(兼容到 IE8)shift、unshift、push、pop 方法,unshift 是在头添加
splice()方法用于替换数组中的指定项,在指定位置插入,删除指定项。会以数组形式返回被删除的项。
slice()方法用于截取子数组,slice(a,b)从 a 到 b 不包含 b,不会更改原数组,不提供 b 的话会截取到最后。
数组的 join()方法可以把数组转为字符串、split()方法将字符串转为数组。join 的参数是以什么作为连接符,如果留空会默认以逗号分隔。
如同调用 toString()
concat()方法合并链接多个数组 arr1.concat(arr2,arr3) 就会把 1,2,3 连起来,不改变原数组
reverse()将数组置反,更改原数组。
indexOf()功能是搜索数组中的元素,并返回位置,不存在返回-1
includes()是判断一个数组是否包含某个值,返回布尔值
sort()数组排序,这个方法的参数是一个函数,函数的参数有 a,b 如果需要他们俩交换位置就返回正数,否则返回负数。
冒泡排序:4 个数字需要比较三趟,比较 3+2+1=6 次
显示转换:
其他类型转成字符串类型:String()、toString()
其他类型转成数字类型:Number() undefined -> NaN | null -> 0 | true/false -> 1/0
注意:Boolean("0") // true
算数运算符/赋值运算符/关系(比较)运算符/逻辑运算符
算数运算符:加、减、乘、除、取余,幂(ES7)2 ** 3 = 8
等于 Math.pow(2,3)
算数运算符其他内容:原地修改:+=等等,自增自减:++/--
运算顺序:非运算 > 数学运算 > 关系运算 > 逻辑运算
== 一般会隐式转换成 Number 类型再比较,null 例外。
0 == ""; // true
0 == false; // true
0 == null; // false
0 == undefined; // false
null == undefined; // true
Number(null); // 0
Number(undefined); // NaN
arguments 是类数组对象,内部包含函数所有传入的实参
递归实现斐波那契数列:
// 数列 1 1 2 3 5 8 13 21 34 55
// 位置 1 2 3 4 5 6 7 8 9 10 第n个数是多少
function fibonacci(n) {
if (n === 1 || n === 2) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
console.log(fibonacci(10));
ES6 之前没有块级作用域的概念,但函数可以定义自己的作用域
作用域(Scope)表示一些标识符的作用有效范围
函数的作用域表示在函数内部定义的变量,只有在函数内部可以被访问到
函数表达式、匿名函数、高阶函数、回调函数、立即执行函数(IIFE)
闭包是函数本身和该函数声明时所处的环境状态的组合,每次创建函数其实都会创建闭包。他有记忆性和模拟私有变量的特点。当闭包产生时,函数所处环境的状态会始终保存在内存中,不会在外层函数调用后被清除,这就是记忆性。
<body>
<div class="box">
<input type="button" value="按钮1" class="btn" />
<input type="button" value="按钮2" class="btn" />
<input type="button" value="按钮3" class="btn" />
<input type="button" value="按钮4" class="btn" />
</div>
<script>
var btns = document.querySelectorAll(".btn");
for (var i = 0; i < btns.length; i++) {
(function (m) {
btns[m].onclick = function () {
console.log("点击了按钮" + (m + 1));
};
})(i);
}
// 在没有块级作用域的时候使用闭包来缓存变量i
</script>
</body>
chrome 调试技巧->Sources,想在哪行停止就点击一下行号,打断点。【在代码中写 debugger】也是同样的作用。
右侧公护栏的 Watch 用来检测某个变量的变化。+ 号 添加要监听的变量。
Breakpoints 是所有断点的列表
Scope 作用域
恢复执行(恢复一个断点的执行)、过掉当前行代码执行下一行、进函数内部、立即跳出函数、最后一个和第三个的区别是不会进入异步函数
对象中的函数称为方法
Object.keys() 返回数组,拿到对象所有的键
Object.values() 拿到所有值
Object.entries() 返回二维数组,拿到键值
for...in...遍历对象
for...of...默认不可遍历对象,因为它只能遍历可迭代对象
一个函数被 new 操作符调用后,它会执行如下操作: 1.在内存中创建一个新的对象(空对象) 2.这个对象内部的[[prototype]]属性会被赋值为该构造函数的 prototype 属性 3.构造函数内部的 this,会指向创建出来的新对象 4.执行函数的内部代码(函数体代码) 5.如果构造函数没有返回非空对象,则返回创建出来的新对象。
构造函数的名称使用大驼峰,普通函数使用小驼峰
浏览器中存在一个全局对象:window
函数也是对象
JS 常见内置类:
包装类:默认情况下,当我们调用一个原始类型的属性或者方法时,会进行如下操作 1.根据原始值,创建一个原始类型对应的包装类型对象 2.调用对应的属性或方法,返回一个新的值 3.创建的包装类对象被销毁 4.通常 JS 引擎会进行很多优化,可以跳过创建包装类的过程在内部直接完成属性的获取或者方法的调用
undefined 和 null 没有包装类
数字类型:Number
toString、toFixed、parseInt、parseFloat 等等方法
IEEE754 使运算一些小数导致丢失精度;Math.pow 幂运算,Math.sqrt 开根号,Math.ceil 向上取整,Math.floor 向下取整
random 公式:[a,b]区间内的随机数是 parseInt(Math.random() * (b-a+1)) + a;
字符串类型:
字符串在定义后是不可被修改的,只能重新赋值
indexOf 查找子串、ES6 新增 includes 是否包含子串、是否以 xxx 开头/结尾:startsWith、endsWith、替换字符串 replace
slice、substr、substring 截取子串,charAt 得到指定位置字符,toUpperCase 转大写、toLowerCase 转小写、concat 字符串拼接
删除首尾空格 trim , 字符串切割 split 返回数组
数组类型:
push/pop 在数组尾部添加/删除元素,unshift/shift 在头部添加/删除元素。前者运行较快
arr.splice(start,deleteCount[,新增的元素]) 可以添加、删除、替换元素。修改数组本身。
arr.slice()截取子串,concat 拼接数组,join 将数组转字符串, 数组内查找元素索引 indexOf
高阶函数 arr.find(fn) 用于查找复杂元素,返回找到的元素。findIndex 查找元素索引。
arr.sort(fn) 比较 a,a-b,b 结果小于 0,a 会放到前边,如果大于 0,b 被放到 a 的前边。从小到大排列。
arr.reverse() 返回新的倒叙数组,其他: forEach/filter/map/reduce
日期对象 Date
UTC 是标准时间
new Date() 获取当前时间
日期的表示方式有两种:RFC 2822 和 ISO 8601
DOM 相当是 JS 和 HTML、CSS 之间的桥梁。通过浏览器提供的 DOM API ,可以对元素以及其中的内容做任何事情。
节点的 nodeType 属性可以显示这个节点的具体类型:
1:元素节点;3:文字节点;8:注释节点;9:document 节点;10:DTD 节点
document.getElementById 兼容 IE6
document.getElementsByTagName 兼容 IE6
document.getElementsByClassName 兼容 IE9
document.querySelector 兼容 IE9,IE8 部分兼容
document.querySelectorAll 兼容 IE9,IE8 部分兼容
在标准的 W3C 规范中,空白文本节点也应该算作节点,但在 IE8 及之前的浏览器中会有一定的兼容问题,他们不把空文本节点当作节点
从 IE9 开始支持一些“只考虑元素节点”的属性
| 关系 | 考虑所有节点 | 只考虑元素节点
| 子节点 | childNodes | children
| 父节点 | parentNode | parentNode
| 第一个子节点 | firstChild | firstElementChild
| 最后一个子节点 | lastChild | lastElementChild
| 前一个兄弟节点 | previousSibling | previousElementSibling
| 后一个兄弟节点 | nextSibling | nextElementSibling
<body>
<div class="box">
<input type="button" value="按钮1" class="btn" />
<input type="button" value="按钮2" class="btn" />
<input type="button" value="按钮3" class="btn" />
<input type="button" value="按钮4" class="btn" />
你好
<p>hello</p>
</div>
<script>
var box = document.querySelector(".box");
// 封装一个函数,这个函数可以返回元素的所有子元素节点(兼容到IE6),类似children功能
function getChildren(node) {
// 结果数组
var children = [];
// 遍历所有子节点,判断每个子节点的nodeType是不是1
for (let i = 0; i < node.childNodes.length; i++) {
if (node.childNodes[i].nodeType === 1) {
children.push(node.childNodes[i]);
}
}
return children;
}
console.log(getChildren(box));
</script>
</body>
获取、设置元素节点的不符合 W3C 标准的属性时,需要使用setAttribute()
和getAttribute()
如果是data-*
的属性,可通过dataset
属性设置(HTML5 新增)
创建节点:document.createElement 挂在到某个节点下 xxx.appendChild 或 insertBefore
删除节点:父节点.removeChild(要删除的子节点);克隆节点:老节点.cloneNode(true) 参数 true 表示深度克隆
常见的鼠标事件监听:
onclick 单击 、ondblclick 双击 、 onmousedown 鼠标被按下 、 onmouseup 鼠标按键被抬起 、onmousemove 鼠标移动
、 onmouseenter 鼠标进入(onmouseover 冒泡) 、 onmouseleave 鼠标离开( onmouseout 冒泡)
常见表单事件监听:
onchange、onfocus、onblur、onsubmit、onreset
事件的传播是先从外到内,再从内到外。(捕获和冒泡)onxxx(DOM0 级事件监听)的方式只能监听冒泡阶段
DOM2 级事件监听 addEventListener 最后的参数 true 是可监听捕获阶段
注:最内部元素不再区分捕获和冒泡,会执行卸载前面的监听。
如果给一个元素设置多个同名事件,DOM0 级写法后面写的会覆盖前面写的,DOM2 级会按顺序依次执行
鼠标位置:
clientX 相当于浏览器的水平坐标
clientY 相当于浏览器的垂直坐标
pageX 相当于整张网页的水平坐标
pageY 相当于整张网页的垂直坐标
offsetX 相当于事件源元素的水平坐标(会计算最内部元素,所以内部不应再有子元素)
offsetY 相当于事件源元素的垂直坐标(会计算最内部元素,所以内部不应再有子元素)
阻止默认行为:e.preventDefault 阻止事件传播(冒泡/捕获):e.stopPropagation
e.charCode 属性用于 onkeypress 事件中,表示用户输入的字符码
e.keyCode 属性用于 onkeydown 和 onkeyup,表示用户按下的键码
鼠标滚轮事件是 onmousewheel,deltaY 属性表示鼠标滚动方向,下滚时返回正值,上滚时返回负值。
事件委托:
要使用能够冒泡的事件,target 是触发此事件最早的元素,currentTarget 是事件处理程序附加到的元素
窗口尺寸相关属性:
innerHeight 浏览器窗口的内容区域的高度,包含水平滚动条
innerWidth 浏览器窗口的内容区域的宽度,包含垂直滚动条
outerHeight 浏览器窗口的外部高度
outerWidth 浏览器窗口的外部宽度
获得不包含滚动条的窗口宽度:document.documentElement.clientWidth
在窗口大小改变后会触发 resize 事件
window.scrollY 表示在垂直方向已滚动的像素值 (只读)
document.documentElement.scrollTop 属性也表示窗口卷动高度
所以为了更好的兼容性,可以同时写上方两个属性
在窗口被卷动后,会触发 scroll 事件,可以使用 window.onscroll 或者 window.addEventListener('scroll')来监听
window.navigator 内含有此次活动的浏览器相关属性(浏览器名称、版本、操作系统等)
window.location 标识当前所在网址,可以通过这个属性让浏览器跳转页面
window.location.reload(true) 方法重载当前页面,入参 true 表示从服务器强制加载
window.location.search 拿到当前浏览器 get 请求的查询参数
DOM 元素都有 offsetTop 属性,表示此元素到定位祖先元素的垂直距离。(在祖先中离自己最近并有定位属性的元素)
reduce 计算 汇总 收敛 把一个数组中的一堆值计算出来一个值(最大值、平均值、和 等等)从左往右
例: 求数组中所有的和 let arr = [1,2,3]可以只传一个函数,也可以传第二个参数:初始值.如果没有传初始值,val 就是第一个元素,item 是第二个
// arr.reduce(function(当前值,当前元素,当前索引,老数组){},0)
arr.reduce(function (val, item, index, origin) {
return val + item; // 0+1 1+2 3+3 (返回值会成为下一次函数执行的val)
}, 0);
// reduceRight从右往左算
Object.assign(obj1,obj2,obj3)合并方法,会把后面的对象合并到第一个对象并返回。结果会===第一个对象.
可以通过 Array.from()转换成数组:
所有可遍历的数组:字符串、Set、Map、NodeList、arguments 、拥有 length 属性的任意对象,而且属性名是数值型或字符串型数字。 Array.from()第二个参数是一个回调函数相当于 map 方法,将即将形成的数组每一项做一下处理。第三个参数是要修改为的 this 指向。
JS 中的函数也是对象,那么它就会有自己的属性和方法。
name
属性 获取函数名称,length
属性 获取形参个数(不包含剩余参数运算符和有默认值的参数)
arguments
是类数组对象,虽然有 length 属性也可以通过 index 访问元素。但没有map
、filter
等数组才有的方法,有时候如果我们需要用到这些方法,就需要将其转为数组,可以使用 ES6 中的方法:Array.from()
、借助展开运算符[...arguments]
老方法:[].slice.apply(arguments)
箭头函数无 arguments,会去上层作用域查找
剩余参数运算符,会将没有形参接收的实参以数组形式储存起来,而 arguments 里面是所有的实参。剩余参数需要放在形参最后一个。
new 操作会进行如下四步: 1.创建一个新对象 2.将构造函数的 this 绑定到这个对象 3.这个对象内部的[[prototype]]属性(一般是__proto__
)会被赋值为该构造函数的 prototype 4.构造函数返回该对象
函数 this 的绑定规则
function 是“运行时上下文”策略,this 是在运行时被绑定的;
默认绑定:当函数在非严格模式下独立调用时,this 默认绑定到全局对象(浏览器中是 window 对象,在 Node 中是 global 对象),严格模式下,this 是 undefined。优先级最低。
隐式绑定:当函数作为对象的方法被调用时,this 绑定到该对象。
显式绑定:通过 call、apply、bind 方法,显式指定 this 绑定的对象。call 和 apply 会立即调用函数并传递 this,而 bind 会返回一个新的函数并绑定着指定的 this。优先级高于隐式绑定,bind 优先级高于 apply 和 call
new 绑定:通过 new 关键字调用构造函数时,this 绑定到新创建的对象实例。优先级高于 bind,不可和 call/apply 使用。
初以上四种规则外,还需注意箭头函数中:
箭头函数不会创建自己的 this,他会使用外部作用域的 this 值,通常是定义箭头函数时所在的上下文。并且是固定的不能被 call 等改变。
箭头函数也没有 arguments 属性,不能作为构造函数使用
注:IIFE、定时器、延时器默认绑定 window,对象、数组调用方法时默认是对象/数组,DOM 事件处理函数的 this 是绑定 DOM 的元素
call 和 apply 的区别是 call 用逗号罗列参数,而 apply 用数组传递参数;bind 绑定时入参列表,在调用时传参会排到绑定时入参的后面。
forEach 等遍历方法的第一个参数是函数,默认绑定 window。第二个参数就是指定绑定 this
严格模式
对代码进行更严格的检测和执行,通过抛出错误来消除一些原有的静默的错误,让 JS 引擎在执行代码时进行更多的优化。
为了兼容早期版本,非严格模式中会有早期语言不合理的问题。
可以给某个文件开启严格模式,也可以给某个函数开启。
对象属性操作的控制
默认没有操作限制,可以将对象的某一个属性进行修改、删除等操作
如果我们想对一个属性进行比较精准的控制,可以使用属性描述符
属性描述符,需要使用Object.defineProperty
来对属性进行添加或者修改
Object.defineProperty(obj,"属性",{}) // 单个监听对象的某个属性
// 一次性添加这个对象的多个属性的监听
Object.defineProperties(obj,{
属性:{
configurable、enumerable、writable、value
}
})
获取普通对象的原型,obj.__proto__
但这是非标准的,浏览器自己加的属性,有可能会出现兼容问题,所以尽量不要在开发中使用。正常获取需要使用Object.getPrototypeOf(obj)
拿到该对象的原型
原型的作用,就是当我们在一个对象中查找某个属性时,如果它自身没有,就会去原型上查找。
将函数看作普通对象时它也是拥有隐式原型的,当将其看作函数时,它拥有显式原型prototype
。它的作用是 new 创建对象后赋值给对象作为它的隐式原型。
原型的 constructor 又指向了函数自己
hasOwnProperty 方法可以检查某个对象是否自己拥有某个对象或方法
in 运算符只能检查某个属性或方法是否可以被对象访问,不能检查是否是自己的属性或方法
new Date()
生成当前时间的日期对象
日期对象的一些方法:
| getDate() | 得到日期 1-31 |
| getDay() | 得到星期 0-6 |
| getMonth() | 得到月份 0-11 |
| getFullYear() | 得到年份 |
| getHours() | 得到小时数 0-23 |
| getMinutes() | 得到分钟数 0-59 |
| getSeconds() | 得到秒数 0-59 |
| getTime() | 转为时间戳(精确到毫秒) |
Date.parse() 将日期对象转为时间戳(精确到秒,最后三位 000)
纯函数
确定的输入一定产生确定的输出,函数在执行过程中不产生副作用。
副作用:在执行一个函数时,除了返回函数值外,还产生了其他影响比如修改了全局变量。
比如数组的 slice 方法就是纯函数,而 splice 方法就不是纯函数。
柯里化
是一种关于函数的高阶技术
只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数,这个过程叫柯里化
组合函数
将多个函数组合,依次调用。
with 语句
拓展一个语句的作用域链
eval 函数
可以将传入的字符串当作 JS 代码执行,会将最后一句代码的执行结果作为返回值
面向对象有三大特性:封装、继承、多态
封装:将属性和方法封装到一个类中,称为封装的过程
继承:是多态的前提,可以减少重复代码的数量
多态:不同的对象在执行时表现出不同的形态
ES5 中实现方法的继承,借助原型链
借用构造函数实现属性的继承
两种方式组合在一起,继承了属性和方法,叫组合继承,但也不完美。(构造函数至少被调用两次,所有子类有两份属性
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.running = function () {
console.log("running");
};
Student.prototype = new Person();
function Student(name, age, score) {
Person.call(this, name, age);
this.score = score;
}
最终继承方案:寄生组合式继承
目的是为了减少一次对构造方法的调用。原理上就是让一个对象的隐式原型指向父类的显式原型
// 之前不好的做法
var p = new Person();
Student.prototype = p;
// 方案1
var obj = {};
// obj.__proto__ = Person.prototype 这是非标准的,存在兼容问题
Object.setPrototypeOf(obj, Person.prototype);
Student.prototype = obj;
// 方案2
function F() {}
F.prototype = Person.prototype;
Student.prototype = new F();
比较便捷的方法:
Student.prototype = Object.create(Person.prototype);
Object.defineProperty(Student.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: Student,
});
// 如果纠结Object.create的兼容性
// 手动实现一个
function createObj(o) {
function F() {}
F.prototype = o;
return new F();
}
// 类方法
Person.eat = function () {};
原型链描述图
浏览器渲染原理
当你在浏览器的地址栏中输入了一个网址并按下回车,发生了什么?
https://dev.to/wassimchegham/ever-wondered-what-happens-when-you-type-in-a-url-in-an-address-bar-in-a-browser-3dob
第一,DNS 解析:
用户输入的 URL 通常会是一个域名地址,直接通过域名是无法找到服务器的,因为服务器的本质上是一台拥有 IP 地址的主机。
需要通过 DNS 服务器来解析域名,并获取 IP 地址。
DNS 会查找缓存,缓存的查找包括浏览器缓存、操作系统缓存、路由器缓存、ISP 缓存,如果缓存中可以找到就可以直接使用对应 IP。
如果缓存没有找到,就需要通过 DNS 递归解析,解析过程包括根域名服务器、顶级域名服务器、权威域名服务器。
最终找到 IP 地址,就通过 IP 地址连接服务器,并将 IP 地址缓存起来。
第二,TCP 连接:
虽然我们发送的是 HTTP 请求,但 HTTP 协议属于应用层,是建立在 TCP 传输层协议之上的。
TCP 的连接会经过三次握手,客户端发送 SYN 包,服务器接收后返回一个 SYN-ACK 包,客户端再次发送一个 ACK 包,完成握手过程。
TCP 连接已建立,双方可以开始传输数据。
第三,HTTP 请求:
一旦 TCP 建立连接成功,客户端就可以通过这个连接发送 HTTP 请求,包括请求方法、URI、协议版本、请求头、请求体。
服务器收到 HTTP 请求后,会处理这个请求,并返回一个 HTTP 响应。
HTTP 响应包括状态码、响应头、响应内容
第四,HTML 解析和 CSS 解析:
浏览器获取到 html 文件后就开始对文档进行解析,用来构建 DOM Tree,在这个过程中还会遇到 CSS 文件和 JS 文件。
遇到 CSS 和 JS 的引用会继续向服务器发送 HTTP 请求,下载 CSS、JS 文件。
之后对 CSS 文件解析,解析出对应的 CSSOM(CSS Object Model)。
第五,渲染 render、布局 layout、绘制 paint:
DOM Tree 和 CSSOM 可以共同构建 Render Tree。
之后在 Render Tree 上运行布局 layout 计算每个结构的几何体。
再由浏览器将每个结构绘制到屏幕的像素点。
第六,composite 合成:
这里还有一个优化手段,就是将元素绘制到多个合成图层中。
默认情况下,标准流中的内容是被绘制到同一个图层(Layer)中的。
可通过一些方法来创建新合成层,新图层可利用 GPU 加速绘制。比如 3D Transforms/video/canvas/iframe/will-change/position fixed 等。
分层虽然可以优化一些性能,但也是以内存管理为代价,所以还需谨慎。
这样用户最终就看到了浏览器中显示的网页,这个过程中还包含更多细节,比如重绘、回流的问题,JavaScript 的执行过程、JS 引擎的相关知识。
接下俩再对上面每一步展开说说:
DNS(Domain Name System)服务器解析过程:
缓存查找过程: 1.浏览器缓存:首先浏览器会检查它缓存中是否有这个域名的记录,之前访问过的网址解析结果可能会被缓存在浏览器中。 2.操作系统缓存,浏览器缓存中没有就在操作系统中寻找。 3.操作系统中也没有就会去本地网络的路由器寻找 4.路由器中也没有就去 ISP(Internet service provider)寻找,它是你的互联网服务的提供商。
DNS 递归解析过程:
如果在缓存的查找过程中都没有查到,那么 DNS 查询就变成了一个递归查询过程,涉及到多个 DNS 服务器。
首先 DNS 的查询请求会被发送到根域名服务器,根域名服务器是最高级别的 DNS 服务器,负责重定向到其所管理的顶级域名服务器。
顶级域名服务器(TLD):根服务器会告诉你 ISP 的 DNS 服务器去查询哪个顶级域名服务器,顶级域名服务器掌握所有例如.com 域名及其对应服务器的信息。
权威域名服务器:一旦你的 DNS 查询到了正确的顶级域名服务器,它会进一步定向到负责 example.com 的权威服务器,权威服务器中有该域名对应的具体 IP 地址。
最终 IP 地址会发送到你的电脑,并且在浏览器、操作系统、路由器、ISP 的 DNS 缓存中留存。
TCP(Transmission Control Protocol,传输控制协议),用于建立两个端点之间的可靠会话。
三次握手: 1.客户端发送一个 SYN(Synchronize)包到服务器以初始化一个连接。(随机序列号) 2.服务器收到 SYN 包后,返回一个 SYN-ACK(Synchronize-Acknowledgment)包作为响应。(也生成一个随机序列号,并将客户端的序列号+1 返回,表示确认收到) 3.客户端收到服务器的 SYN-ACK 包后,发送一个 ACK(Acknowledgment)包作为回应。这个 ACK 包将服务器的序列号+1,并可能包含客户端准备发送的数据开始部分,
比如 HTTP 请求行 GET/HTTP/1.1 和 请求头,这被称为 TCP 快速打开。
HTTP(Hypertext Transfer Protocol,超文本传输协议)
TCP 连接建立后,客户端就通过这个通道发送一个 HTTP 请求到服务器,这个请求包含了方法(GET、POST 等)、URI(统一资源标识符)和协议版本,以及可能包含的请求头和请求体。
服务器收到 HTTP 请求后,处理并返回一个 HTTP 响应。响应通常包括状态码、响应头、响应内容。
TCP 为 HTTP 提供了一个可靠的通道,确保数据正确、完整的从服务器传输到客户端。
在 HTML 和 CSS 解析这一步我们先来简单介绍一下浏览器内核:
- Webkit -> Blink: Google Chrome, Edge Blink 是 Webkit 的分支,谷歌开发
- Webkit:Safari、移动端端浏览器(Android、IOS)由苹果主导的开源内核
- Gecko:Mozilla Firefox
- Trident:IE (微软已放弃)
- Presto:Opera 后来跟谷歌一样改用 Blink
我们常说的浏览器内核指的是浏览器的排版引擎:
排版引擎(layout engine)也成为浏览器引擎(browser engine)、页面渲染引擎(rendering engine)或者样板引擎。网页下载之后是通过它来解析的。
文章:浏览器的工作方式 https://web.dev/articles/howbrowserswork?hl=zh-cn
默认返回 index.html 文件,所以解析 HTML 是所有步骤的开始。
解析 HTML 构建 DOM 树,遇到 link 元素引入 CSS 文件,浏览器就会下载对应文件,下载 CSS 文件不会影响 DOM 解析。下载完后解析 CSS,生成 CSS 规则树也可成为 CSSOM。
有了 DOM 树和 CSSOM 树后,就将两个结合来构建 Render Tree。
注意,link 元素不阻塞 DOM 树的构建,但阻塞 Render 树的构建。Render 树和 DOM 树不是一一对应关系,如果 display 为 none,就不会出现在 Render 树。
然后,在渲染树(Render Tree)上运行布局(Layout)以计算每个节点的几何体。
渲染树会展示有哪些节点以及它的样式,但不表示每个节点的尺寸、位置信息。布局就是确定呈现树中所有节点的尺寸和位置信息。
最后,浏览器将布局阶段计算出来的每个 frame 转为屏幕上的像素点。(绘制,Paint)
注意:布局树是渲染树的子集,不包含渲染树中元素的颜色、背景、阴影等信息。
这里引出知识点:回流(reflow)、重绘(repaint)
回流也称重排,对节点大小、位置的修改需要重新计算。
DOM 结构发生改变、改变了宽/高、padding、font-size 等、窗口 resize、调用 getComputedStyle 方法获取尺寸、位置 都会引起回流。
第一次渲染内容叫绘制,之后重新渲染叫重绘。
比如修改背景色、文字颜色、边框颜色、透明度等。
回流一定会引起重绘,所以回流非常消耗性能。在开发中要尽量避免回流。
比如:
修改样式时尽量一次性修改;
避免频繁操作 DOM,可以在一个 DocumentFragment 或者父元素中将要操作的 DOM 操作完成,再进行一次性操作。现代框架常见操作,比如 Vue 就是这样做的。
避免通过 getComputedStyle 获取尺寸、位置等信息,因为浏览器需要计算。
对某些元素使用 position 的 absolute 或者 fixed,脱离标准文档流,不会对其他元素造成影响。
额外:创建新的合成层(compositingLayer)
因为每个合成层都是单独渲染的
列举一些常见的属性,可创建新的合成层:
- 3D transforms
- video、canvas、iframe
- opacity 动画转换时
position:fixed
- will-change:一个实验性属性,提前告诉浏览器元素可能发生哪些变化
- animation 或 transition 设置了 opacity、transform
分层可以利用 GPU 来获取到一些性能提升,但它以内存管理为代价,所以不能作为 web 性能优化策略的一部分过度使用。
下载 CSS 不影响 DOM 解析。
生成 DOM Tree 和 CSSOM Tree 后,两个结合构建 Render Tree。
所以 Render Tree 的构建也可能受 CSSOM Tree 的加载速度影响。
生成渲染树后会在上面运行“布局 Layout”,以计算每个节点的几何体。(渲染树会表示显示哪些节点和样式,但不表示每个节点的尺寸和位置)
回流(重排):
第一次确定节点的位置和大小,称为布局(Layout),之后对节点大小、位置的修改称为回流
DOM 结构发生改变(添加新节点或移除节点)、修改了布局(width、height、padding、font-size 等)、修改窗口尺寸(resize)、调用 getComputedStyle 方法获取尺寸、位置信息,都会引发回流
重绘:
第一次渲染内容称为绘制(paint)、之后重新渲染称为重绘,修改颜色、边框样式等属于重绘
回流一定引起重绘,所以尽量避免发生回流(消耗性能)
修改样式时尽量一次性修改(比如通过 class 修改)
避免频繁操作 dom
避免通过 getComputedStyle 获取尺寸、位置等信息
对某些元素使用 position 的 absolute 或 fixed,并不是不引起回流,只是开销相对较小,不会对其他元素造成影响
绘制的过程,可以将布局后的元素绘制到多个合成图层中,这是浏览器的一种优化手段。
默认情况下,标准流中的内容都是被绘制在同一个图层中,而一些特殊的属性,会创建新的合成图层,并使用 GPU 加速绘制,因为每个合成层都是单独渲染的。
常见能够形成新合成层的属性:、
3D transforms/video/canvas/iframe/opacity 动画转换时/position 值为 fixed/will-change/animation 或 transition 设置了 opacity、transform
分层确实可以提高性能,但它是以内存管理为代价,因此不能作为 web 性能优化策略过度使用
浏览器在解析 HTML 的过程中,遇到 script 元素会阻塞 DOM 树的构建。它会下载 js 代码,并且执行。执行完毕后才会继续解析 HTML,构建 DOM 树。
这是因为 JavaScript 的作用之一就是操作 DOM,如果等到 DOM 树构建完成并渲染再执行 js 代码,会造成严重的回流和重绘,影响页面性能。
所以遇到 script 元素时,优先下载并执行 js 代码,再继续构建 DOM 树
但也会带来新的问题:
在目前的开发模式中,使用 Vue、React 等框架,脚本往往比 HTML 页面更“重”,下载的时间需要很长。造成页面解析阻塞,浪费很多时间和性能。
为了解决这个问题,script 提供了两个属性 defer 和 async
defer 属性告诉浏览器不要等待脚本下载,而是继续解析 HTML 构建 DOM Tree。
脚本由浏览器下载,下载好后会等待 DOM Tree 构建完成,在 DOMContentLoaded 事件之前执行脚本。
多个带 defer 的脚本是可以保持正确的顺序执行的。
所以 defer 可以提高页面性能,推荐放到 head 元素中。
async 会在脚本下载完后立即执行,执行时会阻塞 DOM 树构建,不保证顺序,独立下载独立运行不等待其他脚本。
在现代化框架开发过程中,往往不需要手动配置 async 或者 defer,使用 webpack 或者 vite 打包时,它会自动加上 defer。
在加载一些第三方分析工具或者广告追踪脚本时可以手动加上 async,对其他脚本或者 dom 没有依赖
注:为了达到更好的用户体验,呈现引擎会力求尽快将内容展示在屏幕上,它不必等整个 HTML 文档解析完毕之后再展示。
一、什么是跨域?什么是同源策略?
跨域问题通常是由浏览器的同源策略(Same-Origin Policy,SOP)引起的访问问题。
同源策略是浏览器的一个重要安全机制,他用于限制一个来源的文档或脚本如何能够与另一个来源的资源进行交互。
当两个 URL 的协议、主机(Host)、端口都相同时才被认为是同源。
在早期,服务器渲染的网站不会有跨域问题。随着前后端分离的出现,前端代码和后端 API 经常部署在不同的服务器上,就会引发跨域问题。
CORS(Cross-Origin Resource Sharing,跨域资源共享)
它使用额外的 HTTP 头来告诉浏览器允许从其他域加载资源。
通过 CORS,服务器可以显示声明哪些源站点有权限访问它的资源。
预请求和实际请求:
对于复杂请求(如 PUT、DELETE 或者自定义头),浏览器会先发一个 OPTIONS 请求,询问服务器是否允许跨域请求。
服务器如果允许跨域,响应会包含 CORS 信息。
如果预请求被允许,浏览器会发送实际请求,并在请求头总包含一些 CORS 的信息。
在日常的开发中我们普遍使用的还是正向代理的方式。
在 webpack、vite 中配置代理。
export default defineConfig({
server: {
proxy: {
"/api": {
target: "http://localhost:3000",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
});
它们底层是开启一个新的 Node 服务器代理来解决跨域的。
Vite 或者 Webpack 使⽤ http-proxy 或 http-proxy-middleware 来创建代理中间件。
生产环境一般使用 Nginx 做反向代理。如果 Nginx 只代理 API 服务器,需要在 Nginx 配置中添加 CORS 信息。(Access-Control-Allow-Origin 等)
正向代理:
代表客户端,向服务端发起请求。客户端知道代理的存在,服务端可能不知道真实的发起者。
反向代理:
代表服务端,接收来自客户端的请求。服务端知道代理的存在,客户端不知道真实的服务端。(负载均衡、防火墙、压缩)
二、事件循环
Event Loop
浏览器的事件循环是一个在 javascript 引擎和渲染引擎之间协调工作的机制。
因为 javascript 是单线程的,所以所有需要被执行的操作都需要通过一定的机制来协调它们有序的进行。
JS 代码在同一时刻只能做一件事情,如果这件事情非常耗时,就意味着当前线程被阻塞
所以真正耗时的操作比如网络请求、定时器等,是用其他线程操作的,只需要在特定的时机,执行对应的回调函数即可
目前多数浏览器是多进程的,每打开一个 tab 页面就会开启一个新进程,每个进程中又有多个线程,其中包括执行 JavaScript 代码的线程。
调用栈(Call Stack)和任务队列(Task Queue)
调用栈:存储在程序执行过程中创建的所有执行上下文,函数调用时它的执行上下文被压入栈,函数执行完毕,弹出。先进后出。
任务队列:存储待处理的事件,比如定时器到期、网络请求完成、点击等等,先进先出。(又分宏任务队列和微任务队列)
JavaScript 单线程任务被分为同步任务和异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。
当调用栈为空时,事件循环会从任务队列中取出任务执行
优先处理微任务队列,微任务队列清空后,开始执行宏任务。再执行下一个宏任务前会检查微任务队列,如有执行微任务。
宏任务是一个比较大的任务单位,可看作是一个独立的工作单元。
当一个宏任务执行完毕后,浏览器可以在两个宏任务之间进行页面渲染或处理其他事务(比如执行微任务)
微任务的执行优先级高于宏任务
MutationObserver:监视 DOM 变更的 API,在 Vue2 源码中也有使用它来实现微任务的调度
宏任务(MacroTasks):包括脚本(script)、setTimeout、setInterval、I/O、UI rendering、setImmediate(node 中)等
微任务(MicroTasks):包括 Promise.then、MutationObserver、process.nextTick(仅在 node 中)、queueMicrotask(显示创建微任务 API)等
1.执行当前宏任务 2.执行完当前宏任务后,检查并执行所有微任务。在微任务执行期间产生的新的微任务也会被连续执行,直到微任务队列清空。 3.渲染更新界面(如果有必要) 4.请求在一个宏任务,重复上述过程
执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去检查微任务(microTask)队列是否为空,如果为空的话,就执行 Task(宏任务),否则就一次性执行完所有微任务。
每次单个宏任务执行完毕后,检查微任务(microTask)队列是否为空,如果不为空的话,会按照先入先出的规则全部执行完微任务(microTask)后,设置微任务(microTask)队列为 null,然后再执行宏任务,如此循环。
由于微任务具有较高的执行优先级,它们适合用于需要尽快执行的小任务,比如处理异步的状态更新
宏任务适合用于分割较大的、需要较长时间执行的任务,以避免阻塞 UI 更新或其他高优先级的操作
以上是浏览器中的事件循环。
浏览器中的 EventLoop 是根据 HTML5 定义的规范来实现的,不同浏览器可能会有不同的实现。
而 Node 中的事件循环是由 libuv 实现的,这是一个处理异步事件的 C 库。
libuv 主要维护了一个 EventLoop 和 worker threads(线程池)
EventLoop 负责调用系统的一些其他操作:文件 IO、Network、child- processes 等
Node.js 的事件循环包含⼏个主要阶段,每个阶段都有⾃⼰的特定类型的任务(宏任务)
从上往下依次执行,然后再次回到顶部 就叫一次循环
对每个阶段进⾏详细的解释:
timers(定时器):这⼀阶段执⾏ setTimeout 和 setInterval 的回调函数。
Pending Callbacks(待定回调):executes I/O callbacks deferred to the next loop iteration(官⽅的解释)
这意味着在这个阶段,Node.js 处理⼀些上⼀轮循环中未完成的 I/O 任务。
具体来说,这些是⼀些被推迟到下⼀个事件循环迭代的回调,通常是由于某些操作⽆法在它们被调度的
那⼀轮事件循环中完成。
⽐如操作系统在连接 TCP 时,接收到 ECONNREFUSED(连接被拒绝)。
idle, prepare(空闲、准备):只⽤于系统内部调⽤。
poll(轮询):检索新的 I/O 事件;执⾏与 I/O 相关的回调。
检索新的 I/O 事件:这⼀部分,libuv 负责检查是否有 I/O 操作(如⽂件读写、⽹络通信)完成,并准备好了相应的回调函数。
执⾏ I/O 相关的回调:⼏乎所有类型的 I/O 回调都会在这⾥执⾏,除了那些特别由 timers 和 setImmediate 安排的回调以及某些关闭回调( close callbacks )。
check(检查): setImmediate() 的回调在这个阶段执⾏。
close callbacks(关闭回调):如 socket.on('close', ...) 这样的回调在这⾥执⾏。
我们会发现从⼀次事件循环的 Tick 来说,Node 的事件循环更复杂,它也分为微任务和宏任务:
宏任务(macrotask):setTimeout、setInterval、IO 事件、setImmediate、close 事件;
微任务(microtask):Promise 的 then 回调、process.nextTick、queueMicrotask;
但是,Node 中的事件循环中微任务队列划分的会更加精细:(又分成两个)
next tick queue:process.nextTick;
other queue:Promise 的 then 回调、queueMicrotask;
整体的执行顺序是:next tick > 微任务 > 宏任务
那么它们的整体执⾏时机是怎么样的呢?
- 调⽤栈执⾏:Node.js ⾸先执⾏全局脚本或模块中的同步代码。这些代码在调⽤栈中执⾏,直到栈被清空。
- 处理 process.nextTick() 队列:⼀旦调⽤栈为空,Node.js 会⾸先处理 process.nextTick() 队列中的所有回调。这确保了任何在同步执⾏期间通过 process.nextTick() 安排的回调都将在进⼊任何其他阶段之前执⾏。
- 处理其他微任务:处理完 process.nextTick() 队列后,Node.js 会处理 Promise 微任务队列。这些微任务包括由 Promise.then() 、 Promise.catch() 或 Promise.finally() 安排的回调。
然后
开始事件循环的各个阶段:(宏任务)
timers 阶段:处理 setTimeout() 和 setInterval() 回调。
I/O 回调阶段:处理⼤多数类型的 I/O 相关回调。
poll 阶段:等待新的 I/O 事件,处理 poll 队列中的事件。
check 阶段:处理 setImmediate() 回调。
close 回调阶段:处理如 socket.on('close', ...) 的回调。
这⾥有⼀个特别的 Node 处理:微任务在事件循环过程中的处理
- 在事件循环的任何阶段之间,以及在上述每个阶段内部的任何单个任务后
- Node.js 会再次处理 process.nextTick() 队列和 Promise 微任务队列。
- 这确保了在事件循环的任何时刻,微任务都可以优先并迅速地被处理。
描述 process.nextTick 在 Node.js 中事件循环的执⾏顺序,以及其与微任务的关系
在 Node.js 中, process.nextTick() 是⼀个在事件循环的各个阶段之间允许开发者插⼊操作的功能:
其特点是具有极⾼的优先级,可以在当前操作完成后、任何进⼀步的 I/O 事件(包括由事件循环管理的其他微任务)处理之前执⾏。
process.nextTick() 的执⾏顺序:
1.调⽤栈清空:Node.js ⾸先执⾏完当前的调⽤栈中的所有同步代码。 2.执⾏ process.nextTick() process.nextTick() 队列:⼀旦调⽤栈为空,Node.js 会检查 process.nextTick() 队列。
如果队列中有任务,Node.js 会执⾏这些任务,即使在当前事件循环的迭代中有其他微任务或宏任务排队等待。 3.处理其他微任务:在 process.nextTick() 队列清空之后,Node.js 会处理由 Promises 等产⽣的微任务队列。 4.继续事件循环:处理完所有微任务后,Node.js 会继续进⾏到事件循环的下⼀个阶段(例如 timers、I/Ocallbacks、poll 等)。
与微任务的关系:
- 优先级: process.nextTick() 创建的任务和 Promise 是不同的,但它们是⼀种微任务,并且在所有微任务中具有最⾼的执⾏优先级。这意味着 process.nextTick() 的回调总是在其他微任务(例如 Promise 回调)之前执⾏。
- 微任务队列:在任何事件循环阶段或宏任务之间,以及在宏任务内部可能触发的任何点,Node.js 都可能执⾏
process.nextTick() 。执⾏完这些任务后,才会处理 Promise 微任务队列。
process.nextTick() 的命名在 Node.js 社区中曾经引起过⼀些讨论,因为它可能会导致⼀些误解。
JS 代码下载好后,如何被一步步的执行的?
浏览器内核由两部分组成,以 webkit 为例:
WebCore 负责 HTML 解析、布局、渲染等相关操作
JavaScriptCore 负责解析、执行 JS 代码
JS 引擎非常多,介绍几个比较重要的:
SpiderMonkey:第一款 JS 引擎,由 Brendan Eich 开发(JS 作者),1996 年发布
Chakra:最初是 IE9 的 JS 引擎,并在后续成为了 Edge 浏览器的引擎,直到 Microsoft 转向 Chromium 架构并采用 V8
JavaScriptCore:是 Webkit 浏览器引擎的一部分,主要用于 Apple 的 Safari 浏览器,也被用在 IOS 设备中
V8:Chrome 浏览器、Node.js 的 JS 引擎
V8 引擎是用 C++编写的 Google 开源高性能 JavaScript 和 引擎,它用于 Node.js 和 Chrome 等
它实现 ECMASCript 和 WebAssembly,并在 Windows7 或更高版本,macOS10.12+和使用 x64,IA-32,ARM 或 MIPS 处理器的 Linux 系统上运行
V8 可以独立运行,也可以嵌入到任何 C++程序中
V8 执行 JS 代码的过程:
源代码先解析,转化成 AST(抽象语法树),然后交给 Ignition(解释器),解释器输出字节码(bytecode),同时 Ignition 也是能够运行字节码的(转成机器码),运行字节码后输出结果。在 Ignition 转换或者运行的时候就会搜集字节码的信息,由 TurboFan 将一些字节码转换成优化后的机器码,这样再运行这段代码的时候就可以直接使用优化后的机器码(MachineCode)无需再转换。
1.解析 (Parse):JavaScript 代码⾸先被解析器处理,转化为抽象语法树(AST)。这是代码编译的初步阶段,主要转换代码结构为内部可进⼀步处理的格式。
2.AST:抽象语法树(AST)是源代码的树形表示,⽤于表示程序结构。之后,AST 会被进⼀步编译成字节码。
3.Ignition:Ignition 是 V8 的解释器,它将 AST 转换为字节码。字节码是⼀种低级的、⽐机器码更抽象的代码,它可以快速执⾏,但⽐直接的机器码慢。 4.字节码(Bytecode):字节码是介于源代码和机器码之间的中间表示,它为后续的优化和执⾏提供了⼀种更标准化的形式。字节码是由 Ignition ⽣成,可被直接解释执⾏,同时也是优化编译器 TurboFan 的输⼊。
5.TurboFan:TurboFan 是 V8 的优化编译器,它接收从 Ignition ⽣成的字节码并进⾏进⼀步优化。⽐如如果⼀个函数被多次调⽤,那么就会被标记为热点函数,那么就会经过 TurboFan 转换成优化的机器码,提⾼代码的执⾏性能。当然还会包括很多其他的优化⼿段,如内联替换(Inlining)、死代码消除(Dead Code Elimination)和循环展开(Loop Unrolling)等,以提⾼代码执⾏效率。 6.机器码:经过 TurboFan 处理后,字节码被编译成机器码,即直接运⾏在计算机硬件上的低级代码。这⼀步是将 JavaScript 代码转换成 CPU 可直接执⾏的指令,⼤⼤提⾼了执⾏速度。 7.运⾏时优化:在代码执⾏过程中,V8 引擎会持续监控代码的执⾏情况。如果发现之前做的优化不再有效或者有更优的执⾏路径,它会触发去优化(Deoptimization)。去优化是指将已优化的代码退回到优化较少的状态,然后新编译以适应新的运⾏情况。
V8 引擎主要包含哪些部件?它们的作用是什么?
- Parse 模块会将 JS 代码转换成 AST(抽象语法树),这是因为解释器并不直接认识 JS 代码,如果函数没有被调用,那么是不会被转换成 AST 的。
- Ignition 是一个解释器,会将 AST 转换成 ByteCode(字节码),同时会收集 TurboFan 优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算),如果函数只调用一次,Ignition 会解释执行 ByteCode
- TurboFan 是一个编译器,可以将字节码编译为 CPU 可以直接执行的机器码如果一个函数被多次调用,那么就会被标记热点函数,就会经过 TurboFan 转换成优化的机器码,提高代码的执行性能。但是机器码实际上也会被还原成字节码,这是因为如果后续执行函数的过程中类型发生了变化(比如入参由 number 类型变成了 string 类型),之前优化的机器码并不能正确的处理运算,就会逆向转换成字节码。所以说 TS 在一定程度上也能优化我们的性能.
官方解析图,来自https://v8.dev/blog/scanner
那么我们的 JavaScript 源码是如何被解析(Parse 过程)的呢?
Blink 将源码交给 V8 引擎,Stream 获取到源码并且进⾏编码转换;
Scanner 会进⾏词法分析(lexical analysis),词法分析会将代码转换成 tokens(记号化);
接下来 tokens 会被转换成 AST 树,经过 Parser(语法分析器)和 PreParser:
Parser 就是直接将 tokens 转成 AST 树架构;
PreParser 称之为预解析,为什么需要预解析呢?
预解析⼀⽅⾯的作⽤是快速检查⼀下是否有语法错误,另⼀⽅⾯也可以进⾏代码优化。
这是因为并不是所有的 JavaScript 代码,在⼀开始时就会被执⾏。那么对所有的 JavaScript 代码进⾏解析,必然会影响⽹⻚的运⾏效率;
所以 V8 引擎就实现了 Lazy Parsing(延迟解析)的⽅案,它的作⽤是将不必要的函数进⾏预解析,
也就是只解析暂时需要的内容,⽽对函数的全解析是在函数被调⽤时才会进⾏;
⽐如我们在⼀个函数 outer 内部定义了另外⼀个函数 inner,那么 inner 函数就会进⾏预解析;
⽣成 AST 树后,会被 Ignition 转成字节码(bytecode)并且可以执⾏字节码,之后的过程就是代码
的执⾏过程(后续会详细分析)。
内存管理
JS 对于原始数据类型内存的分配会在执行时,直接在栈空间进行分配;
JS 对于复杂数据类型内存的分配会在堆内存中,开辟一块空间,并且将这块空间的指针返回值变量引用
因为内存的⼤⼩是有限的,所以当内存不再需要的时候,我们需要对其进⾏释放,以便腾出更多的内存空间。
在⼿动管理内存的语⾔中,我们需要通过⼀些⽅式⾃⼰来释放不再需要的内存,⽐如 free 函数:
但是这种管理的⽅式其实⾮常的低效,影响我们编写逻辑的代码的效率;
并且这种⽅式对开发者的要求也很⾼,并且⼀不⼩⼼就会产⽣内存泄露(memory leaks),指针(dangling pointers);
所以⼤部分现代的编程语⾔都是有⾃⼰的垃圾回收机制:
垃圾回收的英⽂是 Garbage Collection,简称 GC;
对于那些不再使⽤的对象,我们都称之为是垃圾,它需要被回收,以释放更多的内存空间;
⽽我们的语⾔运⾏环境,⽐如 Java 的运⾏环境 JVM,JavaScript 的运⾏环境 js 引擎都会内存 垃圾回收器;
垃圾回收器我们也会简称为 GC,所以在很多地⽅你看到 GC 其实指的是垃圾回收器;
⾃动垃圾回收提⾼了开发效率,使开发者可以更多地关注业务逻辑的实现⽽⾮内存管理的细节。
这在管理复杂数据结构和⼤数据时⾮常重要。
但是这⾥⼜出现了另外⼀个很关键的问题:GC 怎么知道哪些对象是不再使⽤的呢?
这⾥就要⽤到 GC 的实现以及对应的算法;
常⻅的 GC 算法 – 引⽤计数(Reference counting)
引⽤计数垃圾回收(Reference Counting):
每个对象都有⼀个关联的计数器,通常称为“引⽤计数”。
当⼀个对象有⼀个引⽤指向它时,那么这个对象的引⽤就+1;
如果另⼀个变量也开始引⽤该对象,引⽤计数加 1;如果⼀个变量停⽌引⽤该对象,引⽤计数减 1。
当⼀个对象的引⽤为 0 时,这个对象就可以被销毁掉;
这个算法有⼀个很⼤的弊端就是会产⽣循环引⽤,当然我们可以通过⼀些⽅案,⽐如弱引⽤来解决(WeakMap 就
是弱引⽤);
常⻅的 GC 算法 – 标记清除(mark-Sweep)
标记清除:
标记清除的核⼼思路是可达性(Reachability)
这个算法是设置⼀个根对象(root object),垃圾回收器会定期从这个根开始,找所有从根开始有引⽤到的对象,对于哪些没有引⽤到的对象,就认为是不可⽤的对象;
在这个阶段,垃圾回收器标记所有可达的对象,之后,垃圾回收器遍历所有的对象,收集那些在标记阶段未被标记为可达的对象。
这些对象被视为垃圾,因它们不再被程序中的其他活跃对象或根对象所引⽤。
这个算法可以很好的解决循环引⽤的问题;
JS 引擎⽐较⼴泛的采⽤的就是可达性中的标记清除算法,当然类似于 V8 引擎为了进⾏更好的优化,它在算法的实现细节上也会结合⼀些其他的算法。
标记整理(Mark-Compact) 和“标记-清除”相似;
不同的是,回收期间同时会将保留的存储对象搬运汇集到连续的内存空间,从⽽整合空闲空间,避免内存碎⽚化;
分代收集(Generational collection)—— 对象被分成两组:“新的”和“旧的”。
许多对象出现,完成它们的⼯作并很快死去,它们可以很快被清理;
那些⻓期存活的对象会变得“⽼旧”,⽽且被检查的频次也会减少;
增量收集(Incremental collection)
如果有许多对象,并且我们试图⼀次遍历并标记整个对象集,则可能需要⼀些时间,并在执⾏过程中带来明显的延迟。
所以引擎试图将垃圾收集⼯作分成⼏部分来做,然后将这⼏部分会逐⼀进⾏处理,这样会有许多微⼩的延迟⽽不是⼀个⼤的延迟;
闲时收集(Idle-time collection)
垃圾收集器只会在 CPU 空闲时尝试运⾏,以减少可能对代码执⾏的影响。
事实上,V8 引擎为了提供内存的管理效率,对内存进⾏⾮常详细的划分:
新⽣代空间 (New Space / Young Generation)
作⽤:主要⽤于存放⽣命周期短的⼩对象。这部分空间较⼩,但对象的创建和销毁都⾮常频繁。
组成:新⽣代内存被分为两个半空间:From Space 和 To Space。
初始时,对象被分配到 From Space 中。
使⽤复制算法(Copying Garbage Collection)进⾏垃圾回收。
当进⾏垃圾回收时,活动的对象(即仍然被引⽤的对象)被复制到 To Space 中,⽽⾮活动的对象(不再被引⽤的对象)被丢弃。
完成复制后,From Space 和 To Space 的⻆⾊互换,新的对象将分配到新的 From Space 中,原 ToSpace 成为新的 From Space。
⽼⽣代空间 (Old Space / Old Generation)
作⽤:存放⽣命周期⻓或从新⽣代晋升过来的对象。
当对象在新⽣代中经历了⼀定数量的垃圾回收周期后(通常是⼀到两次),且仍然存活,它们被认为是⽣命周期较⻓的对象。
分为三个主要区域:
⽼指针空间 (Old Pointer Space):主要存放包含指向其他对象的指针的对象。
⽼数据空间 (Old Data Space):⽤于存放只包含原始数据(如数值、字符串)的对象,不含指向其他对象的指针。
⼤对象空间 (Large Object Space):⽤于存放⼤对象,如超过新⽣代⼤⼩限制的数组或对象。
这些对象直接在⼤对象空间中分配,避免在新⽣代和⽼⽣代之间的复制操作。
代码空间 (Code Space) :存放编译后的函数代码。
单元空间 (Cell Space):⽤于存放⼩的数据结构如闭包的变量环境。
属性单元空间 (Property Cell Space):存放对象的属性值
主要针对全局变量或者属性值,对于访问频繁的全局变量或者属性值来说,V8 在这⾥存储是为了提⾼它的访问效率。
映射空间 (Map Space):存放对象的映射(即对象的类型信息,描述对象的结构)。
当你定义⼀个 Person 构造函数时,可以通过它创建出来 person1 和 person2。
这些实例(person1 和 person2)本身存储在堆内存的相应空间中,具体是新⽣代还是⽼⽣代取决于它们的⽣命周期和⼤⼩。
每个实例都会持有⼀个指向其映射的指针,这个映射指明了如何访问 name 和 age 属性(⽬的是访问属性效果变⾼)。
堆内存 (Heap Memory) 与 栈 (Stack)
堆内存:JavaScript 对象、字符串等数据存放的区域,按照上述分类进⾏管理。
栈:⽤于存放执⾏上下⽂中的变量、函数调⽤的返回地址(继续执⾏哪⾥的代码)等,栈有助于跟踪函数调⽤的顺序和局部变量。
一些题目:
什么是垃圾回收机制?并且它是如何在现代编程语⾔中管理内存的?
虽然随着硬件的发展,⽬前计算机的内存已经⾜够⼤,但是随着任务的增多依然可能会⾯临内存紧缺的问题,因此管理内存依然⾮常重要。(前提)
垃圾回收(Garbage Collection, GC)是⾃动内存管理的⼀种机制,它帮助程序⾃动释放不再使⽤的内存。在不需要⼿动释放内存的现代编程语⾔中,垃圾回收机制扮演着⾮常重要的⻆⾊,通过⾃动识别和清除“垃圾”数据来防⽌内存泄漏,从⽽管理内存资源。(作⽤)
在运⾏时,垃圾回收机制主要通过追踪每个对象的⽣命周期来⼯作。(原理)
对象通常在它们不再被程序的任何部分引⽤时被视为垃圾。
⼀旦这些对象被识别,垃圾回收器将⾃动回收它们占⽤的内存空间,使这部分内存可以重新被分配和使⽤。
垃圾回收机制有⼏种不同的实现⽅法,最常⻅的包括(实现,可以先回答⼏种,表示你对 GC 的理解,等下再回答
V8 的内容):
引⽤计数:每个对象都有⼀个与之关联的计数器,记录引⽤该对象的次数。当引⽤计数变为零时,意味着没有任何引⽤指向该对象,因此可以安全地回收其内存。
标记-清除:这种⽅法通过从根对象集合开始,标记所有可达的对象。所有未被标记的对象都被视为垃圾,并将被清除。
标记-整理:与标记-清除相似,但在清除阶段,它还会移动存活的对象,以减少内存碎⽚。
V8 引擎的垃圾回收机制具体是如何⼯作的?
V8 引擎使⽤了⼀种⾼度优化的垃圾回收机制来管理内存采⽤了标记-清除、标记整理,同时⼜结合了多种策略来实现⾼效的内存管理,包括结合了分代回收(Generational Collection)和增量回收(Incremental Collection)等多种策略。
分代回收:V8 将对象分为“新⽣代”和“⽼⽣代”。新⽣代存放⽣命周期短的⼩对象,使⽤⾼效的复制式垃圾回收算法;⽽⽼⽣代存放⽣命周期⻓或从新⽣代晋升⽽来的对象,使⽤标记-清除或标记-整理算法。这种分代策略减少了垃圾回收的总体开销,尤其是针对短命对象的快速回收。
增量回收:为了减少垃圾回收过程中的停顿时间,V8 实现了增量回收。这意味着垃圾回收过程被分解为许多⼩步骤,这些⼩步骤穿插在应⽤程序的执⾏过程中进⾏。这有助于避免⻓时间的停顿,改善了应⽤程序的响应性和性能。
延迟清理和空闲时间收集:V8 还尝试在 CPU 空闲时进⾏垃圾回收,以进⼀步减少对程序执⾏的影响。
这些技术的结合使得 V8 能够在执⾏ JavaScript 代码时有效地管理内存,同时最⼩化垃圾回收对性能的影响。
JavaScript 有哪些操作可能引起内存泄漏?如何在开发中避免?
在 JavaScript 中,内存泄漏通常是指程序中已经不再需要使⽤的内存,由于某些原因未被垃圾回收器回收,从⽽导
致可⽤内存逐渐减少。
这些内存泄漏通常是由不当的编程实践引起的,常⻅的引起内存泄漏的操作有如下情况:
全局变量滥⽤:创建的全局变量(例如,忘记使⽤ var , let , 或 const 声明变量)可能会导致这些变量不被回收。
未清理的定时器和回调函数:⽐如使⽤ setInterval 在不适⽤时没有及时清除,阻⽌它们被回收。
闭包:闭包可以维持对外部函数作⽤域的引⽤,如果这些闭包⼀直存活,它们引⽤的外部作⽤域(及其变量)也⽆法被回收。
DOM 引⽤:JavaScript 对象持有已从 DOM 中删除的元素的引⽤,这会阻⽌这些 DOM 元素的内存被释放。
监听器的回调:在使⽤完毕后没有从 DOM 元素上移除事件监听器,这可能导致内存泄漏。
开发中如何避免呢?重要的是平时在开发中就要尽量按照规范来编写代码。
使⽤局部变量:尽量使⽤局部变量,避免⽆限制地使⽤全局变量。
及时清理:使⽤ clearInterval 或 clearTimeout 取消不再需要的定时器。
优化闭包的使⽤:理解闭包和它们的⼯作⽅式。只保留必要的数据在闭包中,避免循环引⽤。
谨慎操作 DOM 引⽤:当从 DOM 中移除元素时,确保删除所有相关的 JavaScript 引⽤。包括 DOM 元素监听器的移除。
⼯具和检测:利⽤浏览器的开发者⼯具进⾏性能分析和内存分析。
代码审查:定期进⾏代码审查,关注那些可能导致内存泄漏的编程实践,⽐如对全局变量的使⽤、事件监听器的添加与移除等。
JS 代码在内存中的运行原理,作用域链是怎么产生的
基于 ES3 文档:
首先,JS 引擎会在执行任何代码之前,在堆内存中创建一个全局对象(GO),所有作用域都可以访问该对象,里面包含 Date、Array、Number、setTimeout 等等,其中还有一个 window 属性指向自己。
js 引擎会创建执行上下文(Execution Contexts)。
js 引擎内部有一个执行上下文栈(EC Stack)也可称调用栈。
现在他要执行的就是全局代码块,它会先创建一个全局执行上下文(GEC),GEC 被放入到 ECS 中。
准备执行时包含两部分内容:
第一部分,在代码执行前,在 parser 转成 AST 的过程中,会将全局定义的变量、函数等加入到 GO 中,但并不会赋值,这个过程就叫做变量作用域提升。
第二部分,在代码执行中,对变量赋值,或执行其他函数。
注:扫描到函数时,会在堆中创建这个函数对象,它的内存地址被保存在 GO 中,此时函数的作用域也被确定(当前所处环境),但不会创建内部函数的对象(如果有。
每一个执行上下文都会关联一个 VO(Variable Object,变量对象),变量和函数声明会被添加到这个 VO 对象中,全局执行上下文中 VO=GO
执行上下文有三个重要内容:作用域链、this、VO
执行代码时,遇到函数时,就会为它创建新的执行上下文(FEC)并压入栈。
执行上下文中的 VO 对应的是在堆中创建一个新的 AO(激活对象)对象。
这个 AO 对象会使用 arguments 作为初始化,初始值是传入的参数。里面的变量会在执行前变量提升不赋值,随后执行,开始赋值。
注意:此时创建的 AO 对象是在执行到函数时创建的,和全局代码执行前创建的函数对象不是一个对象。之前函数对象里记录的作用域就会被放到函数执行上下文中的作用域链中。
作用域链是一个对象列表,在代码执行时,优先在自己 VO 中查找变量。不会先去作用域链上查找。
解释了当一个方法中有某个变量的定义,外面作用域中有同名变量,console.log 在函数内定义变量前打印,会打印 undefined 的原因。
主要作用应该是为了实现闭包。
闭包的记忆性其实就是因为函数对象上的 scope 作用域记录了它定义时所处的作用域环境,当外层函数执行完毕后并且弹栈,因为内层函数的作用域还记录着外层函数的 AO 对象,所以外层函数的 AO 对象不会被销毁。
闭包不是 js 特有的,不是 js 初创的。
一个函数和对其周围状态(词法环境)的引用捆绑在一起,这样的组合就是闭包。
也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。
在 js 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
所以,从广义的角度来说,js 中的函数+外层作用域都是闭包。从狭义的角度来说,js 中的函数,访问了外层作用域的变量,它的组合就是一个闭包。
注:V8 引擎的优化:
AO 对象不会被销毁时,是否里面所有的属性都不会被释放? 答案:没用到的属性就会被释放
闭包形成之后,函数没有用到的外部作用域的属性就会被销毁。
ES5 及以后代码执行发生了一些变化
基本思路相同,只是对于一些词汇的描述发生了改变。执行上下文栈和执行上下文也是相同的
在执行上下文中,以前的全局代码 VO(变量对象)对应着 GO,现在变成了词法环境(LE),属性、方法存储在由环境记录(ER)对应的对象里面。(用于处理 let/const 声明的标识符)
【LE -> ER -> 对象中存储属性、方法】
和 ER 平级的还有一个叫做外部词法环境,作用域链通过它链接,全局的词法环境因为是最顶层作用域了,所以指向 null【类似以前的 scope】。查找变量时先找词法环境再找变量环境。
和 LE 平级的变量环境(VE),它也对应一个 ER,存放处理 var 和 function 声明的标识符。LE 和 VE 处于执行上下文中。
LE 和 VE 对应环境记录(ER),环境记录内又分为声明式环境记录和对象式环境记录。一般声明的变量和方法都在声明式环境记录中。
最新:
ES2023 开始又发生了一些变化
执行上下文关联的词法环境和变量环境都是一个环境记录
全局执行上下文关联一个全局环境记录,全局环境记录中包含一个声明环境记录(let/const 属性)和一个对象环境记录(window 对象/var/function)
函数的执行上下文关联的环境记录中只有一个声明环境记录,和声明环境记录平级的是外部环境记录,存储作用域。
关于重复声明的解释:
一个声明式环境记录如何区分 var 变量和 let/const 变量?
CreateMutableBinding:用于创建 var 变量的可变绑定
CreateImmutableBinding:用于创建 let 和 const 变量的不可变绑定
不可变绑定就是说一旦这个名字和变量关联后,它们的绑定关系不可改变
一些题目:
什么是变量提升?
变量提升是 javascript 中的一个行为,它使得函数声明和变量声明(var 声明的变量)在代码执行前被提升到其作用域的顶部。
- 仅仅是声明被提升,赋值仍然会在代码中声明的位置执行。
为什么存在变量提升?
在 javascript 早期版本中,解释器会通过两个阶段处理代码:编译阶段和执行阶段
在编译阶段,解释器会先读取并处理所有的声明,在执行阶段,才会处理实际的代码。
这种设计使得在同一作用域内的变量和函数可以在声明之前被引用。提供了一定的灵活性。
然而,这也可能导致代码运行结果不直观,这其实是 JavaScript 早期设计的一种缺陷。因此现代 JavaScript(ES6 及以后)引入了 let 和 const 关键字,这两者不会发生变量声明提升,使代码更加可靠和利于理解。
变量声明提升有哪些潜在的缺点?
变量覆盖问题:在同一作用域内,如果不小心重复声明变量,由于变量声明提升,后面的声明会覆盖前面的声明。
意外行为:可能导致逻辑上的错误
函数声明的混淆:函数提升意味函数声明可以在函数实际定义之前调用。可能导致同名覆盖
可读性和维护性降低:可能导致代码逻辑难以理解。
在 JS 中有以下几种常见作用域: 1.全局作用域:当变量在代码中的任何函数外部声明时,它就拥有全局作用域。意味着任何代码的任何部分都可以访问这些全局变量,全局作用域的变量在页面关闭前一直存在,过多的全局变量可能导致命名冲突问题。 2.函数作用域:在函数内部声明的变量具有函数作用域,这些变量只能在函数内部被访问,函数的参数也具有函数作用域。 3.块级作用域:使用 let 和 const 声明的变量具有块级作用域,这些变量尽在其包含的{}大括号中可被访问。是 ES6 的新增特性,在循环或者条件语句中很有用 4.模块作用域:在 ES6 模块中,顶层声明的变量、函数、类等不是全局的,而是模块内部的。这些声明仅在模块内部可用,除非被导出。
作用域链
作用:用于确定当前执行代码的上下文中变量的查找和访问机制
作用域链的构建基于词法作用域的结构,即变量和函数的可见性由它们在代码中的位置决定。
每个执行上下文都关联一个作用域链
作用域链是一个包含多个环境记录的列表
是闭包的核心组成和前提
var、let、const
let、const 不可重复声明
let、const 有块级作用域
let、const 也会变量声明提升,但它有暂时性死区,在作用域内变量没被赋值的时候是禁止访问的,报 ReferenceError
const 声明的是常量不可重新赋值
早期处理异步的主要方式就是使用回调函数,那个时候相对复杂的异步处理常常会出现回调地狱。
为了解决回调地狱 JS 提供了 Promise 和 async/await
Promise
有三个状态,而且只会改变一次。
待定(pending)初始状态,已完成(fulfilled),已拒绝(rejected)
new Promise((resolve, reject) => {
// executor
resolve(); // 当调用resolve函数时,会执行then方法传入的回调函数
// 当调用reject函数时,会执行catch方法传入的回调函数
})
.then((res = {}))
.catch((err) => {});
resolve()传入不同值的区别:
一、传入普通值或者对象,那么这个值就会作为 then 回调的参数
二、传入的是另外一个 Promise,那么这个新 Promise 会决定原 Promise 的状态
三、传入的是一个对象,并且这个对象有 then 方法,那么会执行该 then 方法,并根据 then 方法的结果来决定 Promise 的状态
// 第三种传参,代码示范
new Promise((resolve, reject) => {
resolve({
then: function (resolve, reject) {
resolve("thenable value");
},
});
}).then((res) => {
console.log(res);
});
finally()是 ES9 新增的,无论成功还是失败都会调用
Promise.all() 将多个 Promise 包裹在一起形成一个新的 Promise,新的 Promise 的状态有被包裹的所有 Promise 决定。
当所有 Promise 都变成 fulfilled 时,新 Promise 变成 fulfilled,并将所有 Promise 返回值组成数组
当有一个 Promise 变成 reject 时,新的 Promise 变成 reject,并将第一个 reject 的返回值作为参数
all 方法有一个问题:当有其中一个 Promise 状态变成 reject 时,新 Promise 就会立即变成 reject 状态
那么对于已经完成的以及处于 pending 状态的 Promise,是获取不到对应结果的。
在 ES11 中,添加了新的 API:Promise.allSettled
该方法会在所有的 Promise 都有结果后(无论成功失败)才会有最终状态
而且这个 Promise 的结果一定是 fulfilled
返回的结果是一个数组,存放着每个 Promise 的结果,结果是个对象包含 status 状态和 value 值
race 方法是当任意一个率先有结果,就返回
any 方法是 ES12 新增方法,和 race 类似
any 方法会等到一个 fulfilled 状态,才会决定新 Promise 状态,只要有一个成功就会终止。
如果所有结果都是 reject,那么也会等到所有 Promise 都变成 rejected,都是 reject 会报 AggregateError 错误
类方法:Promise.resolve()、Promise.reject()
Promise.resolve("xxx");
// 等价于
new Promise((resolve) => resolve("xxx"));
Promise.reject(); //同理
then 方法内返回的内容会被 Promise 包裹,默认返回成功态
手写 Promise:https://github.com/coderwhy/HYPromise
生成器 Generator
是 ES6 新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等。
(平时函数终止的条件:执行完、return、throw 错误)
生成器函数在定义时需要在 function 关键字后面加一个符号 *
通过 yield(一般翻译成产出)关键字来控制函数的执行流程
生成器函数的返回值是一个生成器对象,可以反过来操作生成器函数
function* foo() {
console.log("111");
yield "可选产出值";
console.log("222");
yield;
console.log("333");
yield;
// 默认return undefined
}
const gen = foo();
// 此时不会有任何打印输出
// 让它执行第一行输出111
const res = gen.next(); // 可以拿到可选产出值
//让它输出222
gen.next(); // 可以传值,函数yield可以接收到
生成器事实上是一种特殊的迭代器
迭代器可以使用户在容器对象上遍历,其行为像数据库中的光标。
const testArr = ["abc", "a", "b", "c"];
// 给数组创建一个迭代器(一个next方法)
// next方法是一个无参数或者一个参数的函数,返回一个对象拥有done(boolean)属性和value属性。
// 如果迭代器已经将序列迭代完毕done属性需要为true
// 封装一个函数用来迭代数组
function arrIterator(arr) {
let index = 0;
return {
next: function () {
if (index < arr.length) {
return { done: false, value: arr[index++] };
} else {
return { done: true, value: undefined };
}
},
};
}
arrIterator(testArr).next(); // 遍历
可迭代对象
默认对象是不可迭代的,但如果将迭代器方法内置到对象里面,那么这个对象就变成了可迭代对象
添加 [Symbol.iterator]函数作为属性,返回迭代器对象(内有 next 方法)用于迭代当前对象。
可迭代对象可进行 for...of 操作
const info = {
name: "red",
age: 18,
sex: "male",
[Symbol.iterator]: function () {
const values = Object.values(this);
let index = 0;
const iterator = {
next: function () {
if (index < values.length) {
return { done: false, value: values[index++] };
} else {
return { done: true };
}
},
};
return iterator;
},
};
console.log(...info); // red 18 male
从生成器到 async/await
async 函数中如果有返回值,返回一个 Promise.resolve 包裹的值
如果返回值是 Promise,那么 Promise.resolve 的状态由 Promise 决定【它的微任务会被延迟两次(跳过两个个微任务)】
如果返回值是一个对象并实现了 thenable(接口对象),那么由 then 方法决定【它的微任务会被延迟一次(仅跳过一个微任务)】
await 会等到 Promise 的状态变成 fulfilled 状态
比 Promise 编写更加方便,更好的可读性和可维护性,在 Promise 中错误处理通过.catch()方法进行,在 async/await 中使用 try..catch 来捕获并处理
监听对象的属性
// 遍历所有属性,对每一个属性使用defineProperty
const keys = Object.keys(obj);
for (const key of keys) {
let value = obj[key];
Object.defineProperty(obj, key, {
set: function (newValue) {
console.log("监听设置新值");
value = newValue;
},
get: function () {
console.log("监听获取");
return value;
},
});
}
但这样做有什么缺点?
首先 Object.defineProperty 设计的初衷并不是为了去监听一个对象中所有属性的。然后,它只能监听属性的修改和获取,无法监听新增属性和删除属性操作。
在 ES6 中新增了一个 Proxy 类,用于帮助我们创建一个代理
也就是说,当我们想监听一个对象时,我们可以先创建一个代理对象(Proxy 对象),之后对该对象的所有操作都通过代理对象完成。
const objProxy = new Proxy(obj, {
set: function (target, key, newValue) {
console.log(`监听${key}的设置值${newValue}`);
target[key] = newValue;
},
get: function (target, key) {
console.log(`监听${key}的获取`);
return target[key];
},
});
// 对obj的所有操作,应该去操作代理对象 objProxy
新增属性也会被 set 拦截
Proxy 除了 set、get 外还有许多捕获器,可以在网上查
Proxy 也可以监听函数对象
Reflect 是 ES6 新增 API,它是一个对象,字面意思是反射
它主要是提供了很多操作 JS 对象的方法,有点像 Object 中操作对象的方法
比如:Reflect.getPrototypeOf(target)
类似于Object.getPrototypeOf()
,
Reflect.defineProperty(target,propertyKey,attributes)
类似于Object.defineProperty()
为什么需要 Reflect?
早期 ECMA 规范中没有考虑到这种对 “对象本身” 的操作如何设计更规范,所以将这些 API 都放到了 Object 上
但 Object 作为一个构造函数,这些操作放到它身上是不合适的
另外还有一些类似于 in、delete 的操作符,让 JS 看起来有些奇怪
使用 Proxy 时,可以做到不操作原对象
// 原本操作
delete obj.name;
if (obj.name) {
console.log("没有删除成功");
}
// Reflect
if (Reflect.deleteProperty(obj, "name")) {
console.log("删除成功");
}
Reflect 的常见方法可以在 MDN 上查看
使用 Reflect.set 方法去替换 Proxy 中 set 对原对象赋值的操作
set: function(target, key, newValue, receiver) {
// 老方法 target[key] = newValue
// 新方法的好处:不再操作原对象,而且有返回值可以判断是否操作成功
if (Reflect.set(target, key, newValue)){
console.log("设置成功")
}
}
Proxy 中 set/get 方法最后一个参数:receiver 就是代理对象自己。可决定对象访问器 setter/getter 的指向。便于代理拦截
ES6 中使用了 class 关键字定义类
当我们通过 new 关键字调用一个 Person 类时,默认调用 class 中的 constructor 方法
实例方法和 constructor 方法平级直接写就可以,eat(){}
类方法也叫静态方法用 static 关键字定义
高内聚 低耦合
继承关键字 extends
父类的属性需要在子类中调用 super(参数),super 关键字也可在子类的实例方法、静态方法中使用
可继承内置类,使用一些内置类的方法
类的混入 mixin,JS 的类只支持单继承
babel 将 ES6 转 ES5,有时间可以再好好看看经 babel 转换后的源码
ES6 对象字面量增强:计算属性名[key]:value
、属性增强、方法增强
解构:数组按位置解构,对象按 key。
手写个 apply:
Function.prototype.apy = function (targetThis) {
Object.definProperty(targetThis, "fn", {
configurable: true,
value: this,
});
targetThis.fn();
delete targetThis.fn;
};
|| 和 ?? 的区别:A ?? B 只有 A = undefined 或 null 时 才会返回 B
箭头函数无显示原型
严格模式下 0 开头的八进制数字不合法,所以在 ES6 中,2 进制用 0b 开头,8 进制用 0o 开头,16 进制用 0x 开头
Symbol 生成一个独一无二的值,常用来做对象的 key。对象 key 只能是字符串或者 Symbol 如果是其他值,也会转成字符串。使用 Object.keys 获取不到 symbol 的键,需要使用专用方法:Object.getOwnPropertySymbols
Symbol.for("描述") 相同描述在 for 方法中传,才会生成相同的值。值.description 拿到传入的描述。keyFor 获取 for 方法中的描述
Set/Map、WeakSet/WeakMap
Set 元素不可重复
Set/Map 中的 NaN 自等(会被去重)
// 数组去重
const oldArr = [1, 22, 33, 44, 22, 1];
const newArr = [];
for (const i of oldArr) {
if (!newArr.includes(i)) {
newArr.push(i);
}
}
// 方法二
const oldArr = [1, 22, 33, 44, 22, 1];
const newArr = Array.from(new Set(oldArr));
.add(元素) 添加元素、.size 获取长度、.delete(元素) 删除某个元素、.has(元素) 是否包含某个元素、clear 清空、forEach 遍历,也可用 for...of
WeakSet 中只能存放对象类型,不能放基本类型,并且对元素是弱引用,如果元素没有其他引用则会呗 GC 回收。不可遍历
map 映射 相较对象,map 可以用复杂类型做键
.set(key, value) 添加、 get()获取 、delete() 删除、has()判断、clear()清空、forEach 遍历,for...of 遍历时须结构数组
ES8
Object.keys/Object.values/Object.entries
返回由对象的键/值/[键,值]组成的数组
padStart(一共几位,用什么填充)/padEnd 填充,应用场景 时间 日期 填充 0
async/await
ES10
flat 和 flatMap
flat 将一个数组,按照指定深度进行遍历,将遍历到的元素和子数组中的元素组成一个新数组返回(将数组扁平化),也可用递归实现
flatMap 首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。(先进行 map 操作在进行 flat,它的深度是 1)
Object.fromEntries(entries) 将 Object.entries(对象)返回的值转回对象
str.trimStart/trimEnd 去除首/尾空格 str.trim()首尾都去
ES11
空值合并运算符 ?? 只有 undefined 和 null 时会认定 false
可选链 obj?.属性
ES12
FinalizationRegistry 对象可以让你在对象被垃圾回收时请求一个回调
WeakRefs 弱引用
当普通的赋值方法将对象赋值给一个变量时,就形成了强引用,导致我们必须手动将变量指向 null 才可以让对象被垃圾回收。
const infoRef = obj.deref() // 拿到通过弱引用获取的对象
两秒后被释放
逻辑赋值运算符
msg ??= "默认值" // 更严谨
str = obj && obj.name // && 一般的应用场景
字符串 replaceAll 方法,一次替换所有匹配项。以前的 replace('查找项','替换项')方法只能替换第一个匹配项
ES13
method.at() 在数组中或者字符串中,arr.at(1)就等于 arr[1]用来取值
Object.hasOwn(对象,属性) 替代 obj.hasOwnProperty(属性),老方法是实例调用的方法,它放在 Object 的原型上。 1.防止被重写;2.如果用 Object.create(null)创建出来的对象,它隐式原型是 null 根本就没有 hasOwnProperty 方法
class 中的新成员
私有的对象属性和类属性
类中的静态代码块
static{} // 可以做类的初始化操作,它会是类中最先执行的代码
算数运算符没有什么难度(考点),这里只记一下自增、自减
// a++ 先用a原先的值,再自增1;++a先自增1,再提供自增后的值
var a = 1,
b = 2;
var c = b++; // 这里c的值是2,b自增后变成3
// 这里a++提供的是1,--b是2,++a因为前面已经+1所以是3,c是2
console.log(a++ + --b + ++a + c); // 所以结果是8
记一下&&和|| 的短路运算
var a = true && false;
// A && B 时:A真返回B 、 A假返回A
console.log(a); // false
var a = true || false;
// A || B 时:A真返回A 、 A假返回B
console.log(a); // true
==和===的区别
// == 比较值,如果类型不一样会自动转换后再比较
// '10' == 10 是true,因为将string类型的10转为了number
// 需注意,{}对象类型是无法通过这种简单转换的
console.log([] == false); // true
console.log(Boolean([])); // true 说明并不是将[]转为Boolean
// Number([]) 返回0,Number(false) 返回0
// 个人认为应该是通过Number()包装类统一转为数值型再比较。因为[]转String的话会输出空白行。
// 对象作为入参Number()返回NaN。注:NaN不自等
console.log({} == false); // false
// 对这方面了解透彻的小伙伴可以评论哈。
// === 是严格判断是否相等的运算符,会同时比较类型和值
// '10' === 10 是false
正则表达式
// 字面量写法
var regex1 = /abc/;
// 构造函数写法
var regex2 = new RegExp("abc");
console.log(regex1.test("abcd")); // true 因为abcd里包含了abc
// 如果希望被匹配的内容与规则完全相符(全字匹配)
var regex3 = /\babc\b/;
// \b匹配单词边界, \B匹配单词非边界,如果没有也是false
console.log(regex3.test("abcd")); // false
// \d 匹配数字 \D 匹配非数字
// [] 匹配指定范围内的任意一个,[^]如果加了^则取相反意思,不得含有方括号内容
// ^ 开头 $ 结尾
// + 号前面的第一条规则可以重复1次或者N次
var regex4 = /^go+gle$/;
console.log(regex4.test("gooooogle")); // true
// * 号前面的第一条规则可以重复0次或者N次
// ? 号前面的第一条规则可以重复0次或者1次
// {x} 表示前面的第一条规则可以重复x次
// {x,y} 表示前面的第一条规则可以被重复x至y次,{2,4}的意思就是可重复2\3\4次
// {x,} 表示至少x次
// () 可以用来指定多个字符组成字符串,作为一个规则
// | 表示在几个规则中有一个满足就可以返回true
// \ 如果匹配^*+等特殊符号,需要在前面加\做转译
canvas
<body>
<canvas id="canvas" width="800" height="600"></canvas>
<script>
// 获取画布元素
var canvas = document.getElementById("canvas");
// 获取二维绘图对象
var ctx = canvas.getContext("2d");
// 设置线宽
ctx.lineWidth = 3;
// 线条颜色
ctx.strokeStyle = "red";
// 填充颜色
ctx.fillStyle = "blue";
// 起点
ctx.moveTo(10, 10);
// 下一点
ctx.lineTo(100, 10);
// 下一点
ctx.lineTo(100, 100);
// 闭合点,完成三角形
ctx.lineTo(10, 10);
// fill()执行填充色 stroke()执行线条
ctx.stroke();
ctx.fill();
// ctx.beginPath() // 重新绘制
// ctx.closePath() //闭合
</script>
</body>
requestAnimationFrame 比 CSS 动画控制性会强一些,对性能消耗相对较大。
requestAnimationFrame 是由浏览器专门为动画提供的 API,在运行时浏览器会自动优化方法的调用,如果浏览器在后台运行或者该页面 tab 在后台运行时,动画会自动暂停。
动画每一帧的执行的间隔时间紧跟浏览器的刷新频率,动画更流畅,不会掉帧。
<body>
<div>
<button onclick="start()">开始</button>
<button onclick="stop()">停止</button>
</div>
<div class="box" id="box"></div>
<script>
var handle = 0; // 用来接收动画id,它是一个整数,取消动画时需要用
var step = 1; // 每次动画移动1个像素
var el; // 要应用动画的dom元素
window.onload = function () {
el = document.getElementById("box"); // 在页面加载完后获取box元素
};
function start() {
handle = requestAnimationFrame(move); // 开始动画
}
function move() {
el.style.left = step + "px";
step++;
handle = requestAnimationFrame(move); // 需要使用递归,才可以让动画持续
}
function stop() {
cancelAnimationFrame(handle); // 停止动画
}
</script>
</body>
深克隆
// 深克隆
let wm = new WeakMap()
function deepClone(obj) {
// 基本数据类型、null、函数的处理
// 注:typeof function 会返回'function'
if(obj === null || typeof obj !== 'object') {
// 函数处理不了,直接返回
return obj
}
// 处理回环对象,如果已处理过某对象,则直接返回不要再递归了
if(wm.has(obj)){
return wm.get(obj)
}
if (obj instanceof Map){
// 处理Map类型
var tmp = new Map()
wm.set(obj,tmp)
for(var [key,value] of obj){
tmp.set(deepClone(key), deepClone(value))
}
return tmp
} else if (obj instanceof Set) {
// 处理Set类型
var tmp = new Set()
wm.set(obj,tmp)
for(var value of obj){
tmp.add(deepClone(value))
}
return tmp
} else if (obj instanceof RegExp) {
// 处理RegExp 正则表达式
var tmp = new RegExp(obj)
wm.set(obj,tmp)
return tmp
} else {
// 处理数组、对象、Date
var tmp = new obj.constructor() // 每个对象实例都有一个constructor属性,指向创建该实例的构造函数。
wm.set(obj,tmp)
for(var key in obj){
tmp[key] = deepClone(obj[key])
}
return tmp
}
// 注:
JSON.parse(JSON.stringify(obj)) // 无法处理环型对象 而且不能处理包含函数、Set、Map等特殊数据结构。因为JSON不支持那些。
// 环形对象
obj1.a = obj2
obj2.a = obj1
Map 底层实现
Map 的底层实现通常涉及哈希表,哈希表是一种能够快速检索元素的数据结构。在 JavaScript 的 Map 实现中,键的哈希码是通过一个算法来生成的,而这个算法需要保证在键是可比较的情况下,相同的键总是有相同的哈希码。
class SimpleMap {
constructor() {
this.items = {};
}
set(key, value) {
const hash = this.hash(key);
if (!this.items[hash]) {
this.items[hash] = [];
}
this.items[hash].push([key, value]);
return this;
}
get(key) {
const hash = this.hash(key);
if (this.items[hash]) {
for (let i = 0; i < this.items[hash].length; i++) {
if (this.items[hash][i][0] === key) {
return this.items[hash][i][1]; // Value
}
}
}
return undefined;
}
delete(key) {
const hash = this.hash(key);
if (this.items[hash]) {
for (let i = 0; i < this.items[hash].length; i++) {
if (this.items[hash][i][0] === key) {
this.items[hash].splice(i, 1);
return true;
}
}
}
return false;
}
has(key) {
const hash = this.hash(key);
if (this.items[hash]) {
return this.items[hash].some(pair => pair[0] === key);
}
return false;
}
hash(key) {
let hash = 0;
let s = JSON.stringify(key);
for (let i = 0; i < s.length; i++) {
hash = (hash << 5) - hash + s.charCodeAt(i);
hash |= 0; // Convert to 32bit integer
}
return hash;
}
get size() {
let size = 0;
for (const key in this.items) {
if (this.items.hasOwnProperty(key)) {
size += this.items[key].length;
}
}
return size;
}
clear() {
this.items = {};
}
// Other methods like keys(), values(), entries(), forEach()
}
// 使用示例
const map = new SimpleMap();
map.set('key1', 'value1');
map.set('key2', 'value2');
console.log(map.get('key1')); // 输出: value1
console.log(map.has('key1')); // 输出: true
map.delete('key1');
console.log(map.has('key1')); // 输出: false
Promise.all 是一个用于并行执行多个 Promise 的方法,它接收一个 Promise 数组作为参数,并且当所有的 Promise 都成功解决时,它将以一个解决状态的数组作为参数,顺序与输入的 Promise 数组中的 Promise 顺序相同,返回一个新的 Promise。如果任何一个 Promise 失败,则立即停止等待,并返回一个失败的 Promise,失败的原因通常是第一个失败的 Promise 返回的值。
以下是一个简单的 Promise.all 实现的例子:
function myPromiseAll(promises) {
return new Promise((resolve, reject) => {
let results = [];
let remaining = promises.length;
if (remaining === 0) {
resolve(results);
return;
}
promises.forEach((promise, index) => {
Promise.resolve(promise).then(
(value) => {
results[index] = value;
if (--remaining === 0) {
resolve(results);
}
},
(reason) => {
reject(reason);
}
);
});
});
}
// 使用示例
const promises = [fetch("url1"), fetch("url2"), fetch("url3")];
myPromiseAll(promises)
.then((values) => {
console.log(values); // 当所有的 fetch 都完成时,会打印出结果数组
})
.catch((reason) => {
console.error("一个 promise 失败:", reason);
});
WebStorage 主要提供了一种机制,可以让浏览器提供一种比 cookie 更直观的 key、value 存储方式。
localStorage 本地存储,永久性存储,关闭网页也存在
sessionStorage 会话存储,提供本次会话的存储,关闭页面数据清除
搜索框的联想词可以用防抖
用户缩放浏览器的 resize 事件
监听浏览器滚动,完成某些特定操作
当事件触发时,相应的函数并不会立即触发,而是等待一定时间。
当事件密集触发时,函数的触发会被频繁的推迟
只有等待了一段时间,也没有再次触发函数,就会触发响应函数
防抖:
const inpEl = document.getElementById("inp");
inpEl.onclick = debounce(function () {
console.log("触发了事件");
}, 2000);
function debounce(fn, delay) {
let timer = null;
return function () {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.call(this);
timer = null;
}, delay);
};
}
节流:
不管触发的速度有多快,始终按照一定的频率来触发。
throttle
function throttle(fn, interval) {
let startTime = 0;
return function () {
const nowTime = new Date().getTime();
const waitTime = interval - (nowTime - startTime);
if (waitTime <= 0) {
fn();
startTime = nowTime;
}
};
}
请求头 content-type 表示这次请求携带的数据类型
- application/x-www-form-urlencoded 表示数据被编码成以'&'分隔的键 - 值对,同时以'=' 分隔键和值
- application/json 表示是 json 类型
- text/plain 表示文本类型
- application/xml 表示是 xml 类型
- multipart/form-data 表示上传的文件
accept-encoding 告知服务器,客户端支持的文件压缩格式,比如 js 文件可以用 gzip 编码,对应.gz 文件
响应状态码
具体可查看 MDN 文档地址:https://developer.mozilla.org/zh-CN/docs/web/http/status
GET 没有请求体,通过地址在请求头中携带数据,一般几 k,携带的参数如果是非英文就需要编码 POST 既可以通过地址在请求头中携带,也可以通过请求体携带数据,理论携带无限数据。一般 GET 会被浏览器缓存,POST 不会被缓存。301 表示永久重定向,302 表示临时重定向
cookie 的值如果包涵非英文字母,写入时要用 encodeURIComponent()编码,读取时用 decodeURIComponent()解码到期的 cookie 会被浏览器清除,如果没有设置失效时间,就叫会话 cookie,存在内存中,关闭浏览器时就清除
Domain 限定了访问 cookie 的范围,使用 js 只能读取当前域或者父域的 cookie。无法读取其他域的 cookie 想限定 cookie 在某路径下才能访问,用 path 属性。当 Name、Domain、Path 三个属性相同时才是同一个 cookie 设置了 HttpOnly 属性的 cookie 不能通过 js 去访问 Secure 限定了只有使用 https 的时候才可以发给服务端 1.前后端都可以创建 Cookie2.每个域名下的 Cookie 数量有限 3.每个 cookie 大概最多能存 4kb
localStorage 是本地存储,不会发送到服务端。
JSON 中没有 undefined(简单值)JSON.parse 将 json 字符串解析为 js 对象,JSON.stringify 相反
跨域:不同域名、端口、协议
CORS 跨域资源共享(老浏览器不支持 IE10 及以上) JSONP script
响应头中设置:Access-Control-Allow-Origin:*
意思就是允许所有跨域请求
JSONP 的原理就是 script 标签可以加载任何地方的资源不会被跨域限制
服务器端要准备好 JSONP 接口,是一个函数的调用,并传值 handleResponse({值}),函数名当前端请求是什么就变成什么
XHR 的属性 xhr.responseText 是文本形式获取到的响应数据在 send 前,xhr.response.Type = 'text' 意思是让响应数据以文本形式,可以设置为 xhr.responseType= 'json'这样返回值就是 JSON 格式,不能用 xhr.responseText 来获取了,而是 xhr.response,其实返回的还是 json 格式字符串并不是真的对象,只是浏览器处理了下但是 responseType 和 responseIE6-9 不支持 timeout 设置超时时间,单位毫秒 ms 在 open 之后,send 之前设置 xhr.timeout=10,如果超时会触发一个 timeout 事件,IE6-7 不支持,IE8 开始支持
withCredentials 属性指定使用 Ajax 发送请求时是否携带 cookie,默认同域会携带,跨域不携带。IE10 开始支持如果需要跨域时携带 xhr.withCredentials=true,最终能否成功还要看服务端服务端 CORS 配置就不能是*
,要指定具体的域名
XHR 的方法 abort()终止当前请求 有同名的 abort 事件,要在 send 之后调用 setRequestHeader(头部字段名,头部字段值)设置请求头信息,只有很少一部分可以自己设置。大部分头信息浏览器为了安全不让设置。在 open 后。send 前设置用来告诉服务器浏览器发送的数据是什么格式 setRequestHeader('Content-Type','application/x-www-form-urlencoded') //当是 post 想要发送类似 get 形式的参数(username=xxx&age=18)在 form 表单标签中的 enctype 里默认就是 application/x-www-form-urlencoded,可以把 ajax 伪装成表单,发送的形式就会是 FormData
// Ajax的使用步骤
//创建xhr对象
const url = "https://www.imooc.com/api/http/search/suggest?words=js";
const xhr = new XMLHttpRequest();
//为了兼容性更好,把这个监听事件放open前更好
//监听事件,处理响应,readystatechange监听readyState的变化
//readyState的值有0-4 五个状态
//0:未初始化,没有调用open
//1:启动,已经调用open,但未调用send
//2:发送,已经调用send,但未收到响应
//3:接收,已经接收到部分响应数据
//4:完成,已经接收到全部响应数据,而且已经可以在浏览器中使用
//readystatechange可以配合addEventListener使用,IE6-8不支持addEventListener
xhr.onreadystatechange = function () {
//如果状态不是4直接返回
if (xhr.readyState !== 4) return;
//判断http状态码
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
console.log("正常!", xhr.responseText);
} else {
console.log("错误!");
}
};
//准备发送请求,如果是get请求,参数在这步写,最后一个true是启用异步
xhr.open("GET", url, true);
//发送,这一步携带的参数是通过请求体携带,如果是get,最好写个null在send里,更好的兼容
//发对象的话要用JSON.stringify包一下
xhr.send(null);
XHR 的事件写在 open 前 load 事件:当响应数据可用时触发,从 IE9 开始支持,和 addEventListener 一样。error 事件:请求发生错误时触发(和响应没关系)IE10 开始支持 abort 事件:IE10 开始支持 timeout 事件:请求超时了就会触发 IE8 开始支持
fetch 它想为了替换 axios,但是它现在还不成熟,没有 abort、没有 timeout 等 fetch 基于 Promise,不用引入,原生就有 fetch(url).then(response=>{}).catch(err=>{})fetch(url,{配置项})有 method:,但是没有 params 参数,只能在 url 后面自己加,body 通过请求体发送,如果想在 body 中通过请求体传类似 URL 名值对的形式的数据,要 headers 参数中写Content-Type:application/x-www-form-urlencoded
,body 里面不能直接放对象要用 JSON.stringify 转一下,如果要跨域 mode:'cors',如果想携带 cookie 要写 credentials:'include'
body 里是数据流,只能被读一次,读一次后 bodyUsed 就变成 true 锁死了如果 ok 属性是 true 就可以直接用数据,不用判断 HTTP 状态码了 status 是状态码,statusText 是状态码的说明
使用 json 方法可以返回存储着响应数据的 promise 对象,所以要在 then 里获取,如果返回值是字符串可以用 text()
设计原则:
- 单一职责原则:一个类,应该仅有一个引起它变化的原因,简而言之就是功能要单一;
- 开放封闭原则:对拓展开放,对修改关闭;
- 接口隔离原则:一个接口应该是一种角色,不该干的事情不要干,该干的都要干,简而言之就是降低耦合、减低依赖;
工厂模式
工厂模式是由一个方法来决定到底创建哪个类的实例,而这些类经常拥有相同的接口。这种模式主要用在所实例化的类型在编译期不能确定,而是在执行期决定的情况。
分为简单工厂和工厂方法
简单工厂是将创建对象的步骤放在父类进行,工厂方法是延迟到子类中进行,它们两者都可以总结为:根据传入的字符串来选择对应的类
简单工厂
var UserFactory = function (role) {
function Admin() {
this.name = "管理员";
this.viewPage = ["首页", "查询", "权限管理"];
}
function User() {
this.name = "普通用户";
this.viewPage = ["首页", "查询"];
}
switch (role) {
case "admin":
return new Admin();
break;
case "user":
return new User();
break;
default:
throw new Error("参数错误,可选参数:admin、user");
}
};
var admin = UserFactory("admin");
var user = UserFactory("user");
工厂方法
// 安全模式创建的工厂方法函数
var UserFactory = function (role) {
if (this instanceof UserFactory) {
return new this[role]();
} else {
return new UserFactory(role);
}
};
// 工厂方法函数的原型中设置所有对象的构造函数
UserFactory.prototype = {
Admin: function () {
this.name = "管理员";
},
User: function () {
this.name = "用户";
},
};
// 调用
var admin = UserFactory("Admin");
var user = UserFactory("User");
构造器模式
在面向对象的编程语言中,构造器是一个类中用来初始化新对象的特殊方法。并且可以接受参数用来设定实例对象的属性的方法。
利用原型链上被继承的特性,实现了构造器:
function Car(model, year, miles) {
this.model = model;
this.year = year;
this.miles = miles;
}
// 覆盖原型对象上的toString
Car.prototype.toString = function () {
return `${this.model} has done ${this.miles} miles`;
};
// 使用
var civic = new Car("Honda Civic", 2009, 20000);
var mondeo = new Car("Ford Mondeo", 2010, 50000);
单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点。例如页面中的登陆弹窗、全局缓存等。
案例:假设要设置一个管理员,多次调用也仅设置一次,我们可以使用闭包缓存一个内部变量来实现这个单例。
function SetManager(name) {
this.manager = name;
}
SetManager.prototype.getName = function () {
console.log(this.manager);
};
var SingletonSetManager = (function () {
var manager = null;
return function (name) {
if (!manager) {
manager = new SetManager(name);
}
return manager;
};
})();
// 调试
SingletonSetManager("a").getName(); // a
SingletonSetManager("b").getName(); // a
原型模式
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,在 JS 中实现原型模式是在 ECMAScript5 中,提出的 Object.create 方法,使用现有的对象来提供新创建的对象的隐式原型(proto)
案例:使用现有的对象来提供创建的对象proto
var prototype = {
name: "Jack",
getName: function () {
return this.name;
},
};
var obj = Object.create(prototype, {
job: {
value: "IT",
},
});
console.log(obj.getName()); // Jack
console.log(obj.job); // IT
console.log(obj.__proto__ === prototype); //true
发布-订阅模式
发布-订阅模式,它定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在 JS 中通常使用注册回调函数的形式来订阅
优点:一为时间上的解耦,二为对象间解耦,可以用在异步编程中。
缺点:创建订阅者本身要消耗一定的时间和内存,订阅的处理函数不一定会被执行,驻留内存有性能开销,弱化了对象之间的联系,复杂的情况下可能会增加代码的可维护性。
class EventEmitter {
constructor() {
this.events = {};
}
// 订阅事件
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
// 触发事件
emit(eventName, ...args) {
if (this.events[eventName]) {
this.events[eventName].forEach((callback) => callback(...args));
}
}
// 取消订阅事件
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(
(cb) => cb !== callback
);
}
}
}
// 使用示例
const eventEmitter = new EventEmitter();
eventEmitter.on("message", (data) => {
console.log("Received message:", data);
});
eventEmitter.emit("message", "Hello, World!"); // 输出: Received message: Hello, World!
适配器模式
它的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。
适配器的别名是包装器(wrapper),这是一个相对简单的模式。在程序开发中有许多这样的场景:当我们试图调用模块或者对象的某个接口时,却发现这个接口的格式并不符合当前的需求。这时候有两种解决办法,第一种是修改原来的接口实现,但如果原来的模块很复杂,或者我们拿到的模块时一段别人编写的经过压缩的代码,修改原接口就显得太不现实了。第二种办法是创建一个适配器,将原接口转换为客户希望的另一个接口,客户只需要和适配器打交道。
实现一个简单的数据格式转换适配器:
// 渲染数据,格式限制为数组了
function renderData(data) {
data.forEach(function (item) {
console.log(item);
});
}
// 对非数组的进行转换适配
function arrayAdapter(data) {
if (typeof data !== "object") {
return [];
}
if (Object.prototype.toString.call(data) === "[object Array]") {
return data;
}
var temp = [];
for (var item in data) {
if (data.hasOwnProperty(item)) {
temp.push(data[item]);
}
}
return temp;
}
var data = {
0: "A",
1: "B",
2: "C",
};
renderData(arrayAdapter(data)); // A B C
装饰器模式
以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。
是一种“即用即付”的方式,能够在不改变对象自身的基础上,在程序运行期间给对象动态的添加职责。
为对象动态的加入行为,经过多重包装,可以形成一条装饰链。
最简单的装饰者,就是重写对象的属性
function Person() {}
Person.prototype.skill = function () {
console.log("数学");
};
// 装饰器,还会音乐
function MusicDecorator(person) {
this.person = person;
}
MusicDecorator.prototype.skill = function () {
this.person.skill();
console.log("音乐");
};
// 装饰器,还会跑步
function RunDecorator(person) {
this.person = person;
}
RunDecorator.prototype.skill = function () {
this.person.skill();
console.log("跑步");
};
var person = new Person();
// 装饰一下
var person1 = new MusicDecorator(person);
person1 = new RunDecorator(person1);
person.skill(); // 数学
person1.skill(); // 数学 音乐 跑步
代理模式
当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求作出一些处理后,再把请求转交给本体对象。代理和本体的接口具有一致性,本体定义了关键功能,而代理是提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。
代理模式主要有三种:保护代理,虚拟代理,缓存代理
保护代理主要实现了访问主体的限制行为,以过滤字符作为简单的例子:
// 主体,发送消息
function sendMsg(msg) {
console.log(msg);
}
// 代理,对消息进行过滤
function proxySendMsg(msg) {
// 无消息则直接返回
if (typeof msg === "undefined") {
console.log("deny");
return;
}
// 有消息则进行过滤
msg = ("" + msg).replace(/泥\s*煤/g, "");
sendMsg(msg);
}
sendMsg("泥煤呀泥 煤呀"); // 泥煤呀泥 煤呀
proxySendMsg("泥煤呀泥 煤"); // 呀
proxySendMsg(); // deny
它的意图很明显,在访问主体之前进行控制,没有消息的时候直接在代理中返回了,拒绝访问主体,这属于保护代理的形式。有消息的时候对敏感字符进行了处理,这属于虚拟代理的模式。
虚拟代理在控制对主体的访问时,加入了一些额外的操作,如在滚动事件触发的时候,也许不需要频繁触发,我们可以引入函数节流,这是一种虚拟代理的实现。
// 函数防抖,频繁操作中不处理,直到操作完成之后(再过 delay 的时间)才一次性处理
function debounce(fn, delay) {
delay = delay || 200;
var timer = null;
return function() {
var arg = arguments;
// 每次操作时,清除上次的定时器
clearTimeout(timer);
timer = null;
// 定义新的定时器,一段时间后进行操作
timer = setTimeout(function() {
fn.apply(this, arg);
}, delay);
}
};
var count = 0;
// 主体
function scrollHandle(e) {
console.log(e.type, ++count); // scroll
}
// 代理
var proxyScrollHandle = (function() {
return debounce(scrollHandle, 500);
})();
window.onscroll = proxyScrollHandle;
缓存代理可以为一些开销大的运算结果提供暂时的缓存,提升效率。来个栗子——缓存加法操作:
// 主体
function add() {
var arg = [].slice.call(arguments);
return arg.reduce(function(a, b) {
return a + b;
});
}
// 代理
var proxyAdd = (function() {
var cache = [];
return function() {
var arg = [].slice.call(arguments).join(',');
// 如果有,则直接从缓存返回
if (cache[arg]) {
return cache[arg];
} else {
var ret = add.apply(this, arguments);
return ret;
}
};
})();
console.log(
add(1, 2, 3, 4),
add(1, 2, 3, 4),
proxyAdd(10, 20, 30, 40),
proxyAdd(10, 20, 30, 40)
); // 10 10 100 100
外观模式
为子系统中的一组接口提供一个一致的页面,定义一个高层接口,这个接口使子系统更加容易使用。
可以通过请求外观接口来达到访问子系统,也可以选择越过外观来直接访问子系统。
外观模式在 JS 中,可以认为是一组函数的集合
// 三个处理函数
function start() {
console.log("start");
}
function doing() {
console.log("doing");
}
function end() {
console.log("end");
}
// 外观函数,将一些处理统一起来,方便调用
function execute() {
start();
doing();
end();
}
// 调用init开始执行
function init() {
// 此处直接调用了高层函数,也可以选择越过它直接调用相关的函数
execute();
}
init(); // start doing end
迭代器模式
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。
JS 中数组的 map、forEach 已经内置了迭代器
[1, 2, 3].forEach(function (item, index, arr) {
console.log(item, index, arr);
});
不过对于对象的遍历,往往不能与数组一样使用同一的遍历代码,我们可以封装一下:
function each(obj, cb) {
var value;
if (Array.isArray(obj)) {
for (var i = 0; i < obj.length; ++i) {
value = cb.call(obj[i], i, obj[i]);
if (value === false) {
break;
}
}
} else {
for (var i in obj) {
value = cb.call(obj[i], i, obj[i]);
if (value === false) {
break;
}
}
}
}
each([1, 2, 3], function (index, value) {
console.log(index, value);
});
each({ a: 1, b: 2 }, function (index, value) {
console.log(index, value);
});
// 0 1
// 1 2
// 2 3
// a 1
// b 2
Node.js 是一个基于 V8 JavaScript 引擎的 JavaScript 运行时环境。
直到 ES6 才有了自己的模块化方案:ESModule
在此之前:Commonjs/AMD/CMD
CommonJS 是一个规范,最初提出来是在浏览器以外的地方使用,当时被命名为 ServerJS,后来为了体现他的广泛性,修改为 CommonJS,简称 CJS
Node 是 CommonJS 在服务端一个具有代表性的实现
Browserify 是 CommonJS 在浏览器中的一种实现
webpack 打包工具具备对 CommonJS 的支持和转换
Node 中每个 js 文件都是一个单独的模块
exports / module.exports 导出
require 导入 动态加载 多次加载会缓存,只运行一次。(加载模块是同步的)
// 导出
exports.aaa = "aaa";
// 导入
const xxx = require("./abc.js");
console.log(xxx.aaa);
每个模块都是 Module 的实例,module.exports = {} 指向一个新的内存空间,修改不影响源文件
早期为了在浏览器中使用模块化,通常采用 AMD(异步加载模块)/CMD
ESModule
export / export default 导出
import 导入
导入时起别名:as
采用编译期的静态分析,并也加入了动态引入的方式。自动采用严格模式。
如需在代码逻辑中导入,可以使用 import()函数,返回 Promise
解析过程和原理:https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/
peerDependencies 对等依赖(比如 elementPlus 依赖 vue3)
npm 的包通常需要遵从 semver 版本规范:https://semver.org/lang/zh-CN/
X.Y.Z 主版本号(major) 次版本号(minor) 修订号(patch)
开头^的意思是:x 保持不变,y、z 安装最新
开头~的意思是:x、y 保持不变,z 保持最新
通常 package.json 里记录大致的版本号,具体安装的版本记录在 package-lock.json
每个项目都下载一堆依赖(yarn 会缓存)
pnpm 节省磁盘空间(通过硬连接的方式,让所有使用同一版本依赖包的项目引用同一个磁盘数据)
硬连接:电脑文件系统中的多个文件平等的共享同一个文件存储单元(操作系统相对内存中数据的链接)
软连接:以绝对路径或者相对路径指向其他文件的引用
它创建的是非扁平的 node_modules
pnpm add 添加包 / pnpm remove 删除包
path.resolve() 拼接多个路径,一定返回一个绝对路径。
webpack
为什么用 loader,因为 webpack 自身就会处理 js 文件,loader 就是让 webpack 能够处理 js 和非 js 文件。
webpack 通过 babel-loader 与 babel 联通。先 babel-loader 联通,在用 babel 编译,再交给 webpack 打包,webpack 主要就是有打包功能,没有编译功能。
上面的方式 babel 只是能转义语法,一些类似 Object.assign 等 API 还是无法处理,所以要引入一些“polyfill 垫片”文档中有记载。
安装 npm i -D core-js@3.6.5 在源码内引入 import 'core-js/stable' 引入的是稳定版
const path = require('path')
//引入插件
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
//配置模式,默认生产模式,改成开发模式后更方便查看代码
mode:'development',
//指定入口文件,单入口的写法
entry:'./src/index.js',
//多入口的写法
/* entry:{
//主入口文件
main:'./src/index.js',
//假如再有一个搜索入口
search:'./src/search.js'
}, */
//单出口
output:{
//路径,必须是绝对路径,resolve是拼接方法,__dirname代表这个配置文件所在目录
path:path.resolve(__dirname,'dist'),
//输出叫什么文件名
filename:'bundle.js'
},
//多出口
/* output{
path:path.resolve(__dirname,'dist'),
//输出的文件名用[name],name会自动取多入口的main和search作为文件名
filename:'[name].js'
}*/
//loader部分
//模块
module:{
//规则,因为可以配置很多loader所以是数组,不同的loader放在不同对象内
rules:[
{
//正则表达式,你想匹配哪些文件
test:/\.js$/,
//排除
exclude:/node_modules/,
//使用什么loader,安装过的
loader:'babel-loader'
},
{
//同一个匹配,配置多个loader
test:/\.css$/,
// 就不用 loader:'css-loader' 改用 use
//注意use里的loader,会从右往左使用,所以先通过css-loader识别css文件再交给style-loader
use:['style-loader','css-loader']
}
]},
//使用插件
plugins:[
//单入口
//并不是所有插件都这么用,具体用法看插件文档
new HtmlWebpackPlugin({
//指定一个html文件作为模板
template:'./index.html'
})
//多入口,有几个入口就实例化几次
// new HtmlWebpackPlugin({
//指定一个html文件作为模板
// template:'./index.html',
// 防止文件名冲突,要加上文件名,默认都是index
// filename:'index.html',
// 指定想要引入哪一个js文件,不指定就都引入
// chunks:['index'],
//其他一些小功能
// minify:{
// //删除html文件中的注释
// removeComments:true,
// //删除html文件中的空格
// collapseWhitespace:true,
// //删除各种html标签属性值的双引号
// removeAttributeQuotes:true
// }
// }),
// new HtmlWebpackPlugin({
// //指定一个html文件作为模板
// template:'./search.html',
// filename:'search.html',
// chunks:['search']
// }) ]}