JS高级详细笔记
this指向
this的的绑定规则
this到底指向什么呢?
我们先来看一个让人困惑的问题:定义一个函数,我们采用三种不同的方式对它进行调用,它产生了三种不同的结果这个的案例可以给我们什么样的启示呢?
1.函数在调用时,JavaScript会默认给this绑定一个值;
2.this的绑定和定义的位置(编写的位置)没有关系;
3.this的绑定和调用方式以及调用的位置有关系;
4.this是在运行时被绑定的;
那么this到底是怎么样的绑定规则呢?
绑定一:默认绑定;当函数作为独立个体直接调用(不带任何修饰)时,this 在非严格模式下指向全局对象(浏览器中为 window),严格模式下为 undefined
绑定二:隐式绑定;当函数作为某个对象的方法被调用时(如 obj.foo()),this 指向发起调用的那个直接对象。注意隐式丢失!
绑定三:显示绑定;通过 call()、apply() 或 bind() 方法直接指定函数执行时的上下文,强制将 this 指向传入的第一个参数。参数作为第二个参数,call传入参数,apply传入列表
绑定四:new绑定;使用 new 关键字调用构造函数时,JS 会创建一个新对象,并将这个新对象绑定到函数调用的 this 上
规则一
//"use strict"
//定义函数
//1.普通的函数被独立的调用
function foo() {
console.log("foo:", this)
}
foo()
//2.函数定义在对象中,但是独立调用 !!隐式丢失
var obj = {
name: "why",
bar: function () {
console.log("bar", this)
}
}
var baz = obj.bar
baz()
//3.高阶函数
function higherOrder(fn) {
fn()
}
higherOrder(obj.bar)
//4.严格模式下,独立调用函数中的this指向是undefined
规则二
function foo() {
console.log(this)
}
var obj = {
name: "why",
foo: foo
}
obj.foo()
规则四
<!-- new绑定
1.创建一个新对象
2.将this指向新对象
3.执行函数体中的代码
4.默认返回新对象
-->
<script>
function foo() {
this.name = "why"
console.log("foo", this)
}
new foo()
规则三
function foo(name, age, height) {
console.log("foo函数被调用:", this)
console.log("打印参数:", name, age, height)
}
// 1.直接调用
foo("why", 18, 1.88)
// 2.apply调用
foo.apply("apply", ["kobe", 30, 1.98])
// 3.call调用
foo.call("call", "james", 25, 2.05)
//4.bind绑定
function foo(name, age, height, address) {
console.log("foo:", this)
console.log("参数:", name, age, height, address)
}
var obj = { name: "why" }
// 需求: 调用foo时, 总是绑定到obj对象身上(不希望obj对象身上有函数)
// 1.bind函数的基本使用
// var bar = foo.bind(obj)
// bar() // this -> obj
// 2.bind函数的其他参数
var bar = foo.bind(obj, "kobe", 18, 1.88)
bar("james")//这个参数添加到address上边
- 内置函数的绑定思考
有些时候,我们会调用一些JavaScript的内置函数,或者一些第三方库中的内置函数。
这些内置函数会要求我们传入另外一个函数;我们自己并不会显示的调用这些函数,而且JavaScript内部或者第三方库内部会帮助我们执行;
这些函数中的this又是如何绑定的呢?
setTimeout、数组的forEach、div的点击
this绑定优先级
■ 学习了四条规则,接下来开发中我们只需要去查找函数的调用应用了哪条规则即可,但是如果一个函数调用位置应用了多条规则,优先级谁更高呢?
1.默认规则的优先级最低
2.显示绑定优先级高于隐式绑定
3.new绑定优先级高于隐式绑定
4.new绑定优先级高于bind
□ new绑定和call、apply是不允许同时使用的,所以不存在谁的优先级更高
□ new绑定可以和bind一起使用,new绑定优先级更高
结果:new ->bind ->apply/call->隐式绑定->显示绑定
this规则之外 - 忽略显示绑定
情况一: 如果在显示绑定中, 我们传入一个null或者undefined, 那么这个显示绑定会被忽略, 使用默认规则
情况二:创建一个函数的 间接引用,这种情况使用默认绑定规则。
赋值(obj2.foo = obj1.foo)的结果是foo函数;
foo函数被直接调用,那么是默认绑定;
箭头函数
箭头函数的编写优化
■ 优化一: 如果只有一个参数()可以省略
nums.forEach(item => {})
■ 优化二: 如果函数执行体中只有一行代码, 那么可以省略大括号
□ 并且这行代码的返回值会作为整个函数的返回值
nums.forEach(item => console.log(item))
nums.filter(item => true)
■ 优化三: 如果函数执行体只有返回一个对象, 那么需要给这个对象加上()
var foo = () => {
return { name: "abc" }
}
var bar = () => ({name: "abc"})
■ 优化四:只要一行代码时,表达式的返回值会作为箭头函数默认返回值,所以可以省略return
箭头函数不使用this的四种标准规则(也就是不绑定this),而是根据外层作用域来决定this。
箭头函数的应用:
// 网络请求的工具函数
function request(url, callbackFn) {
var results = ["abc", "cba", "nba"]
callbackFn(results)
}
// 实际操作的位置(业务)
var obj = {
names: [],
network: function() {
// 1. 早期的时候
// var _this = this
// request("/names", function(res) {
// _this.names = [].concat(res)
// })
// 2. 箭头函数写法
request("/names", (res) => {
this.names = [].concat(res)//这里的this因为箭头函数没有this,所以指向的是上层即obj
})
}
}
浏览器和js的运行原理
网页解析过程
网页被解析的过程

渲染引擎如何解析页面

