函数的this指向
函数this的绑定规则
默认绑定
- 函数默认调用时,函数的this都指向window
// "use strict"
// 1.普通的函数被独立的调用
function foo() {
console.log('foo', this);
}
foo()//window
// 2.函数定义在对象中,但是独立调用
var obj = {
name: "why",
bar: function() {
console.log("bar", this)
}
}
var baz = obj.bar
baz()//window
// 3.高阶函数
function test(fn) {
fn()
}
test(obj.bar)//window
// 4.严格模式下,独立调用的函数中的this指向的是undefind
// "use strict"
隐式绑定
- 通过某个对象来调用函数,函数中的this指向调用它的对象
function foo() {
console.log("foo函数", this);
}
var obj = {
name: "why",
bar: foo
}
obj.bar()//obj
new绑定
- JS中的函数可以当作一个类的构造函数来使用,也就是使用new关键字
- 使用new创建一个对象时,其中的this会指向这个对象
- 创建一个新对象
- 这个新对象会被执行prototype连接
- this会指向这个新对象
- 函数会返回这个新对象
function Foo() {
console.log("foo函数", this);
this.name = "why"
}
new Foo()//Foo {}
显示绑定
- 使用call、apply显示绑定this
function foo(name, age, height) {
console.log("foo函数", this);
console.log(name, age, height)
}
// ()调用
foo("why", 18, 1.88)
// apply
// 第一个参数绑定this
// 第二个参数:传入额外的实参,以数组的形式
foo.apply("apply", ["why", 30, 1.98])
// call
// 第一个参数:绑定this
// 参数列表:后续的参数以参数列表的形式传入实参
foo.call("call", "james", 25, 1.80)
- 当我们希望一个函数总是显示的绑定到一个对象上时
- 使用bind方法,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()//obj
bar()//obj
bar()//obj
// 2.bind函数的其他参数
var bar2 = foo.bind(obj, "why", 18, 1.99)
bar2("james")//why, 18,1.99, james
this绑定的规则优先级
- 默认规则的优先级最低
- 显示绑定高于隐式绑定
- new绑定高于隐式绑定
- new绑定高于bind的优先级
- new绑定不能与call、apply同时使用
- bind优先级高于call、apply
优先级:
- new
- bind
- apply、call
- 隐式绑定
- 默认绑定
this绑定之外的情况
-
情况一:如果在显示绑定中,我们传入一个null或者undefind,那么这个显示绑定会被忽略,使用默认规则:window
-
情况二:创建一个函数的间接引用,这种情况使用默认绑定规则
- 赋值(obj2.foo = obj1.foo)的结果是foo
- 直接调用foo函数,就是默认绑定规则
(obj2.foo = obj1.foo)()
-
情况三:箭头函数
箭头函数
- 箭头函数式ES6新增的一种函数编写方法,比函数表达式更加简洁
- 不会绑定this、arguments
- 不能作为构造函数来使用,不能和new一起使用,会抛出错误(因为箭头函数没有绑定this)
// 1.之前的方式
function foo1() {}
var foo2 = function(){}
// 2.箭头函数
var foo3 = (name, age) => {
console.log(name, age);
}
箭头函数的编写优化
- 优化一:如果只有一个参数,()可以省略
nums.forEach(item => {})
- 优化二:如果函数执行体中只有一行代码,那么可以省略大括号
- 并且这行代码的返回值会作为整个函数的返回值
- return也要省略,因为会自动返回值
nums.forEach(item => console.log(item))
nums.filter(item => true)
- 优化三:如果函数执行体只有返回一个对象,那么需要给这个对象加上()
- 在redux中经常使用
var foo = () => {
return { name: "abc" }
}
//返回对象时,如果你想省略大括号,需要在对象外加小括号
var bar = () => ({name: "abc"})
箭头函数中的this使用
- 箭头函数中没有this
- 所以当我们在函数中使用this时,它会在上层作用域中查找this
// 1.箭头函数中没有this
// 所以当我们在函数中使用this时,它会在上层作用域中查找this
var bar = () => {
console.log('bar:', this);
}
bar()//window
bar.apply('aaa')//window
// 2.this的查找规则
var obj = {
name: "obj",
foo: function() {
var bar = () => {
console.log("bar", this)
}
return bar
}
}
var fn = obj.foo()
fn.apply("bbb") //obj
var obj2 = {
name: "obj",
foo: () => {
var bar = () => {
console.log("bar", this)
}
return bar
}
}
var fn2 = obj2.foo()
fn2.apply("bbb") //window
箭头函数的this应用
// 网络请求场景
function request(url,fn) {
var results = ["a", "b", "c"]
fn(results)
}
var obj = {
names: [],
network: function() {
request("/lists", res => this.names = [].concat(res))
}
}
obj.network()
console.log(obj.names)
this面试题
var name = "window"
var person = {
name: "person",
sayName: function () {
console.log(this.name);
}
}
function sayName() {
var sss = person.sayName;
sss();//window
person.sayName();//person
(person.sayName)();//person :这个跟上面那个一样是隐式绑定
(b = person.sayName)()//winow:间接函数引用,返回一个独立的函数,然后独立的函数调用
}
sayName()
// 面试题2
var name = "window"
var person1 = {
name: "person1",
foo1: function () {
console.log(this.name);
},
foo2: () => console.log(this.name),
foo3: function () {
return function () {
console.log(this.name)
}
},
foo4: function() {
return () => {
console.log(this.name)
}
}
}
var person2 = { name: "person2"}
person1.foo1();//person1
person1.foo1.call(person2);//person2
person1.foo2();//window
person1.foo2.call(person2); //window
person1.foo3()();//window
person1.foo3.call(person2)();//window
person1.foo3().call(person2);//person2
person1.foo4()(); //person1
person1.foo4.call(person2)();//person2
person1.foo4().call(person2);//person1
// 面试题3
var name = "window"
function Person(name) {
this.name = name
this.foo1 = function () {
console.log(this.name);
},
this.foo2 = () => console.log(this.name),
this.foo3 = function () {
return function () {
console.log(this.name)
}
},
this.foo4 = function () {
return () => {
console.log(this.name)
}
}
}
var person1 = new Person("person1")
var person2 = new Person('person2')
person1.foo1();//person1
person1.foo1.call(person2);//person2
person1.foo2();//window是错的,构造函数有自己的作用域,箭头函数会在这个作用域中找到person1
person1.foo2.call(person2); //person1:理由同上
person1.foo3()();//window
person1.foo3.call(person2)();//window
person1.foo3().call(person2);//person2
person1.foo4()(); //person1
person1.foo4.call(person2)();//person2
person1.foo4().call(person2);//person1
// 面试题4
var name = "window"
function Person (name) {
this.name = name
this.obj = {
name: "obj",
foo1: function () {
return function () {
console.log(this.name);
}
},
foo2: function () {
return () => {
console.log(this.name)
}
}
}
}
var person1 = new Person("person1")
var person2 = new Person('person2')
person1.obj.foo1()();//window
person1.obj.foo1.call(person2)()//window
person1.obj.foo1().call(person2)//person2
person1.obj.foo2()();//obj
person1.obj.foo2.call(person2)()//person2
person1.obj.foo2().call(person2)//obj
浏览器渲染原理
输入URL后资源是如何加载的
渲染引擎如何解析页面
更详细的过程:
- DOM树上有些元素可能是隐藏的,那么在渲染树中是没有这些元素的
- 所以DOM树和渲染树不是一一对应的
- 单独的layout计算位置大小信息,防止有些DOM树中隐藏的元素
解析一:HTML解析过程
- 因为默认情况下服务器会给浏览器返回index.html文件,所以解析HTML是所有步骤的开始
- 解析HTML会构建DOM Tree
解析二:生成CSS规则
- 在解析HTML过程中,如果遇到CSS的link元素,那么会由浏览器负责下载对应的CSS文件
- 注意:下载CSS文件不会影响DOM解析
- 浏览器下载完CSS文件后,就会对CSS文件进行解析,解析出对应的规则树:
- 我们可以称之为CSSDOM(CSS对象模型)
解析三:构建Render Tree
- 当有了DOM Tree和CSSDOM Tree后,就可以结合来构建Render Tree了
- CSS规则树中display为none的值不会出现在DOM树中
- 注意:link元素不会阻塞DOM树的构建过程,但是会阻塞Render树的构建过程
- 因为Render树在构建时,需要对应的CSSOM Tree
解析四:布局和绘制
- 第四步是在渲染树上运行布局以计算每个节点的几何体
- 渲染树会表示显示哪些节点以及其他样式,但是不表示每个节点的尺寸、位置等信息
- 布局就是确定渲染树中所有节点的宽度、高度和位置信息
- 第五步是将每个节点绘制到屏幕上
- 在绘制阶段,浏览器将布局阶段计算的每个frame转为屏幕上的实际的像素点
- 包括将元素的可见部分进行绘制,比如文本、颜色、边框、阴影、替换元素
回流和重绘
- 回流reflow:也可以称之为重排
- 第一次确定节点的大小和位置,称之为布局
- 之后对节点的大小、位置修改重新计算,称之为回流
- 不一定会重建DOM树,但是一定会重新布局
- 什么情况下引起回流?
- 比如DOM结构发生变化(添加新的节点或者移除节点 )
- 比如改变了布局(修改了width、height、padding、font-size等值)
- 比如窗口resize(修改了窗口的尺寸等)
- 比如调用getComputedStyle方法获取尺寸、位置等信息
- 重绘repaint:
- 第一次渲染内容称之为绘制
- 之后重新渲染称之为重绘
- 什么情况下会引起重绘呢?
- 比如修改背景色、文字颜色、边框颜色、样式等
- 回流一定会引起重绘,所以回流是一件很消耗性能的事情
- 所以在开发中要尽量避免发生回流:
- 集中改变样式
- 比如通过cssText修改,比如通过添加class修改
- 尽量避免频繁的操作DOM
* 我们可以在一个DocumentFragment或者父元素中将要操作的DOM操作完成,再一次性的操作 - 尽量避免通过getComputedStyle获取尺寸、位置等信息
- 对某些元素使用position的absolute或者fixed
- 并不是不会引起回流,不会对其他元素造成影响,开销相对较小
- 读写分离
- 离线改变DOM
特殊解析 - composite合成
- 绘制的过程,可以将布局后的元素绘制到多个合成图层中
- 这是浏览器的一种优化手段
- 默认情况下,标准流中的内容都是被绘制在同一个图层中的
- 而一些特殊的属性,会创建一个新的合成层,并且新的图层可以利用GPU来加速绘制
- 因为每个合成层都是单独渲染的
- 哪些属性会形成新的合成层呢?
- 3D transforms
- video、vanvas、iframe
- opacity动画转换时
- positon:fixed
- will-change
- animation或transition设置了opacity、transform
- 分层是以内存为代价提升性能的,所以不要过度使用
script元素和页面解析的关系
- 页面渲染过程中,JS在哪里?
- 浏览器在解析HTML的过程中,遇到了script元素是不能继续构建DOM树的
- 它会停止继续构建,首先下载JS代码,并且执行JS的脚本
- 只有等到JS脚本执行结束之后,才会继续解析HTML,构建DOM树
- 为什么要这样做呢?
- 因为JS的作用之一就是操作DOM,并且可以修改DOM
- 如果我们等到DOM树构建完成并且渲染再执行JS,会造成严重的回流和重绘,影响页面的性能
- 所以在遇到script元素时,优先下载和执行JS代码,再继续构建DOM树
- 带来的问题:
- 目前的开发模式中,脚本往往比HTML页面更‘重“,处理时间需要更长
- 所以造成页面的解析阻塞,在脚本下载、执行完成之前,用户在界面上什么都看不到
- 为了解决这个问题,script元素给我们提供了两个属性:defer和async
defer和async的使用
defer属性
- defer属性告诉浏览器不要等待脚本下载,而继续解析HTML,构建DOM Tree
- 脚本会由浏览器来进行下载,但是不会阻塞DOM Tree的构建过程
- 如果脚本提前下载好了,它会等待DOM Tree构建完成,在DOMContentLoaded事件之前先执行defer中的代码
- DOM 树已经构建完毕,但是像是
<img>
和样式表等外部资源可能并没有下载完毕。
- DOM 树已经构建完毕,但是像是
- 所以DOMContentLoaded总是等待defer中的代码优先执行完成
- 另外多个带defer的脚本可以保持正确的顺序执行
- 从某种角度来说,defer可以提高页面的性能,并且建议放到head中
- defer只适用于外部脚本
async属性
- 与defer有些类似,能让脚本不阻塞页面
- async是让一个脚本完全独立的
- 浏览器不会因为async脚本而阻塞
- async脚本不能保证顺序,它是独立下载、独立运行、不会等待其他脚本
- async不会保证能在DOMContentLoaded之前或者之后执行
-
defer通常用于需要在文档解析之后操作DOM的JS代码,并且对多个script文件有顺序要求
-
async通常用于独立的脚本,对其他脚本、甚至DOM没有依赖的
JavaScript的运行原理
JavaScript代码的执行
- JavaScript代码下载好后,是如何一步步被执行的?
- 浏览器内核由两部分组成,以webkit为例:
- WebCore:负责HTML解析、布局、渲染等等相关的工作
- JavaScriptCore:解析、执行JavaScript代码
- 另外一个强大的JavaScript引擎就是V8引擎
V8引擎的执行原理
- V8引擎
- 是用C++编写的Goole开源高性能JavaScript和WebAssembly引擎,用于Chrome和Node.js等
- V8可以独立运行,也可以嵌入到任何C++应用程序中
V8引擎解析执行JS代码
V8引擎就是将高级语言代码最终转成二进制代码交给CPU执行
- JS源代码解析成AST抽象语法树(知道代码结构)
- Ignition成字节码(跨平台的,可以运行在window或者Mac上)
- TurboFan来收集信息,生成优化的机器码,之后重复调用时,直接运行优化的机器码就可以
function sum(num1,num2) {
return num1 + num2
}
// 多次调用时,每次都需要将字节码翻译成机器指令,效率很低
// 于是有了TurboFan来收集信息,生成优化的机器码,之后重复调用时,直接运行优化的机器码就可以
sum(10,20)
sum(20,30)
// 当类型出现变化,优化的机器码会反优化成字节码,重新由字节码开始运行,翻译成机器指令在运行
// 这样会导致性能降低,所以在开发中尽量保证类型一致
// 所以TS在一定程度上可以提高JS性能
sum("10","20")
V8引擎的架构
- Parse模块会将JS代码转换为AST(抽象语法树)
- 这是因为解释器并不直接认识JS代码
- 如果函数没有被调用,那么是不会被转换为AST的
- Ignition是一个解释器,会将AST转换为ByteCode(字节码)
- 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算)
- 如果函数只调用一次,lgnition会解释执行字节码
- TueboFan是一个编译器,可以将字节码编译成CPU可以直接执行的机器码
- 如果一个函数被多次调用,会被标记为热点函数,就会被TurboFan转换成优化的机器码,提高代码的执行性能。
- 但是,机器码实际上也会被还原为ByteCode(字节码),这是因为如果在后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码
- 还有一个核心模块是垃圾回收器
function sum(num1,num2) {
return num1 + num2
}
// 多次调用时,每次都需要将字节码翻译成机器指令,效率很低
// 于是有了TurboFan来收集信息,生成优化的机器码,之后重复调用时,直接运行优化的机器码就可以
sum(10,20)
sum(20,30)
// 当类型出现变化,优化的机器码会反优化成字节码,重新由字节码开始运行,翻译成机器指令在运行
// 这样会导致性能降低,所以在开发中尽量保证类型一致
// 所以TS在一定程度上可以提高JS性能
sum("10","20")
V8引擎的解析图
先了解两个概念
- 词法分析(lexical analysis)
- 将字符序列转换成token序列的过程
- token是记号化(tokenization)的缩写
- 词法分析器,也叫扫描器(scanner)
- 语法分析(parsing)
- 语法分析器也可以称之为parser
- blink解析html文件,遇到script,拿到js原码,交给v8引擎-Stream
- scanner词法分析,即分词记号化
var name = "why"
- 将var、name、=、"why"标记成token,以区分,确定var是第一个词,并在之后的语法分析中确定是在声明变量
- 然后在AST中声明这一行语句是声明变量的,如果前面是一个function说明在定义函数
- 所以js中关键字很重要,js在进行词法分析之后,拿到关键字,进行第二步语法分析
- parsing语法分析:语法分析中,确定ver在声明变量,确定name是一个变量标识符
- 生成AST
- 生成字节码
JavaScript的执行过程
- 假如有下面一段代码,它在JavaScript中是如何被执行的?
var name = "why"
function foo() {
var name = 'foo'
console.log(name);
}
var num1 = 20
var num2 = 30
var result = num1 + num2
console.log(result)
foo()
一、初始化全局对象
- js引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)
* 该对象在浏览器中就是window
* 该对象 所有的作用域(scope) 都可以访问
* 里面会包含Date、Array、String、Number、setTimeout、setInterval等等
* 其中还有一个window属性指向自己
二、全局代码的执行过程
- js引擎内部有一个执行上下文栈(简称ECS),它是用于执行代码的调用栈
- 那么现在它要执行谁?执行的就是全局的代码块
- 全局的代码块为了执行会构建一个全局执行上下文Global Execution Context(GEC)
- GEC会被放入到ECS中执行
- 全局执行上下文放到执行上下文栈中包含两部分内容:
- 第一部分:在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值
- 这个过程也称之为变量的作用域提升
- 基础数据类型的变量值保存在栈内存中,而变量对象则包含有关这些变量和函数声明的信息。
- 第二部分:在代码执行中,对变量赋值,或者执行其他的函数
- 第一部分:在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值
认识VO对象:
- 每一个执行上下文会关联一个VO(Variable Object,变量对象),变量和函数声明会被添加到这个VO对象中(其中函数会被优先添加,如果有同名的变量赋值如var foo = 1,会覆盖之前的指向函数对象的指针)
- 对于全局上下文来说,这个VO就是GO
三、函数代码的执行流程
- 在执行过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(简称FEC),并且压入到EC Stack中
- 每一个执行上下文都会关联一个VO,那么函数执行上下文关联的VO是什么呢?
- 当进入一个函数执行上下文时,会创建一个AO对象(Activation Object)
- 这个AO对象会使用arguments作为初始化,并且初始值是传入的参数
- 这个AO对象会作为执行上下文的VO来存放变量的初始化
四、函数多次调用的情况
- foo函数第一次执行
- foo第一次执行完,会弹出第一次的函数执行上下文(关联的VO对象不一定被销毁),全局执行上下文继续执行,于是第二次调用foo函数
- foo函数第二次执行,新建执行上下文,关联一个新的VO对象
- …
函数代码相互调用
var message = "Global Message"
var obj = {
name: "why"
}
function bar() {
console.log("bar function");
var address = "bar"
}
function foo(num) {
var message = 'Foo Message'
bar()
var age = 18
var height = 1.88
console.log("foo function")
}
foo(123)
执行过程:
- 创建全局执行上下文和全局对象,在全局对象中声明变量message、obj等值为undefined和声明函数bar(关联一个函数对象)、foo(关联一个函数对象)(函数会优先声明,因为js希望你可以在函数声明之前调用函数)
- 执行全局代码,
var message = "Global Message"
,会在栈中执行并将值保存在栈中,但是在VO上也会添加一个属性message= “Global Message”;obj是引用类型,会在堆中创建一个obj对象,并将地址赋值给obj; foo(123)
会创建一个新的执行上下文,关联一个新的AO对象,AO对象中一样先将变量message、age、height赋值为undefined- 调用bar(),又会创建一个新的执行上下文入栈,关联一个新的AO对象,执行完bar中的代码,该执行上下文出栈,关联的AO对象销毁
- 继续执行foo的执行上下文,age赋值为18、height赋值为1.88,打印foo function。foo执行上下文出栈,关联的AO对象销毁
- 继续执行全局执行上下文栈
JS执行中变量查找的作用域链
- 先在自己的VO对象中查找,如果有对应的变量则使用该变量
- 如果自己的VO对象中没有该变量,沿着作用域链上查找
作用域链:- 当进入一个执行上下文的时,执行上下文也会关联一个作用域链
- 作用域链是一个对象列表,用于变量标识符的求值
- 当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象
- 全局执行上下文的作用域链只有GO
- 函数在声明创建的时候就确定作用域链了
- 当进入一个执行上下文的时,执行上下文也会关联一个作用域链
var message = "Global Message"
function foo() {
console.log(message);
}
var obj = {
bar: function () {
var message = "bar message"
//当这里foo被调用时,打印出来的message的值是Global Message
// 因为函数的作用域链在创建时就确定了
foo()
}
}
obj.bar()
函数代码多层嵌套作用域链
var message = "hello world"
debugger
function foo() {
var message = "hello foo"
function bar() {
var message = "hello bar"
console.log(message);
}
return bar
}
var bar = foo()
bar()
作用域面试题
// 1.面试题一:
var n = 100
function foo() {
// 这里访问的n变量就是全局的n
n = 200
}
foo()
console.log(n);//200
// 面试题二:
var n = 100
function foo(){
console.log(n);//undefined
var n = 200 //作用域提升
console.log(n)//200
}
foo()
// 3.面试题三:
var n = 100
function foo1() {
console.log(n);
}
function foo2() {
var n = 200
console.log(n)//200
foo1()//100
}
foo2()
// 面试题四:
var a = 100
function foo() {
console.log(a); //undefined
return //在代码执行时才return
var a = 100 //在代码解析时就创建了a
}
foo()
// 面试题五:
function foo() {
var a = b = 100
}
foo()
console.log(b)//100 因为不使用var创建变量时,会默认放到全局window中
console.log(a);//a is not defined
JS内存管理和闭包
认识内存管理
- 不管是什么样的编程语言,在代码执行的过程中我们都是需要给它分配内存的
- 有些我们需要手动管理内存,有些自动帮我们管理内存
- 内存的管理都会有如下的生命周期
- 分配申请你需要的内存(申请)
- 使用分配的内存(存放一些东西,比如对象等)
- 不需要使用时,对其进行释放
- 对于开发者来说,JavaScript的内存管理是自动的、无形的
- 我们创建的原始值、对象、函数…这一切都会占用内存
- 我们不需要手动对它们进行管理,JS引擎会帮助我们处理好它
JS的内存管理
- JS在定义数据时为我们分配内存
- 但是内存分配方式是一样的吗?
- JS对于原始数据类型的内存分配会在执行时,直接在栈空间进行分配
- JS对于复杂数据类型内存的分配会在堆内存中开辟一块空间,并且将这块空间的指针返回值变量引用
JS的垃圾回收
- 当内存不再需要的时候,我们需要对其进行释放,以便腾出更多的内存空间
- 大部分现代的编程原因都有自己的垃圾回收机制:
- 垃圾回收(GC)
- 对于那些不再使用的对象,我们称之为垃圾,需要被回收,以释放更多的内存空间
- GC怎么知道哪些对象是不再使用的?
- GC的实现以及对应的算法
常见的GC算法-引用计数
- 引用计数:
- 当一个对象有一个引用指向它时,那么这个对象的引用就+1
- 当一个对象的引用为0时,这个对象就可以被销毁掉
- 弊端是会产生循环引用
常见的GC算法-标记清除
- 标记清除:
- 核心思路是可达性
- 设置一个根对象,垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于那些没有引用到的对象,就认为是不可用的对象
- 这个算法可以很好的解决循环引用的问题
- 在JS中这个根对象就是window对象
常见的GC算法-其他算法优化补充
- JS引擎比较广泛的采用的就是可达性中的标记清除算法,V8引擎为了进行更好的优化,也结合了一些其他的算法
- 标记整理 和“标记-清除” 相似
- 不同的是,回收期间同时会将保留的存储对象搬运汇集到连续的内存空间,从而整合内存空闲,避免内存碎片化
- 分代收集 – 对象被分成两组:"新的"和“旧的”
- 许多对象出现,完成它们的工作并很快死去,它们可以很快被清理
- 那些长期存活的对象会变得**“老旧”,而且被检查的频次也会减少**
- 增量收集
- 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟
- 所以引擎试图将垃圾收集工作分为几部分来做,然后将这几部分逐一处理
- 闲时收集
- 垃圾收集器只会在CPU空闲时尝试运行,以减少对代码执行的影响
JS闭包
闭包的定义
-
闭包就是嵌套函数,使用了外层作用域中的变量
-
如果嵌套函数被返回了出去被调用,一直存在,那么可能造成内存泄漏
-
本质上就是上级作用域内变量的生命周期,因为被下级作用域内引用,没有被释放
// 写一个闭包函数
function foo(){
const a = 2;
function bar () {
console.log(a)
}
return bar
}
let baz = foo()
baz()
闭包的使用过程和内存图
function createAdder(count) {
function adder(num) {
return count + num
}
return adder
}
var adder5 = createAdder(5)
adder5(100)//105
var adder8 = createAdder(8)
adder8(30)//38
闭包内存泄漏和释放内存
- 假设之后不会再使用adder8
- 内存泄漏:对于那些我们永远不会再使用的对象,但是对于GC来说,它不知道要进行释放的对应内存依然保留
- 手动释放内存
adder8 = null
浏览器的优化操作
function foo() {
var name = 'foo'
var age = 18
var height = 1.88
function bar() {
// 浏览器做的优化
// 因为这里没有用到age
// 所以浏览器会将age从内存中删除,以释放内存
console.log(name, height);
}
}
var fn = foo()
fn()
JS函数的增强知识
函数对象的属性
- JS中函数也是一个对象,那么对象中就可以有属性和方法
- 属性name:访问函数的名词
- 属性length:用于返回函数形参的个数
- 剩余参数是不参与参数个数的
// 定义函数
function foo(a, b, c) {
}
var bar = function() {
}
// 自定义属性
foo.message = "hello foo"
console.log(foo.message);
// 默认函数对象中已经有自己的属性
// 1.name属性
console.log(foo.name)
// 2.length属性:参数(形参)个数
// 不算剩余参数
console.log(foo.length)
函数arguments的使用
- arguments 是一个对应于 传递给函数的参数 的 类数组(array-like)对象
// 定义函数
function foo(m, n) {
// 类数组对象
console.log(arguments[3]);
for (var arg of arguments) {
console.log(arg)
}
}
var bar = function () {
}
foo(10, 20, 30, 40)
- array-like意味这它不是一个数组类型,而是一个对象类型
- 但是它拥有数组的一些特性,比如说length,index索引
- 但是没有数组的一些方法,比如filter、map等
arguments转Array
-
在开发中,我们经常需要将arguments转成Array,以便使用数组的一些特性
- 常见传化方式如下
// 定义函数 function foo(m, n) { // 类数组对象 console.log(arguments[3]); // 将arguments转成数组方式一: var newArguments = [] for (var arg of arguments) { newArguments.push(arg) } console.log(newArguments) // 将arguments转成数组方式二:ES6方法 // 传入可迭代对象 var newArgs1 = Array.from(arguments) console.log(newArgs1) // 使用扩展运算符 var newArgs2 = [...arguments] console.log(newArgs2) // 将arguments转成数组方式三:slice var newArgs = [].slice.apply(arguments) var newArgs3 = Array.prototype.slice.apply(arguments) console.log(newArgs) }
箭函不绑定arguments
- 箭头函数是不绑定arguments的,所以箭头函数中使用arguments会去上层作用域查找
函数的剩余参数
- ES6引进了剩余参数,可以将不定量的参数放入到一个数组中
// 剩余参数:rest parameters
// 剩余参数需要写到参数列表最后
function foo(num1, num2, ...args) {
console.log(args);
}
foo(20, 30, 111, 222, 343)
- 剩余参数和arguments的区别?
- 剩余参数只包含那些没有对应形参的实参,而arguments对象包含了传给函数的所有实参
- rest参数是一个真正的数组,而arguments不是
- arguments是早期ES中为了方便去获取所有的参数提供的一个数据结构,而rest参数是ES6来替代arguments的
- 剩余参数必须放到最后一个位置,否则会报错
纯函数的理解和应用场景
理解JS纯函数
- 函数式编程有一个非常重要的概念叫纯函数
- 在react开发中纯函数是被多次提及的
- 比如react组件被要求是一个纯函数,redux中的reducer也被要求是一个纯函数
- 纯函数的维基百科定义:
- 若一个函数符合以下条件,那么称之为纯函数
- 在相同的输入值时,需产生相同的输出
- 函数的输出与输入值以外的其他隐藏信息或状态无关
- 不能有语义上可观察的函数副作用,诸如“触发事件”等
- 若一个函数符合以下条件,那么称之为纯函数
- 总结:
- 确定的输入,一定会产生确定的输出
- 函数在执行过程中,不能产生副作用
- 什么是副作用?
- 表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储
- 纯函数的作用和优势
- 不需要关心外层作用域的值,目前是什么状态
- 调用函数时,确定的输入一定产生确定的输出
柯里化和柯里化函数
- 维基百科对柯里化的解释
- 是把接收多个参数的函数,变成接受一个单一参数的函数,并且返回接受余下的参数,而且返回结果的新函数的技术
- 总结:
- 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数
- 这个过程就称之为柯里化
- 柯里化是一种函数的转化,将一个函数从可调用的f(a, b, c)转化为可调用的f(a)(b)©
- 柯里化不会调用函数,它只是对函数进行转换
// 普通的函数
function foo(x, y, z) {
console.log(x + y + z);
}
foo(10, 20, 30)
//柯里化函数
function foo1(x) {
return function(y) {
return function(z) {
console.log(x, y, z)
}
}
}
foo1(10)(20)(30)
- 箭头函数的写法
// 箭头函数的写法
var foo3 = x => y => z => console.log(x, y, z)
foo3(10)(20)(30)
自动柯里化函数的封装
// 普通的函数
function foo(x, y, z) {
console.log(x + y + z);
}
function sum(num1, num2) {
return num1 + num2
}
function logInfo(data, type, message) {
console.log(`时间:${data} 类型:${type} 内容:${message}`)
}
// 自动转化柯里化函数
function myCurrying(fn) {
function curryFn(...args) {
// 两类操作
// 第一类操作:参数不够,操作返回一个新的函数,继续接受参数
// 第二类操作:参数够,直接执行fn的函数
if (args.length >= fn.length) {
// 执行第二类
return fn(...args)
// 考虑到this时的写法
// return fn.apply(this, args)
} else {
// 执行第一类
// 继续接受参数
return function (...newArgs) {
// 重新进行判断
return curryFn(...args.concat(newArgs))
//return curryFn.apply(this, args.concat(newArgs))
}
}
}
return curryFn
}
// 对其他函数进行柯里化
var fooCurry = myCurrying(foo)
fooCurry(10)(20)(30)
fooCurry(10, 20, 30)
var sumCurry = myCurrying(sum)
console.log(sumCurry(10)(20))
var sum5 = sumCurry(5)
console.log(sum5(10))
console.log(sum5(15))
console.log(sum5(18))
var logInfoCurry = myCurrying(logInfo)
logInfoCurry("2022", "9", '29')
logInfoCurry("2022")("9")("29")
组合函数的概念
- 基本使用
var num = 100
// 第一步对数字*2
function double(num) {
return num * 2
}
// 第二步对数字**2
function pow(num) {
return num ** 2
}
console.log(pow(double(num)))
// 将上面两个函数组合在一起,生成一个新的函数
function composeFn(num) {
return pow(double(num))
}
console.log(composeFn(num))
- 组合函数的工具封装
var num = 100
// 第一步对数字*2
function double(num) {
return num * 2
}
// 第二步对数字**2
function pow(num) {
return num ** 2
}
console.log(pow(double(num)))
// 封装的函数:传入多个函数,自动将多个函数组合,挨个调用
function composeFn(...fns) {
// 1.边界判断
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(`fn ${i} must be function`)
}
}
// 2.返回的新函数
return function(...args) {
// 传递this和参数数组
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))
with语句的使用
- width语句:扩展一个语句的作用域链
var obj = {
message: "hello world"
}
with (obj) {
console.log(message);//hello world
}
- 不建议使用
eval函数的使用
- 内建函数eval允许执行一个代码字符串
- eval是一个特殊的函数,它可以将传入的字符串当作JS代码来运行
- eval会将最后一句执行语句的结果,作为返回值
var codeString = `var message = 'hello world'; console.log(message);`
eval(codeString)//hello world
- 不建议使用
- 可读性差
- 作为字符串,可能在执行过程中被恶意纂改,被攻击
- eval的执行必须经过JS解释器,不能被JS引擎优化
严格模式
- 再ES5中,JS提出了严格模式的概念
- 严格模式是一种具有限制性的JS模式,从而使代码隐式的脱离了“懒散模式”
- 支持严格模式的浏览器在检测到代码中有严格模式时,会以更加严格的方式对代码进行检测和执行
- 严格模式对正常的JS语义进行了一些限制
- 严格模式通过抛出错误来消除一些原有的静默错误
- 严格模式让JS引擎在执行代码时可以进行更多的优化(不需要对一些特殊的语法进行处理)
- 严格模式禁用了在ES未来版本中可能会定义的一些语法
开启严格模式
- 严格模式支持粒度话的迁移:
- 可以支持在js文件中开启严格模式
- 也支持对某一个函数开启严格模式
- 严格模式通过在文件或者函数开头使用 use strict 来开启
<script>
// 给整个script开启严格模式
"use strict"
// 给一个函数开启严格模式
function foo() {
"use strict"
}
</script>
- 现代JS支持class和moudle,默认开启严格模式
严格模式限制
- 无法意外的创建全局变量
- 严格模式会引起静默失败的赋值操作抛出异常
- 严格模式下试图删除不可删除的属性
- 严格模式不允许函数参数有相同的名称
- 不允许0的八进制语法
- 在严格模式下,不允许使用with
- 在严格模式下,eval不再为上层引用变量
- 严格模式下,this绑定不会转成对象类型
"use strict"
// 1.无法意外的创建全局变量
// function foo() {
// message = "hello world"
// }
// foo()
// console.log(message);
// 2.严格模式会引起静默失败(不报错也没有任何效果)的赋值操作抛出异常
var obj = {
name: "why"
}
Object.defineProperty(obj, "name", {
writable:false
})
obj.name = "kobe"
console.log(obj.name)
// 3. 参数名称不能相同
function foo(num, num) {
}
// 4. 不能以0开头
console.log(0o123)
// 5.eval函数不能为上层创建变量
eval(`var message = "hello world"`)
console.log(message)
// 6.严格模式下,this绑定不会转成对象类型
function foo() {
console.log(this)
}
foo.apply("abc")
foo.apply(123)
// 函数的独立调用在默认模式下this绑定widnow,严格模式下this绑定undefined
对象的增强知识
对属性操作的控制
- 如果我们想要对一个属性进行比较精准的操作控制,那么我们就可以使用属性描述符
- 通过属性描述符可以精准的添加或修改对象的属性
- 属性描述符需要使用Object.defineProperty来对属性进行添加或者修改
- Object.defineProperty
- 该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
- 参数:
- 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",
age: 18
}
Object.defineProperty(obj, "name", {
configurable: false ,//告诉js引擎,obj对象的name属性不可以被删除
enumerable:false,//不可枚举 (for-in/Object.keys)
writable: false,//不可以写入(只读属性)
value: "hello world"//返回这个value
})
delete obj.name
console.log(obj.name);//hello world
// 通过Object.defineProperty添加一个新的属性
Object.defineProperty(obj, "address", {})
delete obj.address
console.log(obj.address)//undefined, 没有被删掉
console.log(Object.keys(obj))//age
存取属性描述符
- configurable:与数据属性描述符一致
- enumerable:与数据属性描述符一致
- get:获取属性时会执行的函数,默认为undefined
- set:设置属性时会执行的函数,默认为undefined
var obj = {
name: "why",
}
// 对obj对象中的name添加描述符(存取属性描述符)
var _name = ""
Object.defineProperty(obj, "name", {
configurable: true,
enumerable: true,
set: function(value) {
console.log('hello world', value);
_name = value
},
get: function() {
console.log('get方法')
return _name
}
})
obj.name = "kobe"
// 获取值
console.log(obj.name)
多个属性描述符
- Object.defineProperties()方法直接在一个对象上定义多个属性或者修改现有的属性,并且返回该对象
var obj = {
name: 'why',
age: 18,
height:1.88
}
// Object.defineProperty(obj, "name", {})
// Object.defineProperty(obj, "name", {})
// Object.defineProperty(obj, "name", {})
// 新增的方法
Object.defineProperties(obj, {
name:{
configurable:true,
enumerable:false,
},
age: {
},
height: {
}
})
对象额外方法的补充
var obj = {
name: 'why',
age: 18,
height: 1.88
}
// 获取对象的属性描述符
console.log(Object.getOwnPropertyDescriptor(obj, "name"));
console.log(Object.getOwnPropertyDescriptors(obj));
// 禁止对象扩展新属性:prevenExtensions
// 给一个对象添加新的属性会失败(在严格模式下会报错)
Object.preventExtensions(obj)
obj.address = "china"
console.log(obj.address)//undefined
// 密封对象,不允许配置和删除属性:seal
// 实际是调用preventExtendsions
// 并且将现有属性对的configurable:false
Object.seal(obj)
delete obj.name
console.log(obj.name)//why
// 冻结对象,不允许修改现有属性:freeze
// 实际上是调用seal
// 并且将现有属性的writable:false
Object.freeze(obj)
obj.name = "kobe"
console.log(obj.name)//why
ES5实现继承
认识对象的原型
- JS中每个对象都有一个特殊的内置属性[[prototype]],这个特殊的对象可以指向另外一个对象
- 那么这个对象有什么用呢?
- 当我们通过引用对象的属性key来获取一个value时,它会触发[Get]的操作
- 这个操作会首先检查该对象是否有对应的属性,如果有的话就使用它
- 如果对象中没有属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性
- 只要是对象都会有这个内置属性
- 获取方式:
var obj = {
name: "why",
age: 18
}
console.log(obj);
// 获取对象的原型
console.log(obj.__proto__)
console.log(Object.getPrototypeOf(obj))
console.log(Object.getPrototypeOf(obj) === obj.__proto__) //true
// 这个原型有什么用?
// 当我们通过[[get]]方式获取一个属性对应的value时
// 它会优先在自己的对象中查找,如果找到直接返回
// 如果没有找到,那么会在原型对象中查找
console.log(obj.name)
obj.__proto__.message = "hello world"
console.log(obj.message)//hello world
函数对象的原型
- 将函数看作是一个函数时,它是具备prototype(显式原型)
- 作用:用来构建对象时,赋值给对象的隐式原型
var obj = {}
function foo() {}
// 1.将函数看成是一个普通的对象时,它时具备__proto__(隐式原型)
// 为什么叫隐式原型,因为我们一般不会直接去获取它
// 作用:查找key对应的value时,会找到原型身上
console.log(obj.__proto__);
console.log(foo.__proto__);
// 2.将函数看作是一个函数时,它是具备prototype(显式原型)
// 作用,用来构建对象时,赋值给对象的隐式原型
// 对象没有prototype
console.log(foo.prototype)
console.log(obj.prototype)//undefined
- 回忆一下new操作符:
- 在内存中创建一个新的对象(空对象)
- 这个对象内部的[[prototype]]属性即隐式原型会被赋值为该构造函数的prototype原型即显式原型
- 那就意味这我们通过Person构造函数创建出来的所有对象的[[prototype]]属性都指向Person.prototype
function Foo() {
// 1.创建空的对象
// 将Foo的显式原型赋值给空对象的隐式原型
}
console.log(Foo.prototype);
var f1 = new Foo()
var f2 = new Foo()
console.log(f1.__proto__)
console.log(f2.__proto__)
console.log(f1.__proto__ === Foo.prototype)//true
- 将方法放在原型上
function Student(name, age, sno) {
this.name = name
this.age = age
this.sno = sno
// 方式一:导致每次创建一个新对象时,都会创建这三个方法,这是没必要的
// this.running = function() {
// console.log(this.name + "running");
// }
// this.eating = function() {
// console.log(this.name + "eating");
// }
// this.studying = function() {
// console.log(this.name + "studying");
// }
}
// 方式二:
// 当我们多个对象拥有共同的值,我们可以将它放到构造函数的显式原型上
// 由构造函数创建出来的所有对象,都会分享这些属性
Student.prototype.running = function () {
console.log(this.name + "running");
}
// 1.创建三个学生
var stu1 = new Student('why1', 18, 111)
var stu2 = new Student('why2', 28, 112)
var stu3 = new Student('why3', 38, 113)
stu1.running()
constructor属性
- 显式原型对象上有一个constructor属性,会指向当前的函数对象
// 函数对象中显式原型中重要的属性:constructor,指向Person函数对象
function Person() {
}
var PersonPrototype = Person.prototype
console.log(PersonPrototype);
console.log(PersonPrototype.constructor);
console.log(PersonPrototype.constructor === Person);//true
console.log(Person.name)
console.log(PersonPrototype.constructor.name)
// 2.实例对象
var p = new Person()
console.log(p.__proto__.constructor)
console.log(p.__proto__.constructor.name)
构造函数创建对象的内存表现
重写原型对象
function Person() {
}
console.log(Person.prototype);
// 在原有的原型对象上添加新的属性
Person.prototype.message = "hello world"
Person.prototype.info = { name: 'hhh', age: 30 }
Person.prototype.running = function() {}
Person.prototype.eating = function() {}
// 直接赋值一个新的原型对象
// 弊端:没有指向Person的constructor属性
Person.prototype = {
message: "hello person",
info: { name: "hhh", age: 30},
running: function() {},
eating: function() {},
// 自定义指向Person的constructor属性
// constructor: Person
}
Object.defineProperty(Person.prototype, "construcotor", {
configurable: true,
writable: true,
enumerable:false,
value: Person
})
console.log(Object.keys(Person.prototype))
var p1 = new Person()
console.log(p1.message)//hello person
面向对象的特性-继承
- 面向对象有三大特性: 封装、 继承、 多态
- 封装: 我们前面将属性和方法封装到一个对象或者类中,可以称之为封装的过程
- 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提
- 多态:不同的对象在执行时表现出不同的形态
- 继承
- 继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要继承过来使用即可
- 在很多编程语言中,继承也是多态的前提
默认原型链和自定义原型链
- 原型链:
- 我们从一个对象上获取属性,如果在当前对象中没有获取到就会沿着原型链去它的原型上面获取
// 1.{}的本质
//var info = {}
// 相当于 var info = new Object()
//console.log(info.__proto__);
//console.log(info.__proto__ === Object.prototype);//true
//2. 原型链
var obj = {
name: "why",
age: 18
}
// 查找顺序
// 1.obj上面找到
// 2.obj.__proto__上找到
// 3.obj.__proto__.___proto__ => null 上查找
// console.log(obj.message)//undefined
//3.对现有的代码进行改造
obj.__proto__ = {
// message: "hello world"
}
obj.__proto__.__proto__ = {
message:'hello world'
}
console.log(obj.message)
通过原型链实现继承
Stu类实现继承Person类的属性和方法
-
方式一:父类的原型直接赋值给子类的原型(错误做法)
- 缺点:父类和子类共享一个原型对象,修改了任意一个,另外一个也被修改
-
方式二:创建一个父类的实例对象(new Person()),用这个实例对象作为子类的原型
-
原型链继承的弊端:某些属性(Person类中的name和age属性)其实是保存在p对象上的
- 第一:我们通过**直接打印对象是看不到这个属性(Person类中的name和age属性)**的
- 第二:这个属性被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题
- 第三:不能给Person传递参数(让每个stu有自己的属性),因为这个对象是是一次性创建的(没办法定制化)
// 定义Person构造函数(类)
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.running = function() {
console.log("running");
}
Person.prototype.eating = function() {
console.log('eating')
}
// 定义学生类
function Student(sno, score){
//this.name = name
// this.age = age
this.sno = sno
this.score = score
}
// 实现继承方式一:父类的原型直接赋值给子类的原型(错误做法)
// 缺点:父类和子类共享一个原型对象,修改了任意一个,另外一个也被修改
// Student.prototype = Person.prototype
// 方式二:创建一个父类的实例对象(new Person()),用这个实例对象作为子类的原型
var p = new Person("why", 18)
Student.prototype = p
// Student.prototype.running = function() {
// console.log("running")
// }
// Student.prototype.eating = function() {
// console.log('eating')
// }
Student.prototype.studying = function() {
console.log("studying")
}
// 创建学生
var stu1 = new Student("kobe", 18, 111, 110)
stu1.running()
stu1.studying()
console.log(stu1.name, stu1.age)
借用构造函数继承
- 为了解决原型链继承中属性继承存在的问题,开发人员提供了:借用构造函数继承
- 借用继承的做法:在子类型构造函数的内部调用父类型构造函数
- 可以通过apply()和call()方法在新创建的对象上执行构造函数,从而实现定制化父类上的一些属性
function Student(name, age, sno, score){
// 重点:借用构造函数
Person.call(this, name, age)
this.sno = sno
this.score = score
}
组合借用继承的问题
- 组合继承(原型链继承和构造函数继承的结合)是JavaScript最常用的继承模式之一
- 无论什么情况下,都会调用两次父类构造函数
- 一次在new p对象(创建子类原型)的时候
- 另一次在子类构造函数内部(借用父类构造函数)的时候
- 所有的子类实例会拥有两份父类的属性
- 一份是实例的原型中
- 一份是实例自己本身的
寄生组合式继承
- 继承的目的:重复利用另外一个对象的属性和方法
- 寄生组合式继承
- 原型链
- 借用
- 原型式对象(新建一个对象)
- 寄生式函数
// 满足什么条件
// 1. 必须创建一个对象
// 2. 这个对象的隐式原型指向父类的显式原型
// 3. 将这个对象赋值给了类的显式原型
function Person() { }
function Student() { }
// 1.之前的做法
var p = new Person()
Student.prototype = p
// 2. 方案一
var obj = {}
// _proto_属性在有些浏览器可能不兼容
// obj._proto_ = Person.prototype
Object.setPrototypeOf(obj, Person.prototype)
Student.prototype = obj
// 3. 方案二:和之前的做法相比更具通用性,也更具兼容性
function F() { }
F.prototype = Person.prototype
Student.prototype = new F()
// 4. 方案三
// Object.create实现创建一个新对象,并且赋值一个原型对象
var obj3 = Object.create(Person.prototype)
Student.prototype = obj3
// 封装方案三:寄生组合式继承
// 寄生式函数,新建了一个对象,而不是直接使用父类的实例
function inherit(Subtype, Supertype) {
var obj = Object.create(Supertype.prototype)
Subtype.prototype = obj
//使用数据属性描述符添加construct属性
Object.defineProperties(Subtype.prototype, "contructor", {
enumerable: false,
configurable: true,
writable: true,
value: Subtype
})
}
// 如果担心Object.create有兼容性问题时,自定义createObject函数
function createObject(o) {
function F() { }
F.prototype = o
return new F()
}
//inde.html
<script src="./index.js"></script>
<script>
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) {
Person.call(this, name, age, height)
this.sno = sno
}
inherit(Student, Person)
Student.prototype.studying = function() {
console.log("studying")
}
var stu1 = new Student("why", 18, 1.88, 111)
console.log(stu1)
stu1.running()
stu1.eating()
stu1.studying()
</script>
Object是所有类的父类
function Person() {}
function Student() {}
function Teacher() {}
inherit(Student, Person)
console.log(Person.prototype.__proto__ === Object.prototype);
// 在Object的原型上添加属性
Object.prototype.message = "hello world"
var stu = new Student()
console.log(stu.message)//hello world
// Object原型上已存的一些方法
console.log(Object.prototype)
console.log(stu.toString())
对象的方法补充
- hasOwnProperty
- 对象是否有某一个属于自己的属性(不是原型上的属性)
- in/for in操作符
- 判断某个属性是否在某个对象或者对象的原型上
- instanceof:(实例对象和类之间的关系)
- 用于检测构造函数的prototype,是否出现在某个实例对象的原型链上
- isPrototypeOf:(对象之间的关系)
- 用于检测某个对象,是否出现在某个实例对象的原型链上
原型继承关系式图解
- p1是Person的实例对象
- obj是Object的实例对象
- Function/Object/Foo都是Function的实例对象
- 原型对象默认创建时,隐式原型都是指向Object的显式原型的(Object的显式原型的显式原型指向null)
- Object是Person/Function的父类
ES6实现继承
认识class定义类
- 按照前面的构造函数的形式来创建类,不仅仅与编写普通的函数过于相似,而且代码并不容易理解
- 在ES6中使用了class关键字来直接定义类
- 但是类本质上依然是之前所讲的构造函数、原型链的语法糖而已
// ES6定义类
// {} :对象/代码块/类的结构
class Person {
}
// 创建实例对象
var p1 = new Person()
var p2 = new Person()
console.log(p1, p2);
// 表达式定义方法
var Student = class {
}
var stu1 = new Student()
console.log(stu1)
class类中实例方法和构造方法
class Person {
// 1.类中的构造函数
// 当我们通过new关键字调用一个Person类时,默认调用class中的constructor方法
constructor(name, age) {
// 在constrotor中跟之前的写法一样
this.name = name
this.age = age
}
// 2.实例方法 (定义在类的显式原型上,只有实例对象可以使用)
running() {
console.log(this.name + "running")
}
}
var p1 = new Person("why", 18)
console.log(p1);
//Person类的显式原型也会赋值给实例对象p1的隐式原型
console.log(Person.prototype === p1.__proto__)//true
p1.running()
class定义访问器方法
- 对象中访问器的编写
// 针对对象
// 方式一:描述符
var obj = {
_name: "why"
}
Object.defineProperty(obj, "name", {
configurable: true,
enumerable: true,
set: function () {
},
get: function () {
}
})
// 方式二:直接在对象中定义访问器(不推荐)
var obj = {
_name: "why",
set name(value) {
console.log("setter方法调用")
this._name = value
},
get name() {
console.log("getter方法调用")
return this._name
}
}
obj.name = "join"
console.log(obj.name)
- 类的访问器方法的编写
class Person{
// 程序员约定俗成:以下划线开头的属性和方法,不在外界访问
constructor(name, age) {
this._name = name
this._age = age
}
set name(value) {
this._name = value
}
get name() {
return this._name
}
}
var p1 = new Person("why", 18)
p1.name = "kobe"
console.log(p1.name);
- 类的访问器的应用场景
// 2.访问器应用场景
class Rectangle {
constructor(x, y, width, height) {
this.x = x
this.y = y
this.width = width
this.height = height
}
get position() {
return { x: this.x, y: this.y}
}
}
var react1 = new Rectangle(10, 20, 100, 200)
console.log(react1.position)
类的静态方法(类方法)
- 静态方法通常用于定义直接使用类来执行的方法,不需要有类的实例,使用static关键字来定义
class Person {
// 实例方法
running() { }
// 类方法(静态方法):直接通过类来调用
static randomPerson() {
console.log('类方法被调用');
}
}
var p1 = new Person()
Person.randomPerson()
ES6通过extends实现继承
// 定义父类
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
running() {
console.log("running")
}
eating() {
console.log("eating")
}
}
class Student extends Person {
constructor(name, age, sno, score) {
// this.name = name
// this.age = age
super(name, age)
this.sno = sno
this.score = score
}
studying() {
console.log('studying')
}
}
var stu1 = new Student("why", 18, 111, 12)
console.log(stu1)
stu1.running()
class Teacher extends Person {
constructor(name, age, title) {
this.name = name
this.age = age
this.sno = title
}
teaching() {
console.log("teaching")
}
}
super关键字
-
super关键字:
- 执行super.method来调用父类方法
- 执行super()来调用一个父类constructor(只能在我们的construtor中)
-
super可以用在:子类的构造方法、实例方法、静态方法
继承内置类
- 我们也可以让我们的类继承自内置类,比如Array:
// 继承内置类的应用场景
class MyArray extends Array {
get lastItem() {
return this[this.length - 1]
}
get firstItem() {
return this[0]
}
}
var arr = new MyArray(10, 20, 30)
console.log(arr.lastItem);
console.log(arr.firstItem);
类的混入mixin
- JS中的继承只支持单继承,即只能有一个父类
- 开发中如果想实现多继承,可以使用混入
function mixinAnimal(BaseClass) {
return class extends BaseClass {
running() {
console.log("running");
}
}
}
function mixinRunner(BaseClass) {
return class extends BaseClass {
flying() {
console.log('flying')
}
}
}
class Bird {
eating() {
console.log("eating")
}
}
var NewBird = mixinRunner(mixinAnimal(Bird))
var bird = new NewBird()
bird.flying()
bird.running()
bird.eating()
babel工具
babel对类class的转化
//es6
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
running() { }
static randomPerson() { }
}
let p1 = new Person()
//es5
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
// 如果不是Person的实例则报错,不允许构造函数直接调用
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, "prototype", { writable: false });
return Constructor;
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i);
}
function _toPrimitive(t, r) {
if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) {
var i = e.call(t, r || "default");
if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value.");
} return ("string" === r ? String : Number)(t);
}
// 纯函数:相同的输入一定产生相同的输出,并且不会产生副作用
var Person = /*#__PURE__*/function () {
function Person(name, age) {
_classCallCheck(this, Person);
this.name = name;
this.age = age;
}
_createClass(Person, [{
key: "running",
value: function running() { }
}], [{
key: "randomPerson",
value: function randomPerson() { }
}]);
return Person;
}();
var p1 = new Person();
JS中的多态
- 面向对象的三大特性:封装、继承、多态
- 多态:
- 不同的数据类型进行同一操作,表现出不同的行为,就是多态的体现
##Java中面向对象的多态表现
- 在严格意义的面向对象语言中,多态是存在如下条件的
- 必须有继承(实现接口)
- 必须有父类引用指向子类对象
//继承之后操作统一的接口
class Shap {
getArea(){}
}
class Circle extends Shap {
constructor( radius ){
super()
this.radius = radius
}
getArea() {
return this.radius * this.radius
}
}
class Rectangle extends Shap{
constructor( width, height ) {
super()
this.width = width
this.height = height
}
getArea() {
return this.width * this.height
}
}
var react1 = new Rectangle(100, 200)
var c1 = new Circle(20, 30)
function getShapeArea(shape) {
console.log(shape.getArea())
}
getShapeArea(react1)
getShapeArea(c1)
JS面向对象的多态理解
// 多态的表现:JS到处都是多态
function sum(a1, a2) {
return a1 + a2
}
sum(20, 30)
sum("abc", "cba")
// 多态的表现形式二:
var foo = 123
foo = "hello world"
foo = {
}
foo = []
对象字面量的增强写法
- ES6中对字面量的增强主要包括以下几部分
- 属性的简写
- 方法的简写
- 计算属性名
/*
1.属性的增强
2.方法的增强
3.计算属性的写法
*/
var name = "why"
var age = 18
var key = "address"
var obj = {
// 1.属性的增强
name,
age,
// 2.方法的增强
running: function() {
console.log(this)
},
swimming() {
console.log(this)
},
// 3.计算属性名
[key]: "安吉"
}
obj.swimming()
ES6-解构
- ES6中新增了一个从数组或者对象中方便获取数据的方法,称之为解构
- 解构赋值是一种特殊的语法,它使我们可以将数组或对象"拆包"至一系列变量中
- 我们可以划分为数组的解构和对象的解构
var names = ["a", "b", "c", "d"]
var obj = { name: "why", age: 18, height: 1.88 }
// 1.数组的解构
// 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, ...newArray] = names
console.log(name1, name2, newArray)
// 1.4 解构的默认值
// 当name3没有赋值时,使用默认值w
var [name1, name2, name3 = "w"] = names
// 2.对象的解构
// 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: height2, name: name2, age: age2 } = obj
console.log(name2, height2, age2)
// 2.4 默认值
var { height, name, age, address = "china" } = obj
console.log(name, age, height, address)
// 2.5 对象的剩余内容
var { name, age, ...newObj} = obj
console.log(newObj)
- 解构的应用场景
- 比如开发中拿到一个变量时,自动对其进行解构使用
- 比如对函数的参数进行结构
// 应用:在函数中(其他类似的地方)
function getPosition( {x, y}) {
console.log(x,y)
}
getPosition({ x: 10, y: 20})
补充手写apply-call-bind
- 手写实现apply,利用隐式绑定,使this绑定我们传入的对象
function foo(name, age) {
console.log(this, name, age);
}
// 1.给函数对象添加方法:
Function.prototype.Myapply = function (thisArg, otherArgs) {
// this -> 调用的函数对象
// thisArg -> 传入的第一个参数,要绑定的this
// 1.获取thisArg,并且确保是一个对象类型
thisArg = (thisArg === null || thisArg === undefined) ? window : Object(thisArg)
// thisArg.fn = this
Object.defineProperty(thisArg, "fn", {
configurable: true,
value: this
})
// 检查 otherArgs 是否存在,如果不存在,设置为一个空数组
// if (!otherArgs) {
// otherArgs = [];
// }
thisArg.fn(...otherArgs || [])
delete thisArg.fn
}
foo.Myapply({ name: "why" }, ["james", 25])
foo.Myapply(123)
foo.Myapply(null)
- 手写实现Call方法
Function.prototype.Mycall = function (thisArg, ...otherArgs) {
// 1.获取thisArg,并且确保是一个对象类型
thisArg = (thisArg === null || thisArg === undefined) ? window : Object(thisArg)
// thisArg.fn = this
Object.defineProperty(thisArg, "fn", {
configurable: true,
value: this
})
thisArg.fn(...otherArgs)
delete thisArg.fn
}
foo.Mycall({ name: "why" }, "james", 25)
foo.Mycall(123)
foo.Mycall(null)
- 手写apply-call-bind封装
function foo(name, age) {
console.log(this, name, age);
}
// 1.封装思想
// 1.1 封装到独立的函数中
function execFn(thisArg, otherArgs, fn) {
thisArg = (thisArg === null || thisArg === undefined) ? window : Object(thisArg)
Object.defineProperty(thisArg, "fn", {
configurable: true,
value: fn
})
thisArg.fn(...otherArgs || [])
delete thisArg.fn
}
// 1.2 封装到原型中
Function.prototype.Myexec = function (thisArg, otherArgs) {
thisArg = (thisArg === null || thisArg === undefined) ? window : Object(thisArg)
Object.defineProperty(thisArg, "fn", {
configurable: true,
value: this
})
thisArg.fn(...otherArgs || [])
delete thisArg.fn
}
// 1.给函数对象添加方法:
Function.prototype.Myapply = function (thisArg, otherArgs) {
// execFn(thisArg, otherArgs, this)
this.Myexec(thisArg, otherArgs)
}
foo.Myapply({ name: "why" }, ["james", 25])
foo.Myapply(123)
foo.Myapply(null)
Function.prototype.Mycall = function (thisArg, ...otherArgs) {
// execFn(thisArg, otherArgs, this)
this.Myexec(thisArg, otherArgs)
}
foo.Mycall({ name: "why" }, "james", 25)
foo.Mycall(123)
foo.Mycall(null)
ES6~ES13新特性(一)
let/const基本使用
- let关键字:
- 声明变量
- const关键字:
- 保存的数据一旦被赋值,就不能被修改
- 如果赋值的引用类型,则可以通过引用修改对象的内容
- let、const不允许重复声明变量
let/const没有作用域提升
- let/const和var的区别:
- var声明的变量会进行作用域提升
- 使用let声明的变量,在声明之前访问会报错
console.log(message);//可以访问,但是值是undefined
// 1.var声明的变量会进行作用域的提升
var message = "hello world"
// 2.let/const声明的变量
console.log(address)//会报错,不能访问
let address = "china"
- 作用域提升:在声明变量的作用域中,如果这个变量可以在声明之前被访问,就称之为作用域提升
- 总结:let/const没有进行作用域提升,但是会在解析阶段被创建出来
let/const的暂时性死区
- 在let、const定义的标识符真正执行到声明的代码以前,是不能被访问的
- 从块作用域的顶部一直到变量声明之前,这个变量处于暂时性死区
- 暂时性死区和定义的位置没有关系,和代码执行顺序有关系
- 暂时性死区形成之后,在该区域中的这个标识符不能被访问
// 暂时性死区
function foo() {
// 暂时性死区begin
console.log(bar)
// 暂时性死区end
let bar = "bar"
let baz = "baz"
}
foo()
// 暂时性死区和定义的位置没有关系,和代码执行顺序有关系
function foo() {
console.log(message);
}
let message = "hello world"
foo()
console.log(message)
// 3.暂时性死区形成之后,在该区域中的这个标识符不能被访问
let message = "hello world"
function foo() {
console.log(message);//会报错,不会访问外部的message
let message = "hello let"
}
foo()
let/const不添加window
- var定义的变量是会默认添加到window上的
- let/const定义的变量不会添加到window上
// 1.var定义的变量是会默认添加到window上的
var message = "hello world"
var address = "china"
console.log(window.message);
console.log(window.address)
// 2.let/const定义的变量不会添加到window上
let message = "hello world"
let address = "china"
console.log(window.message);
console.log(window.address)
let/const的块级作用域
- 在ES5以及之前,只有全局作用域和函数作用域
- 在ES6中新增了块级作用域,并且通过let、const、function、class声明的标识符是具有块级作用域的限制的
- 但是我们发现函数拥有块级作用域,但是外面依然是可以访问的
- 这是因为引擎会对函数的声明进行特殊的处理,允许像var一样在块级作用域外进行访问
- 但是我们发现函数拥有块级作用域,但是外面依然是可以访问的
块级作用域的应用
- 通过词法环境保存了每次的变量i
ES6~ES13新特性(二)
模版字符串基本使用
- ES6之前,将字符串和一些动态变量结合在一起的方式非常丑陋
模版字符串基本使用
const name = "why"
const age = 18
// 1.ES6之前
// const info = "my name is" + name + ", age is" + age
// 2.ES6之后
const info = `my name is ${name}, age is ${age}`
console.log(info);
标签模版字符串的使用
- 在React的styled-components库中使用
function foo(...args) {
console.log("参数", args);
}
foo("why", 18, 1.88)
// 标签模版字符串
foo`my name is ${name}, age is ${age}`
函数的默认参数
默认值的写法:
-
两种不严谨的写法
- 默认值写法一:
arg1 = arg1 ? arg1p: "默认值"
- 默认值写法二:
arg1 = arg1 || "默认值"
-
严谨的写法
- 三元运算符
arg1 = (arg1 === undefined || arg1 === null) ? "默认值": arg1
- ES6之后新增的语法:??
arg1 = arg1 ?? "默认值"
-
简便的写法:默认参数
- 可以传入null,因为其中只对undefined进行了判断
function foo(arg1 = "默认值" ,arg2 = '默认值'){}
默认参数的写法注意:
- 有默认参数的形参尽量写到后面
- 有默认参数的形参(和之后的形参),都不会计算在函数的length之内
- 默认参数放到剩余参数前面
箭头函数的额外补充
- 箭头函数是没有显式原型prototype的,所以不能作为构造函数,使用new来创建对象
- 箭头函数也不绑定this、argument、super参数
- 在ES6之后,定义一个类要使用class
展开语法
- 展开语法:
- 可以在函数调用/数组构造时,将数组表达式或者string在语法层面展开
- 还可以在构造字面量对象时,将对象表达式按key-value的方式展开
- 展开语法的场景
- 在函数调用时使用
- 在数组构造时使用
- 在构建对象字面量时,也可以使用展开运算符,这个是在ES9中添加的新特性
- 展开运算符其实是一种浅拷贝
基本使用:
// 基本使用
// ES6
const names = ["a", "b", "c", "d"]
const str = "hello"
function foo(name1, name2, ...args) {
console.log(name1, name2, args);
}
foo(...names)
foo(...str)
// ES9
const obj = {
name: "why",
age: 18
}
// 错误使用:在函数调用时,使用展开运算符,将对应的展开数据,进行迭代
// 可迭代对象:数组/string/arguments
// foo(...obj)
// 在构建对象字面量时使用展开运算符
const info = {
...obj,
height:1.88,
address: "china"
}
console.log(info)
引用赋值-浅拷贝-深拷贝
- 引用赋值
const obj = {
name: "why",
age: 18,
height: 1.88
}
//1.引用赋值
const info1 = obj
//2.浅拷贝
const info2 = {
...obj
}
info2.name = "kobe"
- 浅拷贝内存图
- 如果对象中有一个对象属性,浅拷贝只拷贝对象的引用地址
- 深拷贝
- 第三方库
- 自己实现
- 利用js机制,实现深拷贝JSON
// 深拷贝
const info3 = JSON.parse(JSON.stringify(obj))
数值的表示
- ES6中规范了二进制和八进制的写法:
const num1 = 100
const num2 = 0b100
const num3 = 0o100
const num4 = 0x100
- 在ES2021新增特性:数字过长时,可以使用_连接
const num5 = 100_000_000
Symbol的基本使用
- Symbol是ES6中新增的一个基本数据类型
- 为什么需要Symbol?
- 在ES6之前,对象的属性名都是字符串形式,很容易造成属性名的冲突
- 比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们不确定其中的内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性
- Symbol就是为了解决上面的问题,用来生成一个独一无二的值
- Symbol值就是通过Symbol函数来生成的,生成后可以作为属性名
- 所以在ES6中,对象的属性名可以使用字符串,也可以使用Symbol值
const s1 = Symbol()
//加入对象中
const obj = {
[s1]: "aaa"
}
const s2 = Symbol()
obj[s2] = "bbb"
const s3 = Symbol()
Object.defineProperty(obj, s3, {
value: "aaa"
})
//获取Symbol对应的key
const keys = Object.getOwnPropertySymbols(obj)
for(const key of keys){
}
- ES10的新特性中,我们可以在创建Symbol值的时候传入一个描述description
- 相同值的Symbol
- 如果我们就想创建相同的Symbol
- 我们可以使用Symbol.for方法传入相同的key
- 并且可以通过Symbol.keyFor方法来获取对应的key
//如果相同的key,通过Symbol.for可以生成相同的Symbol值
const s5 = Symbol.for("ddd")
const s6 = Symbol.for("ddd")
console.log(s5 === s6) //true
//获取传入的key
Symbol.keyFor(s5)
Set的基本使用
- 在ES6之前,我们存储数据的结构主要有两种:数组、对象
- 在ES6中新增了另外两种数据结构:Set、Map,以及它们的另外形式WeakSet、WeakMap
- Set是一个新增的数据结构,可以用来保存数据,类似于数组,但是和数组的区别是元素不能重复
- 创建Set我们需要通过Set构造函数(暂时没有字面量创建的方式)
// 创建Set
const set = new Set()
// 添加元素
set.add(10)
set.add(22)
set.add(35)
set.add(22)
console.log(set);
const info = {}
const obj = {}
set.add(info)
set.add(obj)
console.log(set);
// 3.应用场景:数组去重
const names = [ "a", "b", "a", "c", "d", "b"]
// 去重方式一:
// const newNames = []
// for( const item of names) {
// if(!newNames.includes(item)) {
// newNames.push(item)
// }
// }
// console.log(newNames)
// 去重方式二:
const newNamesSet = new Set(names)
const newNames = Array.from(newNamesSet)
console.log(newNames)
- Set常见的属性:
- size:返回Set中元素的个数
- Set常用的方法:
- add(value): 添加某个元素,返回Set对象本身
- delete(value):从set中删除和这个值相等的元素,返回boolean类型
- has(value):判断set中是否存在某个元素,返回boolean类型
- clear():清空set中所有的元素,没有返回值
- forEach(callback, [thisArg]):通过forEach遍历set
- set是一个可迭代对象,也可以使用for…of
set.forEach(item => console.log(item))
WeakSet使用
- WeakSet和Set有什么区别呢?
- weakSet只能存放对象类型,不能存放基本数据类型
- WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC可以对该对象进行回收
- 注意:WeakSet不能遍历
- 因为WeakSet只是对对象的弱引用,如果我们遍历获取到其中的元素,那么可能造成元素不能被正常的销毁
- 所以存储到WeakSet中的对象是没办法获取的
- 应用场景
const pwset = new WeakSet()
class Person {
constructor() {
pwset.add(this)
}
running() {
if(!pwset.has(this)) throw new Error("只能通过实例对象来调用running方法")
console.log("runnning",this)
}
}
Map的基本使用
- ES6新增数据结构Map,用于存储映射关系
- 在之前我们可以使用对象来存储映射关系,他们有什么区别呢?
- 对象存储映射关系**只能使用字符串(ES6新增了Symbol)**作为属性名
- 使用其他类型作为属性名会自动转为字符串类型
- 但是某些时候我们可能希望通过其他类型作为key,比如对象
Map基本使用
const info = { name: "why"}
const info2 = { age: 18}
// Map映射类型
const map = new Map()
map.set(info, "aaa")
map.set(info2, "bbb")
console.log(map);
- Map常见的属性
- size:返回Map中元素的个数
- Map常见的方法:
- set(key, value):在Map中添加key、value,并且返回整个Map对象
- get(key):根据key获取Map中的value
- has(key):判断是否包括某一个key,返回Boolean类型
- delete(key):根据key删除一个键值对,返回Boolean类型
- clear():清空所有的元素
- forEach()
- Map也可以通过for of进行遍历
WeakMap的使用
- WeakMap和Map的区别?
- WeakMap的key只能使用对象,不接受其他的类型作为key
- WeakMap的key对对象的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC可以对该对象进行回收
- 也是不能遍历的
ES6其他知识点
- Promise:用于处理异步的解决方案
- ES Module模块化开发
- 等等
ES7~ES13新特性(三)
ES7-Array Includes
- 在ES7之前,我们通过index Of判断返回值是否是-1来判断一个数组中是否包含某个元素
- 在ES7中,我们可以通过includes来判断
ES7-指数运算符
- 在ES7之前,使用Math.pow
- 在ES7中,使用**运算符
ES8 Object values
- 之前我们通过Object.keys获取一个对象所有的key
- 在ES8中提供了Object.values来获取所有的value值
ES8 Object entries
- 通过Object.entries可以获取一个数组,数组中会存放可枚举属性的键值对数组
- 可以针对对象、数组、字符串进行操作
ES8 - String Padding
- 某些字符串我们需要对其进行前后的填充,来实现某种格式化效果
- ES8中增加了padStart 和padEnd方法,分别对字符串的首尾进行填充
// 1.应用场景一:对时间进行格式化
const minute = "2".padStart(2, "0")
const second = "6".padEnd(2, "0")
console.log(`${minute}:${second}`);
// 2.应用场景二:对一些敏感数据格式化
const cardNumber = "1222334455555"
cardNumber2 = cardNumber.slice(-4).padStart(cardNumber.length, "*")
console.log(cardNumber2)
ES8 - Trailing Commas
- 在ES8中,我们允许在函数定义和调用时在参数尾部多加一个逗号
ES8 - Object Descriptors
- Object.getOwnPropertyDescriptors:获取对象的属性描述符
- Async Function: async、await
ES9新增特性
- Async iterators
- 展开运算符
- Promise finally
ES10 - flat flatMap
- flat()的使用:将一个数组按照指定的深度遍历,将遍历到的元素和子数组中的元素组成一个新的数组,进行返回
// 1.flat()的使用:将一个数组按照指定的深度遍历,将遍历到的元素和子数组中的元素组成一个新的数组,进行返回
const nums = [10, 20, [111, 222], [333, 444], [[123, 321]], [231, 321]]
const newNum1 = nums.flat(1)
console.log(newNum1);
const newNum2 = nums.flat(2)
console.log(newNum2)
- flatMap()方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组
- flatMap是先进行map操作,再做flat的操作
- flatMap中的flat相当于深度为1
const messages = [
"hello world",
"hello flat",
"hello flatmap"
]
const finalMessage = messages.flatMap(item => item.split(" "))
console.log(finalMessage)
ES10 - Object fromEntries
- 将entries转换为对象
// 应用场景
const searchString = "?name=why&&age=18"
const params = new URLSearchParams(searchString)
console.log(params.get(name))
console.log(params.entries())
const paramObj = Object.fromEntries(params.entries())
console.log(paramObj)
ES10 - trimStart trimEnd
- 去除一个字符串首尾的空格,我们可以使用trim方法
- 如果单独去除前面或者后面呢?
- ES10给我们提供了trimStart和trimEnd
ES10 其他知识点
- Symbol description
- Option catch binding
ES11 - BigInt
- 大于Number.MAX_SAFE_INTEGER 的数值,表示的可能是不正确的
- BigInt用于表示大的整数
- BigInt的表示方法是在数值的后面加上n
ES11 - 空值合并运算符
- ES11添加了空值合并运算符
- foo为undefined或者null时使用默认值
const result = foo ?? "默认值"
ES11 - 可选链
- 主要作用是让我们的代码在进行undefined和null判断时更加清晰
const obj = {
name: "why",
frient: {
name: "kobe",
running: function() {
console.log("running");
}
}
}
// 这样使用不安全,不确定friend是否是undefined
obj.frient.running()
// 可选链的写法
// 想判断有没有再获取
obj?.frient?.running?.()
ES11 - Global This
- 在ES11中对获取全局对象进行了统一的规范:globalThis
ES11 - for… in标准化
- 对其进行了标准化,for…in是用于遍历对象中的key的
ES12 - FinalizationRegistry
- FinalizationRegistry 对象可以让你在对象被垃圾回收时请求一个回调
- 可以调用register方法,注册任何你想要清理回调的对象,传入该对象和所含的值
let obj = { name: "why" }
// 先创建对象
const registry = new FinalizationRegistry(value => {
console.log("对象被销毁了", value);
})
registry.register(obj, "obj")
obj = null
ES12 - WeakRefs
- 如果我们默认将一个对象赋值给另外一个引用,那么这个引用是一个强引用
- 如果有多个这样的强引用存在,对象可能不会被真正清除
- 如果我们希望是一个弱引用的话,可以使用WeakRef
let obj = { name:"why"}
let info = new WeakRef(obj)
ES12 - 逻辑赋值运算符
function foo(message) {
//message = message || "默认值"
message ||= "默认值"
// message = message ?? "默认值"
message ??= "默认值"
//message = message && message.name
message &&= message.name
}
ES12 - 其他知识点
- 数字分隔符
- 字符串replaceAll方法
const message = "my name is why"
//replace只替换第一个
const newMessage = message.replace("why", "kobe")
//replaceAll替换所有
const newMessage = message.replaceAll("why", "kobe")
ES13 - method .at()
- 用来获取字符串、数组的元素,可以使用负值
name.at(-1)
name.at(1)
ES13 - Object.hasOwn(obj, porpKey)
- 该方法是一个类方法
- 用于判断对象中是否有某个自己的属性
- 和之前的Object.prototype.hasOwnProperty有什么区别
- 防止对象中重写hasOwnProperty方法
- 原型为null时没有hasOwnProperty方法
ES13 - new members of classes
class Person {
//对象属性:public 公共的属性
height = 1.99
// 对象属性:private 私有:约定俗成
_intro = 'name'
// ES13 私有属性
#intro = "name"
//ES13 私有类属性
static #male = "70"
constructor(name, age) {
// 对象中的属性
this.name = name
this.age = age
}
// ES13 静态代码块
// 会自动执行一次,可以进行初始化操作
static {
console.log("hello world");
}
}
Proxy - Reflect使用详解
监听对象的操作
- 使用Object.defineProperty
const keys = Objects.keys(obj)
for(const key of keys) {
let value = obj[key]
Object.defineProperty(obj, key, {
set: function(newValue) {
value = newValue
},
get: function() {
return value
}
})
}
Proxy基本使用
- ES6新增,帮助我们创建一个代理
- 之后对对象的所有操作,都通过代理对象来完成
const obj = {
name: "why",
age: 18,
height: 1.88
}
// 1. 创建一个Proxy对象
const objProxy = new Proxy(obj, {
// 编写捕获器
get: function(target,key) {
return target[key]
},
set: function(target, key, value) {
target[key] = value
},
deleteProperty: function(target, key) {
console.log("监听删除")
delete target[key]
},
has:function(target, key) {
console.log("监听判断")
return key in target
}
})
// 2.对obj的所有操作,去操作objProxy
console.log(objProxy.name);
objProxy.name = "lobe"
console.log(objProxy.name);
Reflect的作用
- 也是ES6中新增的API,它是一个对象,字面的意思是反射
- 主要提供了很多操作JS对象的方法,有点像Object中操作对象的方法
- 比如Relect.getPrototypeOf(target)类似于Object.getPrototypeOf()
- 为什么还需要Reflect这样的新增对象呢?
- 早期没有考虑到这种对对象本身的操作如何设计更加规范,所以将这些API放到了Object中
- 但是Object作为一个构造函数,这些操作实际上并不合适
- 另外还包含类似于in、delete操作符,让JS看起来很奇怪
- ES6中新增了Reflect,让我们把这些操作集中到Reflect中
- 另外在使用Proxy时,可以做到不操作原对象
Reflect和Proxy共同完成代理
const obj = {
name: "why",
age: 18,
height: 1.88
}
// 1. 创建一个Proxy对象
const objProxy = new Proxy(obj, {
// 编写捕获器
get: function(target,key) {
return target[key]
},
set: function(target, key, value, receiver) {
// 不用直接操作原对象,而且返回布尔值,可以判断是否修改成功
Reflect.set(target, key, value)
// receiver是proxy对象
},
deleteProperty: function(target, key) {
console.log("监听删除")
delete target[key]
},
has:function(target, key) {
console.log("监听判断")
return key in target
}
})
objProxy.name = "lobe"
console.log(objProxy.name);
Promise使用详解
异步任务的处理
- ES5之前
function execCode(counter, successCallback, failureCallback) {
// 异步任务
setTimeout(() => {
if (counter > 0) {
// ...
successCallback(counter)
} else {
failureCallback("error")
}
})
}
execCode(100, (value) => {
console.log(value);
}, (err) => {
console.log(err)
})
promise解决异步问题
- 在通过new创建Promise对象时,我们需要传入一个回调函数,我们称之为executor
- 这个回调函数会被立即执行,并且传入另外两个回调函数resolve、reject
- 当我们调用resolve回调函数时,会执行Promise对象的then方法传入的回调函数
- 当我们调用reject回调函数时,会执行Promise对象的catch方法传入的回调函数
function execCode(counter) {
const promise = new Promise((resolve, reject) => {
// 异步任务
setTimeout(() => {
if (counter > 0) {
// ...
//对应then
resolve(counter)
} else {
//对应catch
reject("error")
}
})
})
return promise
}
execCode(100).then((value) => {
console.log(value);
}).catch((err) => {
console.log(err)
})
promise的三种状态
- pending:待定状态,既没有兑现也没有拒绝
- fulfilled:成功状态,执行了resolve
- rejected:失败状态,执行了reject
- Promise的状态一旦确定,就不能更改,也不能再次执行回调函数
then的返回值
- then方法本身是有返回值的,它的返回值是一个Promise,所以我们可以进行如下的链式调用:
catch返回值
- catch方法也是会返回一个Promise对象的,所以catch方法后面我们可以继续调用then方法或者catch方法
finally方法
- finally是ES9中新增的特性,表示无论Promise对象无论变成fulfilled还是rejected状态,最终都会被执行的代码
- finally方法是不接受参数的
Promise类方法 - resolve/reject
- 有时候我们已经有一个现成的内容了,希望将其转成Promise来使用,这个时候我们可以使用Promise.resolve来完成
- Promise.resolve的用法相当于new Promise, 并且执行resolve操作
all方法
- Promise.all
- 它的作用是将多个Promise包裹在一起形成一个新的Promise
- 当所有的Promise变成fulfilled时,新的Promise变成fulfilled,并且将所有Promise返回值组成一个数组
- 当有一个Promise变成reject时,新的Promise状态为reject,并且会将第一个reject的返回值作为参数
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("p1")
}, 3000)
})
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("p2")
}, 2000)
})
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("p3")
}, 5000)
})
Promise.all([p1, p2, p3]).then(res => {
console.log(res)
})
迭代器和生成器
什么是迭代器?
- 迭代器(iterator):使用户在容器对象(例如链表或者数组)上遍访的对象,使用该接口无需关心对象的内部实现细节
- 总结:迭代器就是帮助我们对某个数据结构进行遍历的对象
- 在JS中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议
- 迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方法
- 在JS中这个标准就是一个特定的next方法:
- next方法有如下的要求:
- 一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象:
- done:
- 判断是否迭代结束
- 如果迭代器可以产生序列中的下一个值,则为false。(等价于没有指定done这个属性)
- 如果迭代器已将序列迭代完毕,则为true。这种情况下,value是可选的,如果它依然存在,即为迭代结束之后的默认返回值
- value:
- 迭代器返回的任何JS值,done为true时可省略
const names = ["abc", "cba", "nba"]
// 封装一个函数创建数组迭代器
function createArrayIterator(arr) {
let index = 0
return {
next: function () {
if (index < arr.length) {
return { done: false, value: arr[index++] }
} else {
return { done: true }
}
}
}
}
const namesIterator = createArrayIterator(names)
console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());
将对象变为可迭代对象
- 当一个对象实现了iterable protocal协议时,它就是一个可迭代对象
- 这个对象的要求是必须实现@@iterator方法,在代码中我们使用Symbol.iterator访问该属性
- 当一个对象变成一个可迭代对象的时候,就可以进行某些迭代操作
- 比如for…of操作时,其实就会调用它的@@iterator方法
// 将infos变成一个可迭代对象
// 1.必须实现一个特定的函数: [Symbol.iterator]
// 2.这个函数需要返回一个迭代器(迭代当前这个对象)
const infos = {
name:"why",
age:18,
height:1.88,
[Symbol.iterator](){
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
}
}
// 可迭代对象具备以下特点:
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)
}
async、await、时间循环
异步函数 async function
- async关键字用于声明一个异步函数:
- async:异步、非同步
- sync:同步、同时
async function foo() {
console.log('1')
return 123
}
foo().then(res => {
console.log("res:", res)
})
await关键字
- 只能在async函数中使用await关键字
- await关键字
- 通常在await后面跟上一个表达式,这个表达式会返回一个Promise
- await会等到Promise的状态变成fulfiled状态,之后继续执行异步函数
async function fetchData() {
try {
const response = await fetch('https://example.com/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Error:', error);
}
}
fetchData().then(result => {
console.log('Data:', result);
});
浏览器事件循环机制
进程和线程
操作系统中:
- 进程:计算机已经运行的程序,是操作系统管理程序的一种方式
- 打开任务管理器可以查看进程,一个打开的应用程序至少有一个进程
- 线程:操作系统能够运行运算调度的最小单位,通常情况下它被包含在进程中
- 每个应用程序都是由代码开发的,而运行这些代码的就是线程,每个进程中至少有一个线程
- 总结:
- 进程:启动一个应用程序,就会默认启动一个进程(也可能是多个进程)
- 线程:每个进程中,都会启动至少一个线程用来执行程序中的代码,这个线程被称之为主线程
- 所以我们可以说进程是线程的容器
- 形象解释
- 操作系统类似于一个大工厂
- 工厂中有很多车间,这个车间就是进程
- 每个车间可能有一个以上的工人在工厂,这个工人就是线程
操作系统的工作方式
- 操作系统是如何做到同时让多个进程同时工作的?
- 因为CPU的运算速度非常快,可以快速在多个进程之间切换
- 当进程中的线程获取到时间片时,就可以快速执行我们编写的代码
- 对于用户来说是感受不到这种快速的切换的
浏览器中的JS线程
-
JS是单线程的,但是JS的线程应该有自己的容器进程:浏览器或者Node
-
浏览器是一个进程吗?
- 目前多数的浏览器其实都是多进程的,当我们打开一个tab页面时就会开启一个新的进程,这是为了防止一个页面卡死而导致所有页面无法响应
- 每个进程中又有很多的线程,其中包括执行JS的线程
-
JS的代码执行是在一个单独的线程中执行的:
- 这就是意味着JS的代码,在同一时刻只能做一件事
- 如果这件事是非常耗时的,就意味当前的线程会被阻塞
-
但是耗时的操作,并不会由JS线程来执行
- 浏览器的每个进程都是多线程的,那么其他线程可以来完成这个耗时的操作
- 比如网路请求、定时器等,我们只需要在特定的时候执行应该有的回调即可
- setTimeout的计时操作由浏览器来执行
-
JS线程执行代码碰到setTimeout函数,会将计时操作交给浏览器线程执行,然后继续执行之后的js代码
-
当js代码执行完毕之后,浏览器的计时操作也结束,会将计时器结束的回调函数的js代码放到一个事件队列中
-
然后依次取出事件队列中的事件执行
浏览器的事件循环
- 如果在执行JS代码的过程中,有异步操作
- 比如说中间我们插入了一个setTimeout的函数调用
- 这个函数会被放到调用栈中,不会阻塞后续代码的执行
单线程-微任务和宏任务
- 浏览器事件循环中有两个队列:
- 宏任务队列:ajax、setTimeout、setInterval、DOM监听、UI Rendering
- 微任务队列:Promise的then回调、Mutation ObServe API、queueMicrotask()等
- 时间循环对于两个队列的优先级是怎么样的?
- 主线程的先执行
- 在执行任何一个宏任务之前,都会先查看微任务队列中是否有任务需要执行
- 也就是宏任务执行之前,必须保证微任务队列是空的
- 如果不为空,那么就优先执行微任务队列中的任务
Promise面试题
- script start
- promise1
- 2
- script end
- then1
- queueMicrotask1
- then3
- setTimeout1
- then2
- then4
- setTimeout2
- await之后的代码相当于调用then()之后执行的,所以会加入到微任务中
- script start
- async1 start
- async2
- promise1
- script end
- async1 end
- promise2
- setTimeout
错误处理方案
- 开发中我们会封装一些工具函数,封装之后给别人使用:
- 在其他人使用的过程中,可能会传递一些参数;
- 对于函数来说,需要对这些参数进行验证,否则可能得到的是我们不想要的结果
- 当我们验证到不是希望得到的参数时,就会直接return
- 弊端是调用者不知道是因为函数内部没有正常执行,还是执行结果就是一个undefined
- 正确的做法应该是如果没有通过某些验证,让外界知道函数内部报错了
- 可以通过throw关键字,抛出一个异常
- throw语句:
- throw语句用于抛出一个用户自定义的异常
- 当遇到throw语句时,当前的函数执行会被停止(throw后面的语句不会执行)
throw关键字
- throw表达式就是在throw后面可以跟上一个表达式来表示具体的异常信息
- Error类型:JS已经给我们提供了一个Error类,我们可以直接创建这个类的对象
- Error包含三个属性
- message
- name
- stack
- Error的子类
- RangeError:下标值越界时使用的错误类型
- SyntaxError:解析语法错误时使用的错误类型
- TypeError:出现类型错误我时,使用的错误类型
- Error包含三个属性
异常的捕获方式
- 很多情况下出现异常时,我们并不需要程序直接推出,而是希望可以正确的处理异常
- 可以使用try catch
try {
foo()
} catch (err) {
console.log(err)
} finally {
}
Storage
认识Storage
- WebStorage提供了一种机制,可以让浏览器提供一种比cookie更直观的key、value存储方式:
- localStorage:本地存储,提供的是一种永久性的存储方式,在关闭网页重新打开时,存储的内容依然保留
- sessionStorage:会话存储,提供的是本次会话的存储,在关闭掉会话时,存储的内容会被清除
storage的基本使用:
let token = localStorage.getItem("token")
if (!token) {
console.log("从服务器获取");
token = "coderwhy"
localStorage.setItem("token", token)
}
let username = localStorage.getItem("username")
let password = localStorage.getItem("password")
if (!username || !password) {
console.log("用户输入账号密码")
username = "coderwhy"
password = "123456"
localStorage.setItem("username", username)
localStorage.setItem("password", password)
}
Storage常见的方法和属性
- 属性:
- Storage.length:只读属性
- 返回一个整数,表示存储在Storage对象中的数据项数量
- Storage.length:只读属性
- 方法:
- Storage.key(index): 该方法接受一个数值n作为参数,返回存储中的第n个key名称
- Storage.getItem()
- Storage.setItem()
- Storage.removeItem()
- Storage.clear()
Storage工具封装
class Cache {
constructor(isLocal = true) {
this.storage = isLocal ? localStorage: sessionStorage
}
setCache(key, value) {
if(value) {
this.storage.setItem(key, JSON.stringify(value))
}
}
getCache(key) {
const result = this.storage.getItem(key)
if(result) {
return JSON.parse(result)
}
}
removeCache(key) {
this.storage.removeItem(key)
}
clear() {
this.storage.clear()
}
}
const localStorage = new Cache()
const sessionCache = new Cache(false)
防抖
- 防抖:通过防抖来延迟某一操作,来防止某一操作频繁执行
- 防抖的应用场景:
- 输入框频繁的输入内容,搜索或者提交信息
- 频繁的点击按钮,触发某个事件
- 监听浏览器滚动事件,完成某些特定操作
- 用户缩放浏览器的resize事件
- 手写防抖代码的基本功能实现
// 手写防抖函数基本功能实现
function Mydebounce(fn, delay) {
// 用于记录上一次事件触发的timer
let timer = null
// 触发事件时执行的函数
const _debounce = () => {
// 取消上一次的事件
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn()
}, delay)
}
return _debounce
}
- this和参数绑定
// 手写防抖函数基本功能实现
function Mydebounce(fn, delay) {
// 用于记录上一次事件触发的timer
let timer = null
// 触发事件时执行的函数
const _debounce = function (...args) {
// 取消上一次的事件
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
// 这里的this可以绑定输入框input,然后使用value值
fn().apply(this, args)
}, delay)
}
return _debounce
}
节流
- 在固定的时间内只触发一次,固定频率
- 应用场景
- 监听页面的滚动事件
- 鼠标移动事件
- 用户频繁点击按钮操作
- 游戏中的一些设计
- 节流的基本实现
// 手写节流函数的时间差基本功能实现
function Mythrottle(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
}
// 利用定时器实现节流
function throttle (fn, delay) {
let timer = null
return function (...arg) {
if(timer) return
timer = setTimeout(()=> {
fn.apply(this, arg)
timer = null
},delay)
}
}
深拷贝
- 浅拷贝:第一层数据的拷贝,如果第一层数据中有对象,那么会引用同一个对象
// 手写deepClone深拷贝算法
function deepCopy(obj) {
if (typeof obj !== "object" || obj == null) {
return obj
}
let res = obj instanceof Array? [] : {}
// 通过for...in循环对象的所有枚举属性,然后再使用hasOwnProperty()方法来忽略继承属性
for (let key in obj) {
if(obj.hasOwnProperty(key)) {
res[key] = deepCopy(obj[key])
}
}
return res
}
// 判断一个标识符是否是对象类型
function isObject(value) {
const valueType = typeof value
return (value !== null) && ( valueType === "object" || valueType === "function")
}
function deepCopy(obj) {
// 1.如果是原始类型,直接返回
if (!isObject(obj)) {
return obj
}
// 2.如果是对象类型,才需要创建对象
const newObj = {}
for (const key in obj) {
newObj[key] = deepCopy(obj[key])
}
return newObj
}
const info = {
name: "coder",
age: 18,
friend:{
name:"why",
address: {
name: "洛杉矶",
detail: "斯坦普斯"
}
}
}
// 深拷贝
// 1.JSON方法
//当拷贝的数据为undefined,function(){}等时拷贝会为空
const obj4 = JSON.parse(JSON.stringify(info))
// 2.自己实现
const obj5 = deepCopy(info)
obj5.friend.name = "hello world"
console.log(info.friend.name);
console.log(obj5.friend.name);
事件总线
// 手写实现事件总线
// 事件总线:我在这里点击触发,在那里执行
class MyEventBus {
constructor() {
this.eventMap = {}
}
on(eventName, eventFn) {
let eventFns = this.eventMap[eventName]
if (!eventFns) {
eventFns = []
this.eventMap[eventName] = eventFns
}
eventFns.push(eventFn)
}
emit(eventName) {
let eventFns = this.eventMap[eventName]
if (!eventFns) return
eventFns.forEach(fn => {
fn(...args)
})
}
off(eventName, eventFn) {
let eventFns = this.eventMap[eventName]
if (!eventFns) return
for (let i = 0; i < eventFns.length; i++) {
const fn = eventFns[i]
if (fn === eventFn) {
eventFns.splice(i, 1)
break
}
}
}
}
JS网络编程
前后端分离的优势
-
早期的网页都是通过后端渲染来完成的:服务器端渲染(SSR)
-
客户端发出请求 -> 服务端接受请求并返回HTML文档 -> 页面刷新,客户端加载新的HTML
文档
-
-
服务器端渲染的缺点:
- 当用户点击页面中的某个按钮向服务器发送请求时,页面本质上只是一些数据发生了变化,而此时服务器却要将重绘的整个页面返回给浏览器加载。
- 这给网络带宽带来了不必要的开销
-
在页面数据变动时,只向服务器请求新的数据,并且在阻止页面刷新的情况下,动态的替换页面中展示的数据?
- 使用AJAX:是一种无页面刷新,获取服务器数据的技术
什么是HTTP?
- HTTP是一个客户端(用户)和服务端之间请求和响应的标准
- 网页中的资源通常是被放在Web服务器中的,由浏览器自动发送HTTP请求来获取、解析、展示
HTTP的组成
- 一次HTTP请求主要包括:请求和响应
- HTTP的组成部分
HTTP的请求方式
- 在RFC中定义了一组请求方式,来表示要对给定资源执行的操作
- GET:GET方法请求一个指定资源的表示形式,使用GET的请求应该只被用于获取数据
- HEAD:与GET的区别是响应没有响应体
- 针对那些比较大的文件,先响应头中获取文件的大小,再决定是否进行下载
- POST:提交数据
- PUT:提交数据并且替换目标资源的所有当前表示
- DELETE:删除数据
- PATCH:修改部分数据
- CONNECT:用在代理服务器和另一个服务器的连接,让代理服务器帮我们获取数据
- TRACE:执行一个消息回环测试
HTTP请求头
- accept-encoding:告知服务器,客户端支持的文件压缩格式
- accept:告知服务器,客户端可接受的文件的格式类型
- user-agent:客户端相关的信息
HTTP响应状态码
- Http状态码是用来表示Http响应状态的数字代码
- 400以上都是错误请求
- 500以上是服务器的错误
AJAX发送请求
// 1.创建XMLHTTPRequest对象
const xhr = new XMLHttpRequest()
// 2.监听状态的改变(宏任务)
xhr.onreadystatechange = function() {
// console.log(xhr.response);
//数据如果没有完全下载完成,直接返回
if (xhr.readyState !== XMLHttpRequest.DONE) return
// 将字符串转成JSON对象(JS对象)
const resJSON = JSON.parse(xhr.response)
console.log(resJSON)
}
// 3.配置请求
// method: 请求的方式(get/post/delete/put/patch...)
// url:请求的地址
xhr.open("get", "http://123.207.32.32:8000/home/multidata")
// 4.发送请求
xhr.send()
- XMLHttpRequest的state
- 发送同步请求:
- 将open的第三个参数设置为false
xhr的其他事件监听
响应数据和响应类型
HTTP响应的状态status
- XMLHttpRequest的state是用于记录xhr对象本身的状态变化,并非针对于HTTP的网络请求状态
- 如果我们希望获取HTTP响应的网络状态,可以通过status和statusText来获取
console.log(xhr.status)
console.log(xhr.statusText)
客户端传递参数的四种形式
- 方式一:GET请求的query参数
- 方式二:POST请求x-www-form-urlencoded格式
- 方式三:POST请求FormData格式
- 方式四:POST请求JSON格式
const xhr = new XMLHttpRequest()
xhr.onload = function () {
console.log(xhr.response);
}
xhr.responseType = "json"
// 传递方式一:数据暴露在url中
// xhr.open("get", "http://123.207.32.32:1888/02_param/get?name=why&age=18")
// 传递方式二:post -> urlencoded 数据放在请求体中
// xhr.open("post", "http://123.207.32.32:1888/02_param/posturl")
// xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded")
// xhr.send("name=why&age=18")
// 传递方式三:传递form表单中的数据
// const formData = new FormData(formEl)
// xhr.send(formData)
// 传递方式四:json
xhr.open("post", "http://123.207.32.32:1888/02_param/postjson")
xhr.setRequestHeader("Content-type", "application/json")
xhr.send(JSON.stringify({ name: "why", age: 18 }))
ajax网络请求封装
function Myajax({
url,
method = "get",
timeout = 10000,
headers = {},
success,
error,
data
} = {}) {
const xhr = new XMLHttpRequest()
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
success && success(xhr.response)
} else {
error && error({ status: xhr.status, message: xhr.statusText })
}
}
xhr.responseType = "json"
if (method.toUpperCase() === "GET") {
const queryStrings = []
for (const key in data) {
queryStrings.push(`${key}=${data[key]}`)
}
url = url + "?" + queryStrings.join('&')
xhr.open(method, url)
xhr.send()
} else {
xhr.open(method, url)
xhr.setRequestHeader("Content-type", "application/json")
xhr.send(JSON.stringify(data))
}
}
//get请求
Myajax({
url: "http://123.207.32.32:8000/home/multidata",
success: function (res) {
console.log(res)
},
error: function (err) {
console.log(err)
}
})
// post请求
Myajax({
url: "http://123.207.32.32:1888/02_param/postjson",
method: "post",
data: {
name: "jsondata",
age: 22
},
success: function (res) {
console.log(res)
},
error: function (error) {
console.log(error)
}
})
- 封装加上Promise
function Myajax({
url,
method = "get",
timeout = 10000,
headers = {},
data
} = {}) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response)
} else {
reject({ status: xhr.status, message: xhr.statusText })
}
}
xhr.responseType = "json"
if (method.toUpperCase() === "GET") {
const queryStrings = []
for (const key in data) {
queryStrings.push(`${key}=${data[key]}`)
}
url = url + "?" + queryStrings.join('&')
xhr.open(method, url)
xhr.send()
} else {
xhr.open(method, url)
xhr.setRequestHeader("Content-type", "application/json")
xhr.send(JSON.stringify(data))
}
})
}
//get请求
Myajax({
url: "http://123.207.32.32:8000/home/multidata",
}).then(res => {
console.log(res)
}).catch(err => {
console.log(err)
})
延迟时间timeout和取消请求
- 过期时间的设置
//监听过期
xhr.ontimeout = function() {
console.log("请求过期:timeout")
}
//timeout:浏览器达到过期时间还没有获取到对应的结果时,取消本次请求
xhr.timeout = 3000
- 取消请求
xhr.abort()
Fetch方法和API
- Fetch可以看作是早期的XMLHttpRequest的替代方案
- 比如返回值是一个Promise
- 不像XMLHttpRequest一样,所有的操作都在一个对象上
- fetch函数的使用
- body:
- 字符串
- FormData对象,以multipart/form-data形式发送数据
// 1.fetch发送get请求
// 优化方式一:
fetch("http://123.207.32.32:8000/home/multidata").then(res => {
const response = res
return response.json()
}).then(res => {
console.log(res)
}).catch(err => {
console.log(err)
})
// 优化方式二:
// async function getData() {
// const response = await fetch("http://123.207.32.32:8000/home/multidata")
// const res = await response.json()
// console.log(res)
// }
// getData()
// post请求并且有参数
async function getData() {
const response = await fetch("http://123.207.32.32:8000/home/multidata", {
method: "post",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: {
name: "why",
age: 18
}
})
const res = await response.json()
console.log(res)
}
getData()
XMLHttpRequest文件上传
<body>
<input type="file" class="file">
<button class="upload">上传文件</button>
<script src="./index.js"></script>
<script>
const uploadBtn = document.querySelector('.upload')
uploadBtn.onclick = function () {
const xhr = new XMLHttpRequest()
xhr.onload = function () {
console.log(xhr.response)
}
xhr.responseType = "json"
xhr.open("post", "http://123.207.32.32:1888/02_param/upload")
// 表单
const fileEl = document.querySelector(".file")
const file = fileEl.files[0]
const formDate = new FormData()
formDate.append("avatar", file)
xhr.send(formDate)
}
</script>
</body>
Fetch文件上传
const uploadBtn = document.querySelector('.upload')
uploadBtn.onclick = async function () {
// 表单
const fileEl = document.querySelector(".file")
const file = fileEl.files[0]
const formDate = new FormData()
formDate.append("avatar", file)
const response = await fetch("http://123.207.32.32:1888/02_param/upload", {
body: formDate
})
const res = await response.json()
console.log(res)
}