-
解析一:html解析过程
因为默认情况下服务器会给浏览器返回index.html文件,所以解析HTML是所有步骤的开始:
解析HTML,会构建DOM Tree
-
解析二:生成css规则
在解析的过程中,如果遇到CSS的link元素,那么会由浏览器负责下载对应的CSS文件: 注意:下载CSS文件是不会影响DOM的解析;
浏览器下载完CSS文件后,就会对CSS文件进行解析,解析出对应的规则树:
我们可以称之为 CSSOM(CSS Object Model,CSS对象模型); -
解析三:构建render Tree
当有了DOM Tree和CSSOM Tree后,就可以两个结合来构建Render Tree了
-
布局(layout)和绘制(paint)
第四步是在渲染树(Render Tree)上运行布局(Layout)以计算每个节点的几何体。
渲染树会表示显示哪些节点以及其他样式,但是不表示每个节点的尺寸、位置等信息;
布局是确定呈现树中所有节点的宽度、高度和位置信息;
第五步是将每个节点绘制(Paint)到屏幕上。
在绘制阶段,浏览器将布局阶段计算的每个frame转为屏幕上实际的像素点;
包括将元素的可见部分进行绘制,比如文本、颜色、边框、阴影、替换元素(比如img) -
特殊解析:composite合成
绘制的过程,可以将布局后的元素绘制到多个合成图层中。
这是浏览器的一种优化手段;
默认情况下,标准流中的内容都是被绘制在同一个图层(Layer)中的;
而一些特殊的属性,会创建一个新的合成层(CompositingLayer),并且新的图层可以利用GPU来加速绘制;因为每个合成层都是单独渲染的;
那么哪些属性可以形成新的合成层呢?常见的一些属性: -
3D transforms
-
video、canvas、iframe
-
opacity 动画转换时;
-
position: fixed
-
will-change:一个实验性的属性,提前告诉浏览器元素可能发生哪些变化;
-
animation 或 transition 设置了opacity、transform;
分层确实可以提高性能,但是它以内存管理为代价,因此不应作为 web 性能优化策略的一部分过度使用。
回流(reflow,重排)与重绘(repaint)
什么是回流?
第一次确定节点的大小和位置,称之为布局(layout)
之后对节点的大小、位置修改重新计算称之为回流
什么情况下会引起回流?
- DOM结构发生改变(添加新的节点或者移除节点)
- 改变了布局(修改了width、height、padding、font-size等值)
- 窗口resize(修改了窗口的尺寸等)
- 调用getComputedStyle方法获取尺寸、位置信息
什么是重绘?
第一次渲染内容称之为绘制(paint)
之后重新渲染称之为重绘
什么情况下会引起重绘?
修改背景色、文字颜色、边框颜色、样式等
回流一定会引起重绘,所以回流是一件很消耗性能的事情
开发中避免回流的操作:
- 修改样式时尽量一次性修改,比如通过cssText修改,比如通过添加class修改
- 尽量避免频繁的操作DOM,可以在一个DocumentFragment或者父元素中将要操作的DOM操作完成,再一次性的操作
- 尽量避免通过getComputedStyle获取尺寸、位置信息
- 对某些元素使用position的absolute或者fixed,并不是不会引起回流,而是开销相对较小,不会对其他元素造成影响
script元素和页面解析的关系
■ 我们现在已经知道了页面的渲染过程,但是JavaScript在哪里呢?
事实上,浏览器在解析HTML的过程中,遇到了script元素是不能继续构建DOM树的;
它会停止继续构建,首先下载JavaScript代码,并且执行JavaScript的脚本;
只有等到JavaScript脚本执行结束后,才会继续解析HTML,构建DOM树;
■ 为什么要这样做呢?
这是因为JavaScript的作用之一就是操作DOM,并且可以修改DOM;
如果我们等到DOM树构建完成并且渲染再执行JavaScript,会造成严重的回流和重绘,影响页面的性能;
所以会在遇到script元素时,优先下载和执行JavaScript代码,再继续构建DOM树;
■ 但是这个也往往会带来新的问题,特别是现代页面开发中:
在目前的开发模式中(比如Vue、React),脚本往往比HTML页面更“重”,处理时间需要更长;
所以会造成页面的解析阻塞,在脚本下载、执行完成之前,用户在界面上什么都看不到;
为了解决这个问题,script元素给我们提供了两个属性(attribute):defer和async。
defer属性
defer 属性告诉浏览器不要等待脚本下载,而继续解析 HTML,构建 DOM Tree。
□ 脚本会由浏览器来进行下载,但是不会阻塞 DOM Tree 的构建过程;
□ 如果脚本提前下载好了,它会等待 DOM Tree 构建完成,在 DOMContentLoaded 事件之前先执行 defer 中的代码;
所以 DOMContentLoaded 总是会等待 defer 中的代码先执行完成。
<script defer src="./js/defer-demo.js"></script>
<script>
window.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded")
})
</script>
另外多个带 defer 的脚本是可以保持正确的顺序执行的。
从某种角度来说,defer 可以提高页面的性能,并且推荐放到 head 元素中;
注意:defer 仅适用于外部脚本,对于 script 默认内容会被忽略。
async属性
- async 特性与 defer 有些类似,它也能够让脚本不阻塞页面。
- async是让一个脚本完全独立的:浏览器不会因 async 脚本而阻塞(与 defer 类似);
- async脚本不能保证顺序,它是独立下载、独立运行,不会等待其他脚本;
- async不会保证在DOMContentLoaded之前或者之后执行;
- defer通常用于需要在文档解析后操作DOM的JavaScript代码,并且对多个script文件有顺序要求的;
- async通常用于独立的脚本,对其他脚本,甚至DOM没有依赖的;
js运行原理
V8引擎
V8引擎的执行原理
我们来看一下官方对V8引擎的定义:
V8是用C++编写的Google开源高性能JavaScript和WebAssembly引擎,它用于Chrome和Node.js等。
它实现ECMAScript和WebAssembly,并在Windows 7或更高版本,macOS 10.12+和使用x64,IA-32,ARM或MIPS处理器的Linux系统上运行。
V8可以独立运行,也可以嵌入到任何C++应用程序中。
v8引擎的架构
V8引擎本身的源码非常复杂,大概有超过100w行C++代码,通过了解它的架构,我们可以知道它是如何对JavaScript执行的:
Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码;
如果函数没有被调用,那么是不会被转换成AST的;
Parse的V8官方文档:https://v8.dev/blog/scanner
Ignition是一个解释器,会将AST转换成ByteCode(字节码)
同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
如果函数只调用一次,Ignition会解释执行ByteCode;
Ignition的V8官方文档:https://v8.dev/blog/ignition-interpreter
TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码;
如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能;
但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成为字节码;
执行上下文
JS引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈。
那么现在它要执行谁呢?执行的是全局的代码块:
□ 全局的代码块为了执行会构建一个 Global Execution Context(GEC);
□ GEC 会被放入到 ECS 中执行;
GEC 被放入到 ECS 中里面包含两部分内容:
□ 第一部分:在代码执行前,在 parser 转成 AST 的过程中,会将全局定义的变量、函数等加入赋值;这个过程也称之为变量的作用域提升(hoisting)
□ 第二部分:在代码执行中,对变量赋值,或者执行其他的函数;
认识vo对象
每一个执行上下文会关联一个VO(Variable Object,变量对象),变量和函数声明会被添加到这个VO对象中
当全局代码被执行的时候,VO就是GO对象了
函数是如何被执行的呢?
在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文并且压入到EC Stack中。(执行上下文栈)
因为每个执行上下文都会关联一个VO,那么函数执行上下文关联的VO是什么呢?
□ 当进入一个函数执行上下文时,会创建一个AO对象(Activation Object);
□ 这个AO对象会使用arguments作为初始化,并且初始值是传入的参数;
□ 这个AO对象会作为执行上下文的VO来存放变量的初始化;
函数每次执行都会创建一个新的AO对象,并把原来的执行上下文弹出执行上下文栈
认识作用域和作用域链
当进入到一个执行上下文时,执行上下文也会关联一个作用域链(Scope Chain)作用域链是一个对象列表,用于变量标识符的求值;
当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象;
函数在创建的时候,作用域链已经确定了,跟所调用的位置无关
JS内存管理与闭包
认识内存管理
不管什么样的编程语言,在代码的执行过程中都是需要给它分配内存的,不同的是某些编程语言需要我们自己手动的管理内存,某些编程语言可以自动帮助我们管理内存,不管以什么样的方式来管理内存,内存的管理都会有如下的生命周期:
第一步:分配申请你需要的内存(申请);
第二步:使用分配的内存(存放一些东西,比如对象等);
第三步:不需要使用时,对其进行释放;
不同的编程语言对于第一步和第三步会有不同的实现:
手动管理内存:比如C、C++,包括早期的OC,都是需要手动来管理内存的申请和释放的(malloc和free函数);
自动管理内存:比如Java、JavaScript、Python、Swift、Dart等,它们有自动帮助我们管理内存;
对于开发者来说,JavaScript 的内存管理是自动的、无形的。我们创建的原始值、对象、函数……这一切都会占用内存;但是我们并不需要手动来对它们进行管理,JavaScript引擎会帮助我们处理好它;
js的内存管理与垃圾回收
JavaScript会在定义数据时为为我们分配内存。但是内存分配方式是一样的吗?
□ JS对于原始数据类型内存的分配会在执行时,直接在栈空间进行分配;
□ JS对于复杂数据类型内存的分配会在堆内存中开辟一块空间,并且将这块空间的指针返回值变量引用;
因为内存的大小是有限的,所以当内存不再需要的时候,我们需要对其进行释放,以便腾出更多的内存空间。
在手动管理内存的语言中,我们需要通过一些方式自己来释放不再需要的内存,比如free函数:但是这种管理的方式其实非常低效,影响我们编写逻辑的代码的效率;并且这种方式对开发者的要求也很高,并且一不小心就会产生内存泄露;
所以大部分现代的编程语言都是有自己的垃圾回收机制:
垃圾回收的英文是 Garbage Collection,简称 GC;
对于那些不再使用的对象,我们都称之为是垃圾,它需要被回收,以释放更多的内存空间;而我们的语言运行环境,比如Java的运行环境JVM,JavaScript的运行环境js引擎都会内存 垃圾回收器;
垃圾回收器我们也会简称为 GC,所以在很多地方你看到 GC 其实指的是垃圾回收器;
但是这里又出现了另外一个很关键的问题:GC怎么知道哪些对象是不再使用的呢?
这里就要用到 GC 的实现以及对应的算法
常见的GC算法 – 标记清除(mark-Sweep)
标记清除的核心思路是可达性(Reachability)
该算法设置一个根对象(root object),垃圾回收器会定期从这个根开始,查找所有从根开始有引用到的对象;
对于没有引用到的对象,就认为是不可用的对象(即垃圾)。
这个算法可以很好地解决循环引用的问题。
常见的GC算法 - 其他算法优化补充
JS引擎比较广泛的采用的就是可达性中的标记清除算法,当然类似于V8引擎为了进行更好的优化,它在算法的实现细节上也会结合一些其他的算法。
标记整理(Mark-Compact) 和 “标记 - 清除” 相似;
□ 不同的是,回收期间同时会将保留的存储对象搬运汇集到连续的内存空间,从而整合空闲空间,避免内存碎片化;
分代收集(Generational collection) —— 对象被分成两组:“新的” 和 “旧的”。
□ 许多对象出现,完成它们的工作并很快死去,它们可以很快被清理;
□ 那些长期存活的对象会变得 “老旧”,而且被检查的频次也会减少;
增量收集(Incremental collection)
□ 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟。
□ 所以引擎试图将垃圾收集工作分成几部分来做,然后将这几部分会逐一进行处理,这样会有许多微小的延迟而不是一个大的延迟;
闲时收集(Idle-time collection)
□ 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。
认识闭包
闭包的定义可从计算机科学和JavaScript两个维度理解,具体如下:
一、计算机科学中的闭包(维基百科定义)
闭包(Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures);是在支持头等函数的编程语言中,实现词法绑定的一种技术;闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表);
闭包与函数的核心区别:捕捉闭包时,它的自由变量会在捕捉时被确定,即使脱离了捕捉时的上下文,它也能照常运行。
二、闭包的起源与JavaScript的关联
闭包的概念出现于20世纪60年代,最早实现闭包的程序是 Scheme;JavaScript 大量设计来源于 Scheme,因此 JavaScript 中也存在闭包机制。
三、MDN对JavaScript闭包的解释
一个函数和其周围状态(lexical environment,词法环境)的引用被捆绑在一起(或说函数被引用包围),这样的组合就是闭包(closure);
闭包允许你在内层函数中访问到其外层函数的作用域;
在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
四、对闭包的理解与总结
广义角度:JavaScript 中的所有函数都是闭包(因为函数在创建时都会关联其词法环境);
狭义角度:JavaScript 中,若一个函数访问了外层作用域的变量,那么它就是一个闭包。
深入闭包的访问过程



闭包的内存泄漏
那么我们为什么经常会说闭包是有内存泄露的呢?
在上面的案例中,如果后续我们不再使用add8函数了,那么该函数对象应该要被销毁掉,并且其引用着的父作用域AO也应该被销毁掉;
但是目前因为在全局作用下add8变量对0xb00的函数对象有引用,而0xb00的作用域中AO(0x200)有引用,所以最终会造成这些内存都是无法被释放的;
所以我们经常说的闭包会造成内存泄露,其实就是刚才的引用链中的所有对象都是无法释放的;
那么,怎么解决这个问题呢?
当将add10设置为null时,就不再对函数对象0xb00有引用,那么对应的AO对象0x200也就不可达了;
在GC的下一次检测中,它们就会被销毁掉;
AO不使用的属性优化
我们来研究一个问题:AO对象不会被销毁时,是否里面的所有属性都不会被释放?
下面这段代码中name属于闭包的父作用域里面的变量;
我们知道形成闭包之后count一定不会被销毁掉,那么name是否会被销毁掉呢?
这里我打上了断点,我们可以在浏览器上看看结果;
函数和对象的增强
函数增强
函数对象的属性和arguments
我们知道JavaScript中函数也是一个对象,那么对象中就可以有属性和方法。
属性name:一个函数的名词我们可以通过name来访问;
属性length:属性length用于返回函数参数的个数;
注意:剩余参数(…arguements)和默认赋值的参数是不参与length计数的;
var baz = (name, age, ...args) => {
}
console.log(baz.length)//2
arguments 是一个对应于传递给函数的参数 的 类数组(array-like)对象。
array-like意味着它不是一个数组类型,而是一个对象类型:
但是它却拥有数组的一些特性,比如说length,比如可以通过index索引来访问;但是它却没有数组的一些方法,比如filter、map等;
在开发中,我们经常需要将arguments转成Array,以便使用数组的一些特性。常见的转化方式如下
转化方式一:
遍历arguments,添加到一个新数组中;
转化方式二:较难理解(有点绕),了解即可
调用数组slice函数的call方法;
转化方式三:ES6中的两个方法
Array.from
[…arguments]
// 1.转化方式一:
var length = arguments.length
var arr = []
for (var i = 0; i < length; i++) {
arr.push(arguments[i])
}
console.log(arr)
// 2.转化方式二
var arr1 = Array.prototype.slice.call(arguments)
var arr2 = [].slice.call(arguments)
console.log(arr1)
console.log(arr2)
// 3.转化方式三:ES6之后
const arr3 = Array.from(arguments)
const arr4 = [...arguments]
console.log(arr3)
console.log(arr4)
注:箭头函数是不绑定arguments的,所以我们在箭头函数中使用arguments会去上层作用域查找。
函数的剩余参数(rest)
ES6中引入了rest parameter,可以将不定数量的参数放入到一个数组中:
□ 如果最后一个参数是 … 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组;
□ 那么剩余参数和arguments有什么区别呢?
剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参;
arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作;
arguments是早期的ECMAScript中为了方便去获取所有的参数提供的一个数据结构,而rest参数是ES6中提供并且希望以此来替代arguments的;
注:剩余参数必须放到最后一个位置,否则会报错。
理解JavaScript纯函数
函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念;
□ 在react开发中纯函数是被多次提及的;
□ 比如react中组件就被要求像是一个纯函数(为什么是像,因为还有class组件),redux中有一个reducer的概念,也是要求必须是一个纯函数;
□ 所以掌握纯函数对于理解很多框架的设计是非常有帮助的;
纯函数的维基百科定义:
在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:
□ 此函数在相同的输入值时,需产生相同的输出。
□ 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
□ 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。
总结一下: 确定的输入,一定会产生确定的输出;函数在执行过程中,不能产生副作用;
副作用概念的理解
□ 副作用(side effect)其实本身是医学的一个概念,比如我们经常说吃什么药本来是为了治病,可能会产生一些其他的副作用;
□ 在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储;
纯函数在执行的过程中就是不能产生这样的副作用:
副作用往往是产生bug的“温床”。
纯函数的作用和优势
为什么纯函数在函数式编程中非常重要呢?
因为你可以安心的编写和安心的使用;
你在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改;
你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;
React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改:
React 非常灵活,但它也有一个严格的规则:
所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
柯里化
柯里化也是属于函数式编程里面一个非常重要的概念。
- 是一种关于函数的高阶技术;
- 它不仅被用于 JavaScript,还被用于其他编程语言;
我们先来看一下维基百科的解释:在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化;是把接收多个参数的函数,变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数,而且返回结果的新函数的技术;柯里化声称 “如果你固定某些参数,你将得到接受余下参数的一个函数”;
总结:
只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数;这个过程就称之为柯里化;
柯里化是一种函数的转换,将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b) ( c)。
柯里化不会调用函数。它只是对函数进行转换。
柯里化函数实现
function hyCurrying(fn) {
function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args)
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2))
}
}
}
return curried
}
组合式函数
组合式函数实现
// 封装的函数:你传入多个函数,我自动的将多个函数组合在一起逐个调用
function composeFn(...fns) {
// 1. 边界判断(edge case)
var length = fns.length
if (length <= 0) return
for (var i = 0; i < length; i++) {
var fn = fns[i]
if (typeof fn !== "function") {
throw new Error(`index position ${i} must be function`)
}
}
// 2. 返回的新函数
return function(...args) {
var result = fns[0].apply(this, args)
for (var i = 1; i < length; i++) {
var fn = fns[i]
result = fn.apply(this, [result])
}
return result
}
}
var newFn = composeFn(double, pow, console.log)
newFn(100)
newFn(55)
newFn(22)
严格模式
JavaScript历史的局限性:
- 长久以来,JavaScript 不断向前发展且并未带来任何兼容性问题;
- 新的特性被加入,旧的功能也没有改变,这么做有利于兼容旧代码;
- 但缺点是 JavaScript 创造者的任何错误或不完善的决定也将永远被保留在 JavaScript 语言中;
在ECMAScript5标准中,JavaScript提出了严格模式的概念(Strict Mode):
- 严格模式很好理解,是一种具有限制性的JavaScript模式,从而使代码隐式的脱离了” 懒散(sloppy)模式 “;
- 支持严格模式的浏览器在检测到代码中有严格模式时,会以更加严格的方式对代码进行检测和执行;
严格模式对正常的JavaScript语义进行了一些限制:
- 严格模式通过 抛出错误 来消除一些原有的 静默(silent)错误;
- 严格模式让JS引擎在执行代码时可以进行更多的优化(不需要对一些特殊的语法进行处理);
- 严格模式禁用了在ECMAScript未来版本中可能会定义的一些语法;
开启严格模式
严格模式支持粒度化的迁移:
可以支持在js文件中开启严格模式;也支持对某一个函数开启严格模式;
严格模式通过在文件或者函数开头使用 use strict 来开启。
没有类似于 “no use strict” 这样的指令可以使程序返回默认模式。
现代 JavaScript 支持 “class” 和 “module”,它们会自动启用 use strict;
开启严格模式的限制
-
无法意外的创建全局变量
-
严格模式会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常
-
严格模式下试图删除不可删除的属性
-
严格模式不允许函数参数有相同的名称
-
不允许0的八进制语法
-
在严格模式下,不允许使用with
-
在严格模式下,eval不再为上层引用变量
-
严格模式下,this绑定不会默认转成对象
对象增强
对属性操作的控制:属性描述符
如果我们想要对一个属性进行比较精准的操作控制,那么我们就可以使用属性描述符。
通过属性描述符可以精准的添加或修改对象的属性;
属性描述符需要使用Object…defineProperty来对属性进行添加或者修改;
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
Object.defineProperty(obj, prop, descriptor)
参数说明:
obj:要定义属性的对象;
prop:要定义或修改的属性的名称或 Symbol;
descriptor:要定义或修改的属性描述符;
分为两类:数据属性描述符和存取属性描述符
数据属性描述符
数据属性描述符有四个特性
[[Configurable]]:表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;
当我们直接在一个对象上定义某个属性时,这个属性的[[Configurable]]为true;
当我们通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为false;
[[Enumerable]]:表示属性是否可以通过for-in或者Object.keys()返回该属性;
当我们直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为true;
当我们通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为false;
[[Writable]]:表示是否可以修改属性的值;
当我们直接在一个对象上定义某个属性时,这个属性的[[Writable]]为true;
当我们通过属性描述符定义一个属性时,这个属性的[[Writable]]默认为false;
[[Value]]:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改;
默认情况下这个值是undefined;
var obj = {
name: "why", // configurable: true
age: 18
}
Object.defineProperty(obj, "name", {
configurable: false, // 告诉js引擎, obj对象的name属性不可以被删除
enumerable: false, // 告诉js引擎, obj对象的name属性不可枚举(for-in/Object.keys)
writable: false, // 告诉js引擎, obj对象的name属性不写入(只读属性: readOnly)
value: "coderwhy" // 告诉js引擎, 返回这个value
})
delete obj.name
console.log(obj)
// 通过Object.defineProperty添加一个新的属性
Object.defineProperty(obj, "address", {})
delete obj.address
console.log(obj)
console.log(Object.keys(obj))
obj.name = "kobe"
console.log(obj.name)
存取属性描述符
[[Configurable]]:表示属性是否可以通过 delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;
和数据属性描述符是一致的;
当我们直接在一个对象上定义某个属性时,这个属性的 [[Configurable]]为 true;
当我们通过属性描述符定义一个属性时,这个属性的 [[Configurable]]默认为 false;
[[Enumerable]]:表示属性是否可以通过 for-in或者 Object.keys()返回该属性;
和数据属性描述符是一致的;
当我们直接在一个对象上定义某个属性时,这个属性的 [[Enumerable]]为 true;
当我们通过属性描述符定义一个属性时,这个属性的 [[Enumerable]]默认为 false;
[[get]]:获取属性时会执行的函数。默认为 undefined
[[set]]:设置属性时会执行的函数。默认为 undefined
// vue2响应式原理
var obj = {
name: "why"
}
// 对obj对象中的name添加描述符(存取属性描述符)
var _name = ""
Object.defineProperty(obj, "name", {
configurable: true,
enumerable: false,
set: function(value) {
console.log("set方法被调用了", value)
_name = value
},
get: function() {
console.log("get方法被调用了")
return _name
}
})
obj.name = "kobe"
obj.name = "jame"
obj.name = "curry"
obj.name = "coderwhy"
// 获取值
console.log(obj.name)
ES5中实现继承
对原型的理解
对象的原型 __ proto__
JavaScript当中每个对象都有一个特殊的内置属性 [[prototype]],这个特殊的对象可以指向另外一个对象。
那么这个对象有什么用呢?
当我们通过引用对象的属性key来获取一个value时,它会触发 [[Get]]的操作;
这个操作会首先检查该对象是否有对应的属性,如果有的话就使用它;
如果对象中没有该属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性;
那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?
答案是有的,只要是对象都会有这样的一个内置属性;
获取的方式有两种:
方式一:通过对象的 proto属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题);
方式二:通过 Object.getPrototypeOf 方法可以获取到;
var obj={
name: "why",
age: 18
}
console.log(obj)
var info = {}
// 获取对象的原型
console.log(obj.name, obj.age)
console.log(obj.__proto__)
console.log(Object.getPrototypeOf(obj))
console.log(obj.__proto__ === Object.getPrototypeOf(obj)) // true
// 疑问:这个原型有什么用呢?
// 当我们通过[[get]]方式获取一个属性对应的value时
// 1. 它会优先在自己的对象中查找,如果找到直接返回
// 2. 如果没有找到,那么会在原型对象中查找
console.log(obj.name)
obj.__proto__.message = "Hello World"
console.log(obj.message)
函数的原型 prototype
所有的函数都有一个 prototype的属性(注意:不是 proto)
function foo() {
}
// 所有的函数都有一个属性,名字是 prototype
console.log(foo.prototype)
var obj = {}
console.log(obj.prototype) // obj就没有这个属性
new 操作的原型赋值
function Foo() {
// 1.创建空的对象
// 2.将Foo的prototype原型(显式原型)赋值给空的对象的__proto__(隐式原型)
}
console.log(Foo.prototype)
var f1 = new Foo()
var f2 = new Foo()
var f3 = new Foo()
var f4 = new Foo()
var f5 = new Foo()
console.log(f1.__proto__)
console.log(f1.__proto__ === Foo.prototype) // true
console.log(f3.__proto__ === f5.__proto__) // true
利用new操作符创建出来的对象的__ proto__ 是foo函数的prototype显示原型
constructor属性
事实上原型对象上面是有一个属性的:constructor
默认情况下原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象;
内存图
例子:
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.running = function() {
console.log("running~")
}
var p1 = new Person("why", 18)
var p2 = new Person("kobe", 30)
console.log(p1.name)
console.log(p2.name)
p1.running()
p2.running()
Person.prototype.address = "中国"
p1.__proto__.info = "中国很美丽!"
p1.height = 1.88
p2.isAdmin = true
console.log(p1.address)
console.log(p2.isAdmin)
console.log(p1.isAdmin)
console.log(p2.info)
p1.address = "广州市"
console.log(p2.address)

重写原型对象
前面我们说过,每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获取constructor)属性;
而我们这里相当于给orototype重新赋值了一个对象,那么这个新对象的constructor)属性,会指向Objectt构造函数,而不是Person构造函数了
function Person() {
}
Person.prototype = {
name: "why",
age: 18,
eating: function() {
console.log(this.name + "在吃东西~")
}
}
如果希望constructor指向Person,那么可以手动添加:
上面的方式虽然可以,但是也会造成constructor的[[Enumerable]]特性被设置了true.
默认情况下,原生的constructor属性是不可枚举的.
如果希望解决这个问题,就可以使用我们前面介绍的Object.defineProperty()函数了.
实现继承
面向对象的特性-继承
面向对象有三大特性:封装、继承、多态
□ 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程;
□ 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中);
□ 多态:不同的对象在执行时表现出不同的形态;
那么继承是做什么呢?
□继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可;
□ 在很多编程语言中,继承也是多态的前提;
那么JavaScript当中如何实现继承呢?
我们先来看一下JavaScript原型链的机制;再利用原型链的机制实现一下继承;
js原型链

组合借用实现继承
通过原型链实现方法的继承
如果我们现在需要实现继承,那么就可以利用原型链来实现了:
□ 目前stu的原型是p对象,而p对象的原型是Person默认的原型,里面包含running函数
□ 注意:步骤4和步骤5不可以调整顺序,否则会有问题

弊端:某些属性其实是保存在p对象上的,我们通过直接打印对象是看不到这个属性的;
这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题;
不能给Person传递参数(让每个stu有自己的属性),因为这个对象是一次性创建的(没办法定制化);
通过借用构造函数实现属性的继承
为了解决原型链继承中存在的问题,开发人员提供了一种新的技术: constructor stealing(有很多名称: 借用构造函数或者称之为经典继承或者称之为伪对象)
借用继承的做法非常简单:在子类型构造函数的内部调用父类型构造函数。
因为函数可以在任意的时刻被调用;
因此通过 apply()和call()方法也可以在新创建的对象上执行构造函数;
function Student(name, friends, sno) {
Person.call(this, name, friends)
this.sno = sno
}
Student.prototype = Person.prototype
//组合继承
// 1. 定义父类 Person
function Person(name, age, height, address) {
this.name = name;
this.age = age
this.height = height
this.address = address
}
Person.prototype.running = function () {
console.log("running~")
}
Person.prototype.eating = function () {
console.log("eating~")
}
// 2. 定义子类 Student
function Student(name, age, height, address, sno, score) {
Person.call(this, name, age, height, address)
// 子类自身属性
this.sno = sno
this.score = score
}
// 创建一个父类的实例对象(new Person()),用这个实例对象来作为子类的原型对象
var p = new Person("why", 18)
Student.prototype = p
Student.prototype.studying = function () {
console.log("studying~")
}
// 3. 测试继承效果
var stu1 = new Student("kobe", 30, 111, 100, "S001", 90)
var stu2 = new Student("james", 25, 111, 100, "S002", 85)
// 调用方法
stu1.running() // 继承自 Person.prototype
stu1.eating() // 继承自 Person.prototype
stu1.studying() // 子类自身原型方法
// 访问属性
console.log(stu1.name, stu1.age) // 继承自 Person(通过借用构造函数)
console.log(stu1.sno, stu1.score) // 子类自身属性
console.log(stu2.name, stu2.age) // 每个实例属性独立
// 查看原型链关系
console.log(stu1.__proto__ === p) // true(子类原型指向 p)
console.log(p.__proto__ === Person.prototype) // true(p 的原型是 Person 原型)

组合继承存在什么问题呢?
组合继承最大的问题就是无论在什么情况下,都会调用两次父类构造函数。
一次在创建子类原型的时候;
另一次在子类构造函数内部(也就是每次创建子类实例的时候);
另外,如果你仔细按照我的流程走了上面的每一个步骤,你会发现:所有的子类实例事实上会拥有两份父类的属性
一份在当前的实例自己里面(也就是person本身的),另一份在子类对应的原型对象中(也就是person.proto里面);
当然,这两份属性我们无需担心访问出现问题,因为默认一定是访问实例本身这一部分的;
最终实现继承的方法
//实现继承的工具函数
// 原型继承 没有兼容性问题
function createObject(o) {
function F() { }
F.prototype = o
return new F()
}
//将Subtype和Supertype的原型连接起来
//寄生式函数
function inherit(Subtype, Supertype) {
Subtype.prototype = createObject(Supertype.prototype)
Object.defineProperty(Subtype.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: Subtype
})
}
//实现继承
// 原型链/借用/原型式(对象之间)/寄生式函数
function Person(name, age, height) {
this.name = name
this.age = age
this.height = height
}
Person.prototype.running = function () {
console.log("running~")
}
Person.prototype.eating = function () {
console.log("eating~")
}
function Student(name, age, height, sno, score) {
Person.call(this, name, age, height)
this.sno = sno
this.score = score
}
inherit(Student, Person)
Student.prototype.studying = function () {
console.log("studying")
}
// 创建实例对象
var stu1 = new Student("why", 18, 1.88, 111, 100)
var stu2 = new Student("kobe", 30, 1.98, 112, 80)
stu1.eating()
stu1.running()
stu1.studying()

对象方法的补充
hasOwnProperty:对象是否有某一个属于自己的属性(不是在原型的属性)返回布尔值
in/for in 操作符: 判断某个属性是否在某个对象或者对象的原型上 返回布尔值
instanceof: 用于检测构造函数(Person、Student类)的pototype,是否出现在某个实例对象的原型链上 a instanceof b:监测a是否是b的实例
isPrototypeOf:用于检测某个对象,是否出现在某个实例对象的原型链上
a.prototype.isPrototypeOf(b) :监测a的原型对象是否是b的原型
ES6实现继承
原型继承的图解

对象的 __ proto__ 指向它的构造函数的 prototype
函数是function的实例,函数又是特殊的对象
所以function Person(){}的__proto__指向function的原型对象
较为官方的图
类方法和实例方法
function Person(name, age) {
this.name = name
this.age = age
}
Person.totalCounter = "70亿"
// 添加Person原型上的方法也成为实例方法
//没有对象实例的情况下,不可以调用到
Person.prototype.running = function() {
console.log(this.name + "running~")
}
Person.prototype.eating = function() {
console.log("eating~")
}
// 添加Person对象本身的方法成为类方法
var names = ["abc", "cba", "nba", "mba"]
Person.randomPerson = function() {
var randomName = names[Math.floor(Math.random() * names.length)]
return new Person(randomName, Math.floor(Math.random() * 100))
}
// 实例对象
var p1 = new Person("why", 18)
var p2 = new Person("kobe", 30)
p1.running()
var p=Person.randomPerson()
console.log(p)
class类的使用
// 编程:高内聚低耦合
class Person {
// 1.类中的构造函数
// 当我们通过new关键字调用一个Person类时,默认调用class中的constructor方法
constructor(name, age) {
this.name = name
this.age = age
}
// 2.实例方法
// 本质上是放在Person.prototype
running() {
console.log(this.name + " running~")
}
eating() {
console.log(this.name + " eating~")
}
}
// 创建实例对象
var p1 = new Person("why", 18)
// 使用实例对象中属性和方法
console.log(p1.name, p1.age)
p1.running()
p1.eating()
// 研究内容
console.log(Person.prototype === p1.__proto__)
console.log(Person.running)
console.log(Person.prototype.running)
当我们通过 new 关键字操作类的时候,会调用这个 constructor 函数,并且执行如下操作:
- 在内存中创建一个新的对象(空对象);
- 这个对象内部的 [[prototype]] 属性会被赋值为该类的 prototype 属性;
- 构造函数内部的 this,会指向创建出来的新对象;
- 执行构造函数的内部代码(函数体代码);
- 如果构造函数没有返回非空对象,则返回创建出来的新对象。
不同点:class定义的类不能作为一个普通的函数进行调用
对象的访问器方法:
Object.defineProperty(obj, "name", {
configurable: true,
enumerable: true,
set: function() {
},
get: function() {
}
})
类的访问器方法:
class Person {
// 程序员之间的约定:以_开头的属性和方法,
constructor(name, age) {
this._name = name
}
set name(value) {
console.log("设置name")
this._name = value
}
get name() {
console.log("获取name")
return this._name
}
}
var p1 = new Person("why", 18)
p1.name = "kobe"
console.log(p1.name)
// console.log(p1._name)
var p2 = new Person("james", 25)
console.log(p2.name)
类的静态方法(类方法)
使用static关键字
var names = ["abc", "cba", "nba", "mba"]
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
// 实例方法
running() {
console.log(this.name + " running~")
}
eating() {}
}
// 类方法(静态方法)
static randomPerson() {
console.log(this)
var randomName = names[Math.floor(Math.random() * names.length)]
return new this(randomName, Math.floor(Math.random() * 100))
}
var p1 = new Person()
p1.running()
p1.eating()
var randomPerson = Person.randomPerson()
console.log(randomPerson)
通过extends实现继承
class Student extends Person {
constructor(name, age, sno, score) {
// this.name = name
// this.age = age
super(name, age)//super关键字调用父类构造函数
this.sno = sno
this.score = score
}
// running() {
// console.log("running~")
// }
// eating() {
// console.log("eating~")
// }
studying() {
console.log("studying~")
}
}
var stu1 = new Student("why", 18, 111, 100)
stu1.running()
stu1.eating()
stu1.studying()
super关键字
-
Class为我们的方法中还提供了super关键字:
- 执行
super.method(...)调用一个父类方法。 - 执行
super(...)来调用一个父类 constructor(只能在我们的 constructor 中)
- 执行
-
注意:在子(派生)类的造函数中使用
this或者返回默认对象之前,必须先通过super调用父类的构造函数! -
super的使用位置有三个:子类的构造方法、实例方法、静态方法;
多态
在严格意义的面向对象语言中,多态的是存在如下条件的:
- 必须有继承(实现接口)
- 必须有父类引用指向子类对象
// shape形状
class Shape {
getArea() {}
}
class Rectangle extends Shape {
constructor(width, height) {
super()
this.width = width
this.height = height
}
getArea() {
return this.width * this.height
}
}
class Circle extends Shape {
constructor(radius) {
super()
this.radius = radius
}
getArea() {
return this.radius * this.radius * 3.14
}
}
var rect1 = new Rectangle(100, 200)
var rect2 = new Rectangle(20, 30)
var c1 = new Circle(10)
var c2 = new Circle(15)
function getShapeArea(shape) {
console.log(shape.getArea())
}
getShapeArea(rect1)
getShapeArea(c1)
JavaScript有多态吗?
维基百科对多态的定义:多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口,或使用一个单一的符号来表示多个不同的类型。
非常的抽象,个人的总结:不同的数据类型进行同一个操作,表现出不同的行为,就是多态的体现。
那么从上面的定义来看,JavaScript是一定存在多态的。
对象字面量的增强与解构
- 属性的增强
- 方法的增强
- 计算属性名的写法
var name = "why"
var age = 18
var key = "address"
var obj = {
// 1.属性的增强
name,
age,
// 2.方法的增强
running: function() {
console.log(this)
},
swimming() {
console.log(this)
},
eating: () => {
console.log(this)
},
// 3.计算属性名
[key]: "广州"
}
解构
//数组的解构
var names = ["abc", "cba", undefined, "nba", "mba"]
// 1.数组的解构
// var name1 = names[0]
// var name2 = names[1]
// var name3 = names[2]
// 1.1.基本使用
// var [name1, name2, name3] = names
// console.log(name1, name2, name3)
// 1.2.顺序问题:严格的顺序
// var [name1, , name3] = names
// console.log(name1, name3)
// 1.3.解构出数组
// var [name1, name2, ...newNames] = names
// console.log(name1, name2, newNames)
// 1.4.解构的默认值
var [name1, name2, name3 = "default"] = names
console.log(name1, name2, name3)、
//对象的解构
// 2.对象的解构
var obj = { name: "why", age: 18, height: 1.88 }
// var name = obj.name
// var age = obj.age
// var height = obj.height
// 2.1.基本使用
// var { name, age, height } = obj
// console.log(name, age, height)
// 2.2.顺序问题:对象的解构是没有顺序,根据key解构
// var { height, name, age } = obj
// console.log(name, age, height)
// 2.3.对变量进行重命名
// var { height: wHeight, name: wName, age: wAge } = obj
// console.log(wName, wAge, wHeight)
// 2.4.默认值
var { height: wHeight, name: wName, age: wAge, address } = obj
手写实现apply
核心思想:把函数(调用apply的函数,也就是里边写的this)变成thisArg对象的一个临时的方法,然后通过这个对象来调用它
// 1. 给函数对象添加方法:
Function.prototype.hyapply = function(thisArg, otherArgs) {
// this --> 调用的函数对象
// thisArg --> 传入的第一个参数, 要绑定的this
// console.log(this) // --> 当前调用的函数对象
// this.apply(thisArg)
// 1. 获取thisArg, 并且确保是一个对象类型
thisArg = (thisArg === null || thisArg === undefined) ? window : Object(thisArg)
// thisArg.fn = this
//下边的代码是上边代码更为严谨的写法
Object.defineProperty(thisArg, "fn", {
enumerable: false,
configurable: true,
value: this//这里指的调用apply的函数
})
thisArg.fn(...otherArgs)
delete thisArg.fn
}
foo.hyapply({ name: "why" }, ["james", 25])
foo.hyapply(123, ["why", 18])
foo.hyapply(null, ["kobe", 30])
手写实现bind函数
与apply不同之处在于:参数分两次传递,第一次是调用bind传递的参数(otherArgs)和第二次调用bind之后返回的函数调用时传递的参数(newArgs)
// 实现hybind函数
Function.prototype.hybind = function(thisArg, ...otherArgs) {
// console.log(this) // -> foo函数对象
thisArg = thisArg === null || thisArg === undefined ? window : Object(thisArg)
Object.defineProperty(thisArg, "fn", {
enumerable: false,
configurable: true,
writable: false,
value: this
})
return (...newArgs) => {
// var allArgs = otherArgs.concat(newArgs)
var allArgs = [...otherArgs, ...newArgs]
thisArg.fn(...allArgs)
}
}
var newFoo = foo.hybind("abc", "kobe", 30)
newFoo(1.88, "广州市")
let/const基本使用
在ES5中我们声明变量都是使用的var关键字,从ES6开始新增了两个关键字可以声明变量:let、const
let关键字
从直观的角度来说,let和var是没有太大的区别的,都是用于声明一个变量;
const关键字
const关键字是constant的单词的缩写,表示常量、衡量的意思;
它表示保存的数据一旦被赋值,就不能被修改;
但是如果赋值的是引用类型,那么可以通过引用找到对应的对象,修改对象的内容;
注意事项:
- let、const不允许重复声明变量;
- let、const 声明的变量没有存到window上边
- 在该作用域中,被定义之前会造成暂时性死区(定义之前不能被访问)
- 暂时性死区和定义的位置没有关系,和代码执行的顺序有关系
- 暂时性死区形成之后,在该区域内这个标识符不能访问
在开发中如何选择呢?
对于var的使用:
我们需要明白一个事实,var所表现出来的特殊性:比如作用域提升、window全局对象、没有块级作用域等都是一些历史遗留问题;
其实是JavaScript在设计之初的一种语言缺陷;
当然目前市场上也在利用这种缺陷出一系列的面试题,来考察大家对JavaScript语言本身以及底层的理解;
但是在实际工作中,我们可以使用最新的规范来书写,也就是不再使用var来定义变量了;
对于let、const:
对于let和const来说,是目前开发中推荐使用的;
我们会优先使用const,这样可以保证数据的安全性不会被随意的篡改;
只有当我们明确知道一个变量后续会需要被重新赋值时,这个时候再使用let;
这种在很多其他语言里面也都是一种约定俗成的规范,尽量我们也遵守这种规范;
ES6之后新特性
模板字符串
模板字符串还有另外一种用法:标签模板字符串(Tagged Template Literals)。如果我们使用标签模板字符串,并且在调用的时候插入其他的变量:
模板字符串被拆分了;第一个元素是数组,是被模板字符串拆分的字符串组合;后面的元素是一个个模板字符串传入的内容;
默认参数
在ES5之后,允许给函数一个默认值,但要注意这个默认参数及在它后边的参数不会放在foo.length里边,所以一般默认参数放在最后使用
函数的参数是对象类型
function foo({ name, age } = { name: "why", age: 18 }) {
console.log(name, age)
}
function foo({ name = "why", age = 18 } = {}) {
console.log(name, age)
}
foo()
展开运算符
在构建对象字面量时,可以运用展开运算符
对象字面量 指的是在 JavaScript 中,通过直接书写键值对来创建一个新对象的方法,而不需要使用 new Object()或构造函数。
字面量(Literal) 指的是在代码中直接写出来的值
// ES9(ES2018) —— 注释说明这是 ES2018 标准新增的特性
const obj = {
name: "why",
age: 18
}
// 注释说明:不可以在函数调用时这样用(下文会解释原因)
// foo(...obj) // 在函数调用时,用展开运算符将对应的展开数据进行迭代
// 可迭代对象:数组/string/arguments
// ↑ 这里强调展开运算符主要用于可迭代对象(如数组),而普通对象在 ES2018 之前不是可迭代的
const info = {
...obj, // 核心用法:将 obj 的属性展开到新对象中
height: 1.88, // 新增属性
address: "广州市" // 新增属性
}
console.log(info) // 输出合并后的新对象
引用赋值
浅拷贝:只拷贝了第一层代码
深拷贝:所有代码都是新生成的,不存在地址的直接引用,可以利用JSON,
// 1.引用赋值
// const info1 = obj
// 2.浅拷贝
// const info2 = {
// ...obj
// }
// info2.name = "kobe"
// console.log(obj.name)
// console.log(info2.name)
// info2.friend.name = "james"
// console.log(obj.friend.name)
// 3.深拷贝
// 方式一:第三方库
// 方式二:自己实现
// function deepCopy(obj) {}
// 方式三:利用现有的JS机制,实现深拷贝JSON
const info3 = JSON.parse(JSON.stringify(obj))
console.log(info3.friend.name)
info3.friend.name = "james"


symbol的基本使用
在ES6之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突;
比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们在不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性;比如开发中我们使用混入,那么混入中出现了同名的属性,必然有一个会被覆盖掉;
Symbol就是为了解决上面的问题,用来生成一个独一无二的值。
Symbol值是通过Symbol函数来生成的,生成后可以作为属性名;
也就是在ES6中,对象的属性名可以使用字符串,也可以使用Symbol值;
Symbol即使多次创建值,它们也是不同的:Symbol函数执行后每次创建出来的值都是独一无二的;
我们也可以在创建Symbol值的时候传入一个描述description:这个是ES2019(ES10)新增的特性;
const s1 = Symbol()
// const info = { name: "why" }
const obj = {
[s1]: "aaa" // 3. 使用计算属性名语法,将 s1 作为属性名,值为 "aaa"
}
console.log(obj) // 4. 打印 obj,输出:{ [Symbol()]: 'aaa' }
const s2 = Symbol()
obj[s2] = "bbb"
console.log(obj) //[Symbol()]: 'bbb' }
function foo(obj) {
const sKey = Symbol()
obj[sKey] = function() {}
delete obj[sKey]
}
foo(obj)
获取symbol的key:Object.getOwnPropertySymbols(obj)
set 和 weakset区别:
weakset的key只能是对象类型,否则会报错;weakset创建的键值对是弱引用,可能会被销毁;weakset不能遍历
map:键值对的形式存放,和对象不同的是map的key可以是任一类型;利用for of 遍历的时候,返回值是一个key和value组成的数组;
weakmap的key只能是对象类型,否则会报错;weakmapt创建的键值对是弱引用,可能会被销毁;weakmap不能遍历
Proxy
Proxy的set和get捕获器
如果我们想要侦听某些具体的操作,那么就可以在handler中添加对应的捕捉器(Trap):
set和get分别对应的是函数类型;
set函数有四个参数:
target:目标对象(侦听的对象);
property:将被设置的属性key;
value:新属性值;
receiver:调用的代理对象;
get函数有三个参数:
target:目标对象(侦听的对象);
property:被获取的属性key;
receiver:调用的代理对象;
const objProxy = new Proxy(obj, {
//in操作符的捕捉器
has: function(target, key) {
console.log("has捕捉器", key)
return key in target
},
set: function(target, key, value) {
console.log("set捕捉器", key)
target[key] = value
},
get: function(target, key) {
console.log("get捕捉器", key)
return target[key]
},
//删除操作符的捕捉器
deleteProperty: function(target, key) {
console.log("delete捕捉器")
delete target[key]
}
})
Reflect Vue的响应原理
Reflect的作用
Reflect也是ES6新增的一个API,它是一个对象,字面的意思是反射。
那么这个Reflect有什么用呢?
它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法;
比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf();
比如Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty();
如果我们有Object可以做这些操作,那么为什么还需要有Reflect这样的新增对象呢?
这是因为在早期的ECMA规范中没有考虑到这种对对象本身的操作如何设计会更加规范,所以将这些API放到了Object上面;
但是Object作为一个构造函数,这些操作实际上放到它身上并不合适;
另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的;
所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上;
另外在使用Proxy时,可以做到不操作原对象;
- 好处一: 代理对象的目的: 不再直接操作原对象
- 好处二: Reflect.set 方法有返回Boolean值, 可以判断本次操作是否成功
- 好处三:
receiver 就是外层Proxy对象
Reflect.set/get最后一个参数, 可以决定对象访问器setter/getter的this指向
const obj = {
_name: "why",
set name(newValue) {
console.log("this:", this) // 默认是obj
this._name = newValue
},
get name() {
return this._name
}
}
// obj.name = "aaaa"
// console.log(obj.name)
// obj.name = "kobe"
const objProxy = new Proxy(obj, {
set: function(target, key, newValue, receiver) {
// target[key] = newValue
console.log("proxy中设置方法被调用")
const isSuccess = Reflect.set(target, key, newValue, receiver)
if (!isSuccess) {
throw new Error(`set ${key} failure`)
}
},
get: function(target, key, receiver) {
console.log("proxy中获取方法被调用")
return Reflect.get(target, key, receiver)
}
})
在如上代码中,修改name属性时,其实会有一次修改和一次访问,一次是set函数,另一次是在set函数内部访问obj对象的name属性,可是因为set函数内部使用的是this(默认指向的是obj对象),如果Reflect中没有传receiver参数,那么在会导致obj对象中的set方法中的this指向源对象,而不是代理对象,就会监听不到第二次访问,在传入receiver之后,就变成了访问的是代理对象,第二次访问就可以监听到了。
Promise
请求函数得到结果之后,获取到对应的回调,但是它存在两个主要的问题:
第一,我们需要自己来设计回调函数、回调函数的名称、回调函数的使用等;
第二,对于不同的人、不同的框架设计出来的方案是不同的,那么我们必须耐心去看别人的源码或者文档,以便可以理解它这个函数到底怎么用;
我们来看一下Promise的API是怎么样的:
Promise是一个类,可以翻译成 承诺、许诺 、期约;
当我们需要的时候,给予调用者一个承诺:待会儿我会给你回调数据时,就可以创建一个Promise的对象;在通过new创建Promise对象时,我们需要传入一个回调函数,我们称之为executor,这个回调函数会被立即执行,并且给传入另外两个回调函数resolve、reject;
当我们调用resolve回调函数时,会执行Promise对象的then方法传入的回调函数;
当我们调用reject回调函数时,会执行Promise对象的catch方法传入的回调函数;
当resolve(promise)时,如果resolve的值本身是Promise对象,那么当前的Promise的状态会有传入的Promise来决定
resolve§
Promise 的三种状态
在 Promise 的生命周期中,它会处于以下三种状态之一:
待定 (Pending):初始状态。既没有被兑现,也没有被拒绝。
触发时机:当执行 executor中的代码时,处于该状态。
已兑现 (Fulfilled):意味着操作成功完成。
触发时机:执行了 resolve函数时,Promise 状态变为 fulfilled。
已拒绝 (Rejected):意味着操作失败。
触发时机:执行了 reject函数时,Promise 状态变为 rejected。
Executor
Executor是在创建Promise时需要传入的一个回调函数,这个回调函数会被立即执行,并且传入两个参数:
通常我们会在Executor中确定我们的Promise状态:
通过resolve,可以兑现(fulfilled)Promise的状态,我们也可以称之为已决议(resolved);
通过reject,可以拒绝(rejected)Promise的状态;
这里需要注意:一旦状态被确定下来,Promise的状态会被锁死,该Promise的状态是不可更改的, 在我们调用resolve的时候,如果resolve传入的值本身不是一个Promise,那么会将该Promise的状态变成兑现(fulfilled);在之后我们去调用reject时,已经不会有任何的响应了(并不是这行代码不会执行,而是无法改变Promise状态);
then和catch的返回值
promise 支持链式调用
因为 then 方法本身就会返回一个新的 Promise
这个新 Promise 的决议是等到 then 方法传入的回调函数有返回值,系统内部会把这个返回值包装成一个新的promise,链式中的 then 是在等待这个新的 Promise 有决议之后执行的
catch也会返回一个promise,与then不同的是,它并非执行某一个特定的promise的reject,而是捕获最先抛出异常的reject
const promise = new Promise((resolve, reject) => {
resolve("aaaaaaa")
// reject()
})
promise.then(res => {
console.log("第一个then方法:", res)
return "bbbbbbbb"
}).then(res => {
console.log("第二个then方法:", res)
return "cccccccc"
}).then(res => {
console.log("第三个then方法:", res)
})
// promise.then(res => {
// console.log("添加第二个then方法:", res)
// })
//catch
// 2. catch方法的执行时机
promise.then(res => {
console.log("then第一次回调:", res)
// throw new Error("第二个Promise的异常error")
return "bbbbbb"
}).then(res => {
console.log("then第二次回调:", res)
throw new Error("第三个Promise的异常")
}).then(res => {
console.log("then第三次回调:", res)
}).catch(err => {
console.log("catch回调被执行:", err)
})
promise的类方法
then、catch、finally方法都属于Promise的实例方法,都是存放在Promise的prototype上的,有时候我们已经有一个现成的内容了,希望将其转成Promise来使用,这个时候我们可以使用 Promise.resolve 方法来完成。
Promise.resolve的用法相当于new Promise,并且执行resolve操作:
// 类方法
Promise.resolve("why")
// 等价于
new Promise((resolve) => resolve("why"))
const promise = Promise.reject("rejected error")
promise.catch(err => {
console.log("err:", err)
})
// 相当于
// new Promise((_, reject) => {
// reject("rejected error")
// })
resolve参数的形态:
情况一:参数是一个普通的值或者对象
情况二:参数本身是Promise
情况三:参数是一个thenable
另外一个类方法是Promise.all:
它的作用是将多个Promise包裹在一起形成一个新的Promise;
新的Promise状态由包裹的所有Promise共同决定:
当所有的Promise状态变成fulfilled状态时,新的Promise状态为fulfilled,并且会将所有Promise的返回值组成一个数组;
当有一个Promise状态为reject时,新的Promise状态为reject,并且会将第一个reject的返回值作为参数;
// 创建三个Promise
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
// resolve("p1 resolve")
reject("p1 reject error")
}, 3000)
})
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("p2 resolve")
}, 2000)
})
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("p3 resolve")
}, 5000)
})
// all:全部/所有
Promise.all([p1, p2, p3]).then(res => {
console.log("all promise res:", res)
}).catch(err => {
console.log("all promise err:", err)
})
allsetted方法
all方法的缺陷:当有其中一个Promise变成reject状态时,新Promise就会立即变成对应的reject状态。那么对于resolved的,以及依然处于pending状态的Promise,我们是获取不到对应的结果的。
Promise.allSettled (ES11/ES2020)
该方法会在所有的Promise都有结果(settled),无论是fulfilled,还是rejected时,才会有最终的状态。
并且这个Promise的结果一定是fulfilled的。
allSettled的结果是一个数组,数组中存放着每一个Promise的结果,并且是对应一个对象的。这个对象中包含status状态,以及对应的value值。
Promise.allSettled([p1, p2, p3]).then(res => {
console.log(res)
}).catch(err => {
console.log(err)
})
race方法
如果有一个Promise有了结果,我们就希望决定最终新Promise的状态,那么可以使用race方法:
race是竞技、竞赛的意思,表示多个Promise相互竞争,谁先有结果,那么就使用谁的结果;
any方法
any方法是ES12中新增的方法,和race方法是类似的:
any方法会等到一个fulfilled状态,才会决定新Promise的状态;
如果所有的Promise都是reject的,那么也会等到所有的Promise都变成rejected状态;如果所有的Promise都是reject的,那么会报一个AggregateError的错误
Promise.any([p1, p2, p3]).then(res => {
console.log(res)
}).catch(err => {
console.log("err:", err)
})
iterator-Generator
什么是迭代器?
迭代器(Iterator),使用户在容器对象(Container,例如链表或数组)上遍访的对象,使用该接口无需关心对象的内部实现细节。
从迭代器的定义我们可以看出,迭代器是帮助我们对某个数据结构进行遍历的对象。在JavaScript中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议(Iterator Protocol):
迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式;
在JavaScript中这个标准就是一个特定的 next 方法;
next 方法有如下的要求:
- 一个无参数或者一个参数的函数
- 返回一个应当拥有以下两个属性的对象:
- done (boolean):如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。
- value:迭代器返回的任何 JavaScript 值,done 为 true 时可省略。
const nums = [100, 24, 55, 66, 86]
let indexNum = 0
const numsIterator = {
next: function() {
if (indexNum < nums.length) {
return { done: false, value: nums[indexNum++] }
} else {
return { done: true }
}
}
}
// 封装成一个函数
function createArrayIterator(arr) {
let index = 0
return {
next: function() {
if (index < arr.length) {
return { done: false, value: arr[index++] }
} else {
return { done: true }
}
}
}
}
可迭代对象
将某一个对象变成一个可迭代对象
- 必须实现一个特定的函数: [Symbol.iterator]
- 这个函数需要返回一个迭代器(这个迭代器用于迭代当前的对象)
可迭代对象必然有一个[Symbol.iterator]函数
const infos = {
friends: ["kobe", "james", "curry"],
[Symbol.iterator]: function() {
let index = 0
const infosIterator = {
next: ()=> {
// done: Boolean
// value: 具体值/undefined
if (index < this.friends.length) {
return { done: false, value: this.friends[index++] }
} else {
return { done: true }
}
}
}
return infosIterator
}
}
// 可迭代对象必然具备下面的特点
// const iterator = infos[Symbol.iterator]()
// console.log(iterator.next())
// console.log(iterator.next())
// console.log(iterator.next())
// console.log(iterator.next())
// 可迭代对象可以进行for of操作
for (const item of infos) {
console.log(item)
}
// 可迭代对象必然有一个[Symbol.iterator]函数
// 数组是一个可迭代对象
const students = ["张三", "李四", "王五"]
console.log(students[Symbol.iterator])
const studentIterator = students[Symbol.iterator]()
console.log(studentIterator.next())
console.log(studentIterator.next())
console.log(studentIterator.next())
console.log(studentIterator.next())
可迭代对象的应用
在 JavaScript 中语法:for …of、展开语法(spread syntax)、yield*、解构赋值(Destructuring assignment);
创建一些对象时:new Map([iterable])、new WeakMap([iterable])、new Set([iterable])、new WeakSet([iterable]);
一些方法的调用:Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable);
所有可迭代对象都可以进行for of 遍历
迭代器的中断
迭代器在某些情况下会在没有完全迭代的情况下中断:
循环操作被中断:比如在遍历的过程中通过 break、return、throw中断了循环操作。
解构赋值未取完:比如在解构的时候,没有解构所有的值。
生成器
什么是生成器?
生成器是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等。平时我们会编写很多的函数,这些函数终止的条件通常是返回值或者发生了异常。
生成器函数也是一个函数,但是和普通的函数有一些区别:
首先,生成器函数需要在function的后面加一个符号:*
其次,生成器函数可以通过yield关键字来控制函数的执行流程:
最后,生成器函数的返回值是一个Generator(生成器):
基本使用
// 1. 定义了一个生成器函数
function* foo() {
console.log("1111")
console.log("2222")
yield
console.log("3333")
console.log("4444")
yield
console.log("5555")
console.log("6666")
}
const generator = foo()
// 调用next方法
generator.next()
generator.next()
generator.next()
生成器函数参数的返回值
yield 后边的返回值是next()方法的返回值的value属性值
在调用next方法时,传入的参数被上一次的yield接收,在后续的代码中可以使用
// 1. 定义了一个生成器函数
function* foo(name1) {
console.log("执行内部代码:1111", name1)
console.log("执行内部代码:2222", name1)
const name2 = yield "aaaa"
console.log("执行内部代码:3333", name2)
console.log("执行内部代码:4444", name2)
const name3 = yield "bbbb"
// return "bbbb"
console.log("执行内部代码:5555", name3)
console.log("执行内部代码:6666", name3)
yield "cccc"
return undefined
}
// 2. 调用生成器函数,返回一个 生成器对象
const generator = foo("next1")
// 调用next方法
// console.log(generator.next()) // { done: false, value: "aaaa" }
// console.log(generator.next()) // { done: false, value: "bbbb" }
// console.log(generator.next()) // { done: false, value: "cccc" }
// console.log(generator.next()) // { done: true, value: undefined }
// 3. 在中间位置直接return,结果
// console.log(generator.next()) // { done: false, value: "aaaa" }
// console.log(generator.next()) // { done: true, value: "bbbb" }
// console.log(generator.next()) // { done: true, value: undefined }
// console.log(generator.next()) // { done: true, value: undefined }
// console.log(generator.next()) // { done: true, value: undefined }
// console.log(generator.next()) // { done: true, value: undefined }
// 4. 给函数每次执行的时候,传入参数
console.log(generator.next("next1"))
console.log(generator.next("next2"))
console.log(generator.next("next3"))
// console.log(generator.next())
生成器代替迭代器
// 1. 对之前的代码进行重构(yong)
const names = ["abc", "cba", "nba"]
const nums = [100, 22, 66, 88, 55]
function* createArrayIterator(arr) {
for (let i = 0; i < arr.length; i++) {
yield arr[i]
}
// yield arr[0]
// yield arr[1]
// yield arr[2]
// return undefined
}
const namesIterator = createArrayIterator(names)
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
const numsIterator = createArrayIterator(nums)
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
事实上我们还可以使用yield*来生产一个可迭代对象:
这个这个时候相当于是一种yield的语法糖,只不过会依次迭代这个可迭代对象,每次迭代其中的一个值;
function* createArrayIterator(arr) {
yield* arr
}
await async
await 关键字
await 条件:必须在异步函数中使用
function bar() {
console.log("bar function")
return new Promise(resolve => {
setTimeout(() => {
resolve(123)
}, 10000)
})
}
async function foo() {
console.log("-------")
// await 后续返回一个 Promise,那么会等待 Promise 有结果之后,才会继续执行后续的代码
const res1 = await bar()
console.log("await 后面的代码:", res1)
const res2 = await bar()
console.log("await 后面的代码:", res2)
console.log("+++++++")
}
foo()
JavaScript单线程
浏览器中的JavaScript线程
我们经常会说JavaScript是单线程(可以开启workers)的,但是JavaScript的线程应该有自己的容器进程:浏览器或者Node。
浏览器是一个进程吗,它里面只有一个线程吗?
目前多数的浏览器其实都是多进程的,当我们打开一个tab页面时就会开启一个新的进程,这是为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出;
每个进程中又有很多的线程,其中包括执行JavaScript代码的线程;
JavaScript的代码执行是在一个单独的线程中执行的:
这就意味着JavaScript的代码,在同一个时刻只能做一件事;
如果这件事是非常耗时的,就意味着当前的线程就会被阻塞;
所以真正耗时的操作,实际上并不是由JavaScript线程在执行的:
浏览器的每个进程是多线程的,那么其他线程可以来完成这个耗时的操作;
比如网络请求、定时器,我们只需要在特性的时候执行应该有的回调即可;
JavaScript 本身是单线程的(同一时间只能干一件事),但它通过浏览器提供的多线程环境和事件循环机制,实现了“不阻塞”的异步编程。
会被加入到任务队列(事件队列)中的两个异步操作
- setTimeout((),0),即使时间是0 (会被加入到宏任务队列)
- promise.then回调(会被加入到微任务队列)
但是new Promise((resolve,reject)=>{})花括号内的代码不会被加入到任务队列中
微任务和宏任务
事件循环中并非只维护着一个队列,事实上是有两个队列:
宏任务队列 (macrotask queue):ajax、setTimeout、setInterval、DOM监听、UI Rendering等;
微任务队列 (microtask queue):Promise的then回调、Mutation Observer API、queueMicrotask()等;
那么事件循环对于两个队列的优先级是怎样的呢?
-
main script中的代码优先执行(编写的顶层script代码);
-
在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行:
也就是宏任务执行之前,必须保证微任务队列是空的;
如果不为空,那么就优先执行微任务队列中的任务(回调);
代码执行过程

localStorage 和 sessionStorage
localStorage 和 sessionStorage 的区别
那么它们有什么区别呢?
验证一:关闭网页后重新打开,localStorage 会保留,而 sessionStorage 会被删除;
验证二:在页面内实现跳转,比如a标签的target=“self”,localStorage 会保留,sessionStorage 也会保留;
验证三:在页面外实现跳转(打开新的网页)比如target=“blank”,localStorage 会保留,sessionStorage 不会被保留;
Storage 常见的方法和属性
Storage 对象(包括 localStorage 和 sessionStorage)提供了一套简单直观的 API 来管理本地数据。
-
属性
Storage.length:只读属性。
返回一个整数,表示当前 Storage 对象中存储的数据项数量。 -
常用方法
Storage.key(index):
接收一个数值 n 作为参数,返回存储中第 n 个 key 的名称。通常用于遍历整个存储对象。
Storage.getItem(key)
接收一个 key 作为参数,并返回该 key 对应的 value。如果 key 不存在,则返回 null。
Storage.setItem(key, value):
接收一个 key 和 value,并将该键值对添加到存储中。
注意:如果该 key 已经存在,则会更新其对应的值。
Storage.removeItem(key):
接收一个 key 作为参数,并把该 key 及其对应的值从存储中删除。
Storage.clear():
该方法的作用是清空存储中的所有 key,使存储对象回到初始为空的状态。
防抖和节流
防抖
防抖 (Debounce)定义: 在事件被触发n秒后再执行回调。如果在这n秒内事件又被触发,则重新计时。形象比喻: 像英雄联盟里的“回城”。回城需要 8 秒,如果你在第 5 秒被敌人打了一下(再次触发事件),回城就会被打断,必须重新等 8 秒才能回去。核心逻辑: 只有最后一次操作生效。
防抖基本功能实现:可以实现防抖效果
优化一:优化参数和 this 指向
优化二:优化取消操作(增加取消功能)
优化三:优化立即执行效果(第一次立即执行)
基本实现
function xldebounce(fn, delay) {
let timer = null
const _debounce = () => {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn()
timer = null
}, delay)
}
return _debounce
}
const inputEl = document.querySelector("input")
//underscore的防抖函数实现
// let counter=1
// inputEl.oninput=_.debounce(function(){
// console.log('发送网络请求',this.value)
// },1000)
// 自定义防抖函数实现
let counter = 1
//注意:这里inputEL绑定的是xldebounce函数返回的函数,不是xldebounce函数本身
//传入xldebounce函数的fn是独立调用的,里边的this指向的是window
inputEl.oninput = xldebounce(function () {
console.log('发送网络请求', this.value)
}, 3000)
this指向inputEl
function xldebounce(fn, delay) {
let timer = null
const _debounce = function () {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this)//这里的this向上层找是debounce
timer = null
}, delay)
}
return _debounce
}
const inputEl = document.querySelector("input")
//underscore的防抖函数实现
// let counter=1
// inputEl.oninput=_.debounce(function(){
// console.log('发送网络请求',this.value)
// },1000)
// 自定义防抖函数实现
let counter = 1
//注意:这里inputEL绑定的是xldebounce函数返回的函数,不是xldebounce函数本身
//传入xldebounce函数的fn是独立调用的,里边的this指向的是window
inputEl.oninput = xldebounce(function () {
console.log('发送网络请求', this)
}, 3000)
取消操作
_debounce.cancel = function () {
if (timer) {
clearTimeout(timer)
timer = null
}
}
立即执行
function xldebounce(fn, delay, immdiate = false) {
let timer = null
let imdevoke = false
const _debounce = function () {
if (timer) {
clearTimeout(timer)
}
if (immdiate && !imdevoke) {
fn.apply(this)
imdevoke = true
return
}
timer = setTimeout(() => {
fn.apply(this)//这里的this向上层找是debounce
timer = null
}, delay)
}
_debounce.cancel = function () {
if (timer) {
clearTimeout(timer)
timer = null
}
}
return _debounce
}
const inputEl = document.querySelector("input")
//underscore的防抖函数实现
// let counter=1
// inputEl.oninput=_.debounce(function(){
// console.log('发送网络请求',this.value)
// },1000)
// 自定义防抖函数实现
let counter = 1
//注意:这里inputEL绑定的是xldebounce函数返回的函数,不是xldebounce函数本身
//传入xldebounce函数的fn是独立调用的,里边的this指向的是window
inputEl.oninput = xldebounce(function () {
console.log('发送网络请求', this)
}, 3000)
节流
节流 (Throttle)
定义: 在一个单位时间内,不管事件被触发多少次,只执行一次回调。
形象比喻: 像技能冷却 (CD)。如果你放了一个技能,它的冷却时间是 5 秒,那么在这 5 秒内你疯狂按键也是没用的,技能每隔 5 秒才能释放一次。
核心逻辑: 控制执行的最高频率,让函数均匀地触发。
认识节流 throttle 函数
当事件触发时,会执行这个事件的响应函数;
如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数;
不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的;
基本实现
function xlthrottle(fn, interval) {
let startTime = 0
const _throttle = function () {
const nowTime = new Date().getTime()
const waitTime = interval - (nowTime - startTime)
if (waitTime <= 0) {
fn()
startTime = nowTime
}
}
return _throttle
}
//使用库函数实现
const inputEl = document.querySelector("input")
//underscore的节流函数实现
// let counter=1
// inputEl.oninput=_.throttle(function(){
// console.log('发送网络请求',this.value)
// },1000)
// 自定义节流函数实现
let counter = 1
inputEl.oninput = xlthrottle(function () {
console.log('发送网络请求', this.value)
}, 1000)
this指向和参数传递
function xlthrottle(fn, interval) {
let startTime = 0
const _throttle = function (...args) {
const nowTime = new Date().getTime()
const waitTime = interval - (nowTime - startTime)
if (waitTime <= 0) {
fn.apply(this, args)
startTime = nowTime
}
}
return _throttle
}
//使用库函数实现
const inputEl = document.querySelector("input")
//underscore的节流函数实现
// let counter=1
// inputEl.oninput=_.throttle(function(){
// console.log('发送网络请求',this.value)
// },1000)
// 自定义节流函数实现
let counter = 1
//此时浏览器或默认将event作为参数传递给xlthrottle函数,里边包含有输入框的内容,所以在_throttle函数中可以使用args
inputEl.oninput = xlthrottle(function () {
console.log('发送网络请求', this.value)
}, 1000)
深拷贝和浅拷贝
浅拷贝 (Shallow Copy)
定义: 只拷贝对象的第一层属性。
如果属性是基本类型,拷贝的是值。
如果属性是引用类型(对象或数组),拷贝的是内存地址(即指针)。
结果: 原始对象和新对象仍然共享深层的嵌套对象。如果你修改了新对象里嵌套对象的属性,原对象也会跟着变。
常用手段:
- Object.assign({}, obj)
- 扩展运算符 {…obj} 或 […arr]
- Array.prototype.slice()
深拷贝 (Deep Copy)
定义: 递归地拷贝对象的所有层级。它会在内存中开辟一块完全独立的新空间,将原对象中的所有内容(包括嵌套的对象和数组)逐层复制过去。
结果: 两个对象完全独立,修改其中一个,另一个绝对不会受到影响。
常用手段:
- JSON 序列化: JSON.parse(JSON.stringify(obj))(简单但有缺陷:会丢失函数、undefined、正则等)。
缺点:这种深拷贝的方式其实对于函数、Symbol等是无法处理的;
并且如果存在对象的循环引用,也会报错的; - 浏览器原生 API: structuredClone(obj)(现代浏览器推荐方案)。
- 第三方库: Lodash 的 _.cloneDeep(obj)(最稳健,能处理各种边缘情况)。
自定义函数实现
// 需求:判断一个标识符是否是对象类型
function isObject(value) {
// null, object, function, array
// null -> object
// function -> function -> true
// object/array -> object -> true
const valueType = typeof value
return (value !== null) && ( valueType === "object" || valueType === "function" )
}
// 深拷贝函数
function deepCopy(originValue) {
// 1. 如果是原始类型,直接返回
if (!isObject(originValue)) {
return originValue
}
// 2. 如果是对象类型,才需要创建对象
const newObj = Array.isArray(originValue)?[]:{}
for (const key in originValue) {
newObj[key] = deepCopy(originValue[key]);
}
return newObj
}
const info = {
name: "why",
age: 18,
friend: {
name: "kobe",
address: {
name: "洛杉矶",
detail: "斯坦普斯中心"
}
}
}
const newObj = deepCopy("abc")
console.log(newObj)
事件总线
为了实现不同组件间通信
class XLEventBus {
constructor() {
this.eventMap = {}
}
on(eventName, eventFn) {
let eventFns = this.eventMap[eventName]
if (!eventFns) {
eventFns = []
this.eventMap[eventName] = eventFns
}
eventFns.push(eventFn)
}
off(eventName, eventFn) {
let eventFns = this.eventMap[eventName]
if (!eventFns) {
return
}
eventFns = eventFns.filter(fn => fn !== eventFn)
this.eventMap[eventName] = eventFns
}
emit(eventName, ...args) {
let eventFns = this.eventMap[eventName]
if (!eventFns) {
return
}
eventFns.forEach(fn => fn.apply(this, args))
}
}
// 使用过程
const eventBus = new XLEventBus()
// aside.vue组件中监听事件
eventBus.on("navclick", (name, age, height) => {
console.log("navclick listener 01", name, age, height)
})
const click = () => {
console.log("navclick listener 02")
}
eventBus.on("navclick", click)
setTimeout(() => {
eventBus.off("navclick", click)
}, 5000);
eventBus.on("asideclick", () => {
console.log("asideclick listener")
})
// nav.vue
const navBtnEl = document.querySelector(".nav-btn")
navBtnEl.onclick = function () {
console.log("自己监听到了")
eventBus.emit("navclick", "why", 18, 1.88)
}
XHR
网页的两种渲染方式:
服务器渲染和客户端渲染(前后端分离)
前后端分离渲染,通常指的是以客户端渲染(CSR)为核心的模式:后端只提供 API(JSON 数据),前端负责页面的结构和渲染。这种架构被广泛用于现代 Web 应用(如中后台系统、类 App 的交互型平台),它的优缺点主要体现在以下几个方面:
✅ 优点
开发解耦,效率更高
前后端通过 API 契约并行开发,前端不再依赖后端模板,后端不用管页面布局,团队协作和维护成本更低。
交互体验更流畅
属于单页应用(SPA)模式,页面切换无需整页刷新,只需拉取数据更新视图,更接近原生 App 的体验。
复用性强,生态丰富
前端可独立部署,同一套后端 API 能同时支撑 Web、移动端、小程序等多端场景;也能充分利用 React、Vue 等成熟组件生态。
⚠️ 缺点
首屏加载相对较慢
浏览器需先下载并执行较庞大的 JS 框架包,再请求数据渲染页面,弱网或低端设备上首屏白屏时间更明显。
SEO 不够友好
服务端初始返回的是空 HTML,搜索引擎爬虫早期难以抓取真实内容(虽可通过预渲染或 SSR 混合方案弥补,但会增加复杂度)。
浏览器性能和适配压力
渲染和计算全放客户端,对设备性能、兼容性要求更高,过复杂的页面容易造成低端机卡顿。
如果业务侧重强交互、低 SEO 依赖(比如内部管理系统、控制台),前后端分离(CSR)非常合适;如果是内容为主的官网、营销页,通常会考虑 SSR 或静态生成做混合优化。
XHR的基本使用
在这里插入代码片// 1. 创建XMLHttpRequest对象
const xhr = new XMLHttpRequest()
// 2. 监听状态的改变(宏任务)
xhr.onreadystatechange = function () {
console.log(xhr.response)
if (xhr.readyState !== XMLHttpRequest.DONE) return
if (xhr.status !== 200) console.log(xhr.status)
// 将字符串转成JSON对象(js对象)
const resJSON = JSON.parse(xhr.response)
const banners = resJSON.data.banner.list
console.log(banners)
}
// 3. 配置请求open
// method: 请求的方式(get/post/delete/put/patch...)
// url: 请求的地址
xhr.open("get", "http://123.207.32.32:8000/home/multidata")
// 4. 发送请求(浏览器帮助发送对应请求)
xhr.send()
除了 onreadystatechange还有其他的事件可以监听
loadstart:请求开始。
progress:一个响应数据包到达,此时整个 response body 都在 response 中。
abort:调用 xhr.abort()取消了该请求。
error:发生连接错误,例如,域错误。不会发生诸如 404 这类的 HTTP 错误。
load:请求成功完成。
timeout:由于请求超时而取消了该请求(仅发生在设置了 timeout 的情况下)。
loadend:在 load,error,timeout 或 abort 之后触发。
注:以上内容涉及的是 XMLHttpRequest(XHR)对象的事件监听器。
响应数据和响应类型
发送了请求后,我们需要获取对应的结果:response属性
XMLHttpRequest response属性返回响应的正文内容;
返回的类型取决于responseType的属性设置;
通过responseType可以设置获取数据的类型,如果将 responseType的值设置为空字符串,则会使用 text作为默认值。
xhr.responseType = "json"
和responseText、responseXML的区别:
早期通常服务器返回的数据是普通的文本和XML,所以我们通常会通过responseText、responseXML来获取响应结果;之后将它们转化成JavaScript对象形式;
目前服务器基本返回的都是json数据,直接设置为json即可;
HTTP响应的状态status
■ XMLHttpRequest的state是用于记录xhr对象本身的状态变化,并非针对于HTTP的网络请求状态。
■ 如果我们希望获取HTTP响应的网络状态,可以通过status和statusText来获取:
console.log(xhr.status)
console.log(xhr.statusText)

GET和POST的四种传参方式
常见的传递给服务器数据的方式有如下几种:
方式一:GET请求的query参数
方式二:POST请求 x-www-form-urlencoded 格式
方式三:POST请求 FormData 格式
方式四:POST请求 JSON 格式
// 1. 传递参数方式一: get -> query
// xhr.open("get", "http://123.207.32.32:1888/02_param/get?name=why&age=18&address=广州市")
// 2. 传递参数方式二: post -> urlencoded
// xhr.open("post", "http://123.207.32.32:1888/02_param/posturl")
// // 发送请求(请求体body)
// xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
// xhr.send("name=why&age=18&address=广州市")
// 3. 传递参数方式三: post -> formdata
// xhr.open("post", "http://123.207.32.32:1888/02_param/postform")
// // formElement 对象转成FormData 对象
// const formData = new FormData(formEL)
// xhr.send(formData)
// 4. 传递参数方式四: post -> json
xhr.open("post", "http://123.207.32.32:1888/02_param/postjson")
//告诉服务器——请求体(body)里的数据是 JSON 格式。
xhr.setRequestHeader("Content-type", "application/json")
xhr.send(JSON.stringify({name: "why", age: 18, height: 1.88}))
完整封装
但一般不会使用,制作一个练习,后续会直接使用axios库
function hyajax({
url,
method = "get",
data = {},
headers = {},
success,
failure
}) {
// 1. 创建 XHR 对象
const xhr = new XMLHttpRequest();
// 2. 监听请求状态变化 (onload 表示请求完成,无论成功与否)
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
// 状态码 2xx 表示成功
// 因为设置了 responseType = 'json',这里会自动解析成对象
success && success(xhr.response);
} else {
// 其他状态码表示失败
failure && failure({
status: xhr.status,
message: xhr.statusText
});
}
};
// 【新增】设置响应类型,让浏览器自动把 JSON 字符串转为对象
xhr.responseType = "json";
// 3. 判断请求方式并处理参数
const upperMethod = method.toUpperCase();
if (upperMethod === "GET") {
// --- GET 请求:拼接 Query 参数 ---
const queryStrings = [];
for (const key in data) {
queryStrings.push(`${key}=${data[key]}`);
}
// 如果原本 url 已有 ?,这里简单处理加 &,否则加 ? (这里为了演示简单,直接加 ?)
url = url + "?" + queryStrings.join("&");
xhr.open(upperMethod, url);
// 设置请求头(如果有自定义的话,否则默认)
for(let key in headers) {
xhr.setRequestHeader(key, headers[key]);
}
xhr.send();
} else {
// --- POST 请求:发送 JSON 数据 ---
xhr.open(upperMethod, url);
// 默认设置 JSON 格式头(如果用户没传 headers,或者没覆盖 Content-Type)
// 注意:如果 headers 对象里有 'Content-Type',则使用用户的
if (!headers['Content-Type']) {
xhr.setRequestHeader("Content-type", "application/json");
}
// 设置用户传入的其他自定义头
for(let key in headers) {
xhr.setRequestHeader(key, headers[key]);
}
// 发送数据:转为 JSON 字符串
xhr.send(JSON.stringify(data));
}
}
// ==========================================
// 测试调用示例
// ==========================================
// 1. GET 请求测试
hyajax({
url: "http://123.207.32.32:1888/02_param/get",
method: "GET",
data: {
name: "why",
age: 18
},
success: function(res) {
console.log("GET 成功:", res);
},
failure: function(err) {
console.log("GET 失败:", err);
}
});
// 2. POST 请求测试
/*
hyajax({
url: "http://123.207.32.32:1888/02_param/postjson",
method: "POST",
data: {
name: "why",
age: 18,
height: 1.88
},
headers: {
// Authorization: "Bearer token123" // 可以自定义头
},
success: function(res) {
console.log("POST 成功:", res);
},
failure: function(err) {
console.log("POST 失败:", err);
}
});
*/
Fetch
认识Fetch和Fetch API
Fetch可以看做是早期的XMLHttpRequest的替代方案,它提供了一种更加现代的处理方案:
比如返回值是一个Promise,提供了一种更加优雅的处理结果方式
在请求发送成功时,调用resolve回调then;
在请求发送失败时,调用reject回调catch;
比如不像XMLHttpRequest一样,所有的操作都在一个对象上;
fetch函数的使用:
Promise fetch(input[, init]);
input:定义要获取的资源地址,可以是一个URL字符串,也可以使用一个Request对象(实验性特性)类型;
init:其他初始化参数
method:请求使用的方法,如 GET、POST;
headers:请求的头信息;
body:请求的 body 信息;
基本实现
async function getData() {
const response = await fetch("http://123.207.32.32:1888/02_param/postjson", {
method: "post",
// headers: { ... } // 这部分被注释掉了
body: JSON.stringify({
name: "why",
age: 18
})
})
const res = await response.json()
console.log("res:", res)
}
getData()
Fetch的数据响应主要分为两个阶段:
阶段一:当服务器返回了响应(response)
fetch 返回的 promise 就使用内建的 Response class 对象来对应答头进行解析;
在这个阶段,我们可以通过检查响应头,来检查 HTTP 状态以确定请求是否成功;
如果 fetch 无法建立一个 HTTP 请求,例如网络问题,亦或是请求的网址不存在,那么 promise 就会 reject;
异常的 HTTP 状态,例如 404 或 500,不会导致出现 error;
我们可以在 response 的属性中看到 HTTP 状态:
status: HTTP 状态码,例如 200;
ok: 布尔值,如果 HTTP 状态码为 200-299,则为 true;
第二阶段,为了获取 response body,我们需要使用一个其他的方法调用。
response.text() —— 读取 response,并以文本形式返回 response;
response.json() —— 将 response 解析为 JSON;
更多推荐

所有评论(0)