JavaScript核心知识
本文最后更新于 2024-12-13,文章内容可能已经过时。
浏览器工作原理和V8引擎
浏览器工作原理
当我们在网址输入一个站点敲下回车的时候,浏览器做了什么?
首先,浏览器会将
域名
通过DNS解析
获取服务器的IP地址
,然后向服务器发送请求
,待服务器接到请求并返回响应
给客户端index.html
,这样浏览器获取到html文件
后开始对html结构进行解析
,当遇到link标签
和script标签
的时候,浏览器会根据href 和 src属性
对应的链接并向服务器发送请求
,下载对应的css和js文件内容
并执行!
以上解析步骤,就是通过
浏览器内核(渲染引擎)
来实现的!
浏览器渲染原理
在浏览器中,从服务器中
获取html文件
时,在本地是如何解析和渲染的
?
如上图所示:
首先浏览器会
同时解析
加载HTML DOM
以及Stylesheet
样式表,并生成DOM tree
和Style rules
样式规则,两者相结合
后会生成Render tree
渲染树并进行布局
,随后开始绘制界面
,之后在浏览器界面中进行显示
!
注意:
在浏览器
解析DOM tree
的时候,JS是可以在这个阶段进行 DOM 操作
的!在加载
Script脚本
的时候,HTML的解析进度会被阻塞并停止解析
!
浏览器中的JS引擎
JS编写的语言是高级语言,原本是机器所无法理解的语言,因此 JS引擎
,需要给机器做编码转换成机器所能理解的语言
!
SpiderMonkey:
第一款JavaScript引擎
,由Brendan Eich开发
(也就是JavaScript作者
) ;
Chakra:
微软开发,用于IE浏览器
;
JavaScriptCore
: WebKit
中的JavaScript引擎
,Apple
公司开发口;
V8:
Google
开发的强大JavaScript
引擎,也帮助Chrome
从众多浏览器中脱颖而出;
浏览器内核和JS引擎关系
以
webkit
为例,webkit
实际上包含两部分内容
:
WebCore内核:
主要负责html 解析 布局 绘制 渲染
等相关工作!
JavaScriptCore引擎:
负责解析JS代码
!
v8引擎原理
v8引擎
是c++
编写的JavaScript
和WebAssembly
引擎,用于chrome浏览器
和Nodejs
,并且可以在不同平台(win mac linux
)下运行!
如上图所示,
JS引擎
会首先Parse解析
JS源代码并且进行语法分析
和词法解析
,其次会生成AST语法树
,接着通过ignition
转换成字节码
,最后在转换成机器指令
!注意:
字节码支持跨平台!
词法分析
和语法解析
如下有一段代码声明:
const name = "why";
通过
parse
解析后大概如下
{ tokens: [{ "type": "keyword", "value": "const"},{ "type": "identifier", "value": "name"} ] }
以上链接可以编写
JS代码在线解析AST语法树运行结果
!
MachineCode 优化机器码
Turbofun
这个库通过ignition收集执行信息
的,获取执行频率比较高的函数
,会被标记为hot函数
,被标记hot函数会被 MachineCode 直接转换为机器指令
,无须在通过字节码转为汇编然后在转换为机器指令
这个过程!
注意:
如果
函数中的参数类型
发生变化时
function sum(num1,, num2){
return num1+num2;
}
sum(5, 10)
sum(20, 10)
sum('a', 'b')
MachineCode
会转换给字节码
在转换给汇编
最终翻译成机器指令
!
那么我们的JavaScript源码
是如何被解析(Parse过程
)的呢?
Blink
将源码交给V8引擎
,Stream
获取到源码并且进行编码转换
;Scanner
会进行词法分析(lexical analysis )
,词法分析会将代码转换成tokens;
接下来tokens会被转换成AST树
,经过Parser
和PreParser:
口 Parser
就是直接将tokens转成AST树架构
;
口 PreParser
称之为预解析
,为什么需要预解析呢?
必然会这是因为并不是所有的JavaScript代码在一开始时就会被执行
那么对所有的JavaScript代码进行解析会影响网页的运行效率
;
所以V8引擎
就实现了Lazy Parsing
( 延迟解析
)的方案,它的作用是将不必要的函数进行预解析
,也就是只解析暂时需要的内容
,而对函数的全量解析是在函数被调用时才会进行
;
比如我们在一个函数outer内部定义了另外一个函数inner,那么inner函数就会进行预解析:
function outer(){
function inner(){
console.log("asdasd")
}
}
outer()
生成AST树
之后会被Ignition
转换为bytecode(字节码),
之后就是代码的执行
过程了!
全局对象GlobalObject
GlobalObject
也称go
,是v8引擎
在源码解析到AST树生成时
产生的一个全局对象
! 即是window
也是go
!
var name = "张三";
var age = 18;
var weight = 130;
var result = weight / age;
以上代码在
v8引擎解
析过程中会产生一个全局对象GlobalObject
var globalObject = {
Math: {},
String: {}.
setTimeout: 'function',
date: 'Date',
name: undefined,
age: undefined,
weight: undefined,
result: undefined,
window: 'this'
}
以上全局对象代码为伪代码!
window
其实指向的就是globalObject全局对象
!
代码解析
代码
在执行前
,会先行解析
,根据上述变量声明代码
,在解析
过程中,会将全局变量提取到全局对象中GlobalObject
,并在代码开始执行前
,赋值初始值为 undefind!
运行代码
为了能够运行代码,
v8引擎
有一个执行上下文栈(Execution Context Stack)
(函数调用栈
)!通常在函数调用
时会进行入栈
和出栈
!运行代码前,代码会提取到
内存当中
,然后划分两个区域堆
和栈
!
代码运行过程
代码的
运行过程
,就是编写的代码,从开始解析
到执行的一个过程
全局执行上下文
执行变量赋值
的一个过程!
基本类型
解析代码:
如下代码name 变量
在解析代码
的时候被初始值
为undefind!
执行代码:
当执行代码 name 变量赋值时,
该name属性
会从GO(GlobalObject)全局对象
中找相应属性
,若找到并其赋值为"张三"
!
var name = '张三'
引用类型
解析代码:
遇到函数类型
的时候,如下第一行代码先是调用了foo()函数
,但是在解析代码这个阶段并不会执行此代码
,因此会忽略函数调用,
进行下一步代码解析,下一步为函数的声明
,这个时候foo声明
会被提取到全局对象(GO GlobalObject)
当中,随后会开辟一个新的空间
用来存储函数(引用类型)
的空间,在全局对象中被声明foo函数变量
会在新的空间(函数存储)
中开辟一个内存地址
,并进行映射关联
!
foo( 123 ); // 在函数声明前调用函数
function foo( num ){
console.log(m);
var m = 12;
var n = 13;
console.log('foo fun')
}
var globalObject = {
setTimeout: 'function',
date: 'Date',
window: 'window',
foo: "0x0000"
}
foo函数:
对应函数存储空间
中的一个地址(0x00000)
!
执行代码:
当我们在全局上下文中调用foo()函数
时,会从全局对象
中找到对应声明函数变量所映射的一个地址!
函数执行上下文
每当
执行一个函数时
,在执行上下文内都
会生成一个函数执行上下文
!函数执行上下文,就是
函数内部代码块的一些执行逻辑
!函数内部的一些
变量声明等相关逻辑
!其实就是
foo函数内部
的这些!函数
内部声明的变量
,会存放在AO(Activation Object)激活对象
当中!一旦
函数内部执行完
后,函数执行上下文会自动从执行上下文内部弹出并销毁
,同样AO局部作用域内部变量也会被销毁
!
// foo 函数内部声明的变量
console.log(m);
var m = 12;
var n = 13;
console.log('foo fun')
作用域链
作用域链
,函数内部属于单独一个作用域
,也是局部作用域
,当函数内部访问的变量为定义或不存在时
,会查找父作用域
,至到全局作用域
范围内位置!
总结
浏览器工作原理
- 浏览器分为两部分组成->(
内核
和引擎
)! - 内核:
- 内核主要负责页面
html 解析 布局 绘制 渲染
等工作! - DNS解析域名
- 获取服务IP地址
- 向后段发送请求
- 返回
HTML内容
- 浏览器开始解析
- 同时
解析HTML生成(DOM tree)
和CSS生成(Style Rules)
- 生成
Render-Tree
并进行布局调整 - 开始绘制
- 界面显示
- 内核主要负责页面
- js引擎:
js引擎
主要负责js代码解析转换为机器指令最终执行
的一个过程!js源代码
->parse解析
(进行词法分析 语法解析
)解析结果
->AST语法树
的生成语法树
->bytecode(字节code)
接近于汇编编码_(更接近于机器指令)运行代码
代码运行过程
v8引擎: js源代码 -> parse解析[词法解析] -> AST语法树 -> bytecode字节码 -> 运行代码
GO(Global Object)
全局对象
是在代码解析
词法分析是创建的,解析代码时,会将变量声明提取到全局对象中
,并初始值为undefind
,若是函数时
将会
开辟一个新的函数存放空间
并生成一个引用地址指向函数变量
!
VO(Variable Object)
vo
是每个执行上下文会被关联到一个变量环境VO(Variable Object)!
全局上下文
VO
对应GO
全局对象!函数上下文
VO
对应AO
激活对象!
执行上下文栈
执行代码
的一个空间栈
,包含全局执行上下文
和函数执行上下文!
全局执行上下文:
全局变量赋值,函数调用
等相关逻辑操作!
函数执行上下文:
会执行函数内部
的逻辑,如内部变量声明赋值,逻辑操作
等,当函数执行完后会被从执行上下文中销毁
!
// 全局代码
var name = "张三";
var age = 18;
var weight = 130;
function foo( num ){
console.log('局部代码');
var m = 12;
var n = 13;
console.log('foo fun')
}
foo( 1411 ); // 函数调用 函数执行上下文逻辑触发 并生成AO对象包含函数内部变量声明的信息!
全局执行上下文
当代码
解析完毕
时,开始执行代码
,碰到变量赋值
时,会依次在全局对象
中找到对应属性进行赋值!
全局
执行上下文中有一个VO(Variable Object)对象
,且指向GO对象
!
GO(Global Object)全局对象
var globalObject = {
setTimeout: 'function',
date: 'Date',
window: 'window',
name: undefind, // 执行过程中会将name值赋予张三
age: undefind, // 执行代码 赋值18
weight: undefind, // 执行代码 130
foo: "0x0000" // 引用地址 指向函数的存放地址 函数需要手动调用
}
函数执行上下文
当
函数被调用时
,会在执行上下文中插入一条函数执行上下文
,此时会生成AO(Activation Object)
,用来存放函数内部变量声明数
函数在被解析的时候,作用域会
默认加上父级作用域
!
函数
上下文中有一个VO(Variable Object)对象
,且指向AO对象
!当在
函数内部试图访问某个变量值
的时候,若函数内部没有声明
,则会随着作用域链向上查找
,直到GO全局对象上出现为止
,否则报错显示变量未定义的问题!
AO(Activation Object)函数局部对象
{
m: 12;
n: 13;
}
内存管理和垃圾回收
内存管理
内存管理
在各种编程语言内都会设计到内存的使用,我们的代码执行过程都是在内存管理中实现的 !
内存的生命周期
: 内存的空间申请
内存的使用
以及不用的时候销毁
!内存有
手动管理
,和自动管理
,像js java dart
等是自动管理内存空间
的!
在 js 中
内存管理存储
分为两种: 一种是栈
, 一种是堆
!
栈stack
: 用来存放基本数据类型
,字符串 布尔值 数字 symbol
等 !
堆Heap
: 用来存储引用类型
,如函数,对象,数组
!
栈
栈空间
内部直接存储基础类型数据
!
var name = "张三";
var age = 18;
var weight = 130;
堆
堆空间
内用来存储引用类型
,主要是对复杂类型开辟空间后的一个引用地址存储
,并会将地址指向声明变量名称
!
var obj = {
m: 12;
n: 13;
}
栈空间内
会存放一个叫obj的变量
,其值是一个引用地址
,引用地址在堆空间开辟后返回的一个引用地址而指向obj的
!
垃圾回收
垃圾回收的算法,就是
GC Counter
计数!
当一个对象被其它对象所引用时,counter会计数➕ 1
通过
将引用对象 = null
的方式,减少对目标对象的引用
,直到计数为0的时候,会被回收
!
还有一种算法就是
标记清除
闭包
闭包可以理解为“
定义在一个函数内部的函数
”,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数
。当内部函数在外部函数之外被调用时,就会形成闭包。一个
函数内部返回一个函数
,其函数内部有自由变量
,便可称为闭包
!
function outerFunction() {
var outerVar = 'I am outside!';
function innerFunction() {
console.log(outerVar); // 使用外部函数的变量
}
return innerFunction; // 返回内部函数,形成闭包
}
var innerFunc = outerFunction();
innerFunc(); // 输出 "I am outside!"
闭包与函数的区别,当
捕捉到闭包的时候,其内部与外部关联的自由变量也同样会被捕捉,当自由变量即使丢失上下文(外部函数)
,内部也仍然可以访问到!
外部函数一旦执行完
return
之后便会销毁
,其内部变量
依旧可以被内部函数所访问
!
一个
普通的函数function
,如果它可以访问外层作用于的自由变量
,那么这个函数就是一个闭包
;从广义的角度来说: JavaScript中的函数都是闭包;
从狭义的角度来说:
JavaScript中一个函数
,如果访问了外层作用于的变量
,那么它是一个闭包
;
闭包内存泄漏问题
为什么闭包外部函数执行完后,函数执行上下文销毁后,
AO对象却没销毁
?
上图内表示两个
普通函数
,在上下文中从解析到执行的一个过程
,当函数执行完
毕后,函数上下文和AO对象都会被销毁
!
foo函数
的AO对象
没有被销毁的原因是,因为还被bar函数父级作用域所引用指向
,无法被销毁
,因为bar函数内部存在对foo函数作用域的引用,且无法销毁!
总结
- 什么是闭包:
- 闭包就是一个
外部函数内部
包含一个内部函数
并且访问外部函数中自由变量
的一个函数体,当函数被执行时,闭包就会形成!
- 闭包就是一个
- 闭包为什么会有内存泄漏问题:
- 每个
函数执行的时候都会生成一个AO对象
,其内部函数
对外部自由变量访问
时,内部
与外部函数
产生了作用域之间的指向
,导致外部函数销毁时,其AO对象
还被内部函数父级作用域的指向挂载
,且无法销毁! - 销毁的条件,就是当一个引用不存在被其它引用时,该对象就会被销毁!
- 每个
- 如何处理内存泄漏:
- 将不用的函数设置为null即可,只要从
全局作用域
中没有在对其它引用有所指向
了,那么将会被销毁!
- 将不用的函数设置为null即可,只要从
this 指向
在js中,this指向是动态绑定的,一般是谁调用,且this会指向调用者!
全局作用域下的this会指向window对象
!
this指向绑定规则:
可分为默认绑定 隐式绑定 显式绑定 以及 new 绑定
!默认绑定: 就是直接调用 `
foo()
`,默认会绑定window对象
!隐式绑定: 就是通过
函数调用
,如`obj.foo()
`则会 隐式绑定到obj
对象上!显式绑定: 就是通过
call apply bind
方式改变函数内部this指定
!
function thisTest() {
console.log("thisTest", this)
}
thisTest(); // 默认绑定
var obj = {
name: "张三",
age: 18,
weight: 132,
thisTest: thisTest
}
obj.thisTest(); // 隐式绑定
var obj2 = {
name: "李四",
age: 22,
weight: 144,
}
thisTest.apply(obj2); // 显式绑定
以上代码会输出
不同this指向
!
thisTest();
调用后指向 window对象
, 其实调用是window.thisTest(); window 前缀可以省略的
!
obj.thisTest();
调用后会指向obj对象!
thisTest.apply(obj2);
调用后会将this指向绑定到obj2对象上!
显示绑定
在
JavaScript
中,call, apply
, 和bind
都是函数对象的方法
,用于改变函数的执行上下文
(即函数内部的this值
)。\
call
call()
方法使用给定的this值
和单独提供的参数来调用函数
,改变函数本身的this
指向 !如果
thisArg
参数传递是null undefind
时,则默认会指向全局对象window
!
function.call(thisArg, arg1, arg2, ...)
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.call(this, name, price);
this.category = 'food';
}
console.log(new Food('cheese', 5).name); // 输出 "cheese"
apply
与
call
方法相似,却别是参数类型不一样,参数类型以数组方式
传递!如果
thisArg
参数传递是null undefind
时,则默认会指向全局对象window
!
function.apply(thisArg, [argsArray])
thisArg
:在函数运行时使用的this
值。argsArray
:一个数组或类数组对象,其中包含了传递给函数的参数。
function sum(a, b) {
return a + b;
}
function callSum1(a, b) {
return sum.call(this, a, b);
}
function callSum2(a, b) {
return sum.apply(this, [a, b]);
}
console.log(callSum1(1, 2)); // 输出 3
console.log(callSum2(1, 2)); // 输出 3
apply
特别有用在不确定参数数量时
,或者当参数已经以数组形式存在
时。
bind
bind()
方法创建一个新的函数
,当被调用时,其this
值被设定为提供的值
,其参数列表中的序列将被传递给绑定函数
。如果
thisArg
参数传递是null undefind
时,则默认会指向全局对象window
!
function.bind(thisArg[, arg1[, arg2[, ...]]])
var person = {
name: 'John',
age: 25,
greet: function() {
console.log('Hello, my name is ' + this.name);
}
};
var greet = person.greet.bind(person);
greet(); // 输出 "Hello, my name is John"
call
和apply
直接调用函数,并立即执行。call
接受参数列表,而apply
接受一个参数数组。bind
创建一个新函数,可以稍后调用,允许你指定this
的值和初始参数。
new 绑定
在
Javascript
中,函数是可以作为构造函数通过new操作符
来创建!
new关键字调用函数,会有以下步骤:
创建一个
新的对象
这个对象会被执行
prototype
原型连!这个
新对象会绑定到函数调用的this上
(this绑定
在这个步骤完成!)如果函数
没有返回其它对象
,则这个函数会返回这个新对象
!
function Person(name, age){
this.name = name;
this.age = age;
}
// 每产生一个新对象都会绑定到 函数的 this 上
var p1 = new Person("张三", 21);
// 每产生一个新对象都会绑定到 函数的 this 上
var p2 = new Person("李四", 24);
// 每产生一个新对象都会绑定到 函数的 this 上
var p3 = new Person("王五", 20);
绑定规则优先级
默认绑定(window)
<隐式绑定(obj.fun())
<显示绑定(apply/call/bind)
<new绑定
function thisTest() {
console.log("thisTest", this)
}
thisTest(); // 默认绑定 window
var obj = {
name: "张三",
age: 18,
weight: 132,
thisTest: thisTest
}
obj.thisTest(); // 隐式绑定
var obj2 = {
name: "李四",
age: 22,
weight: 144,
}
thisTest.apply(obj2); // 显式绑定
// new 绑定
function Person(name, age){
this.name = name;
this.age = age;
}
// 每产生一个新对象都会绑定到 函数的 this 上
var p1 = new Person("张三", 21);
// 每产生一个新对象都会绑定到 函数的 this 上
var p2 = new Person("李四", 24);
// 每产生一个新对象都会绑定到 函数的 this 上
var p3 = new Person("王五", 20);
显示绑定方法实现
这里对
apply call bind
方法手动实现!
call
// 判断是否为 undefind or null
function isNull(arg) {
return Object.prototype.toString.call(arg) === "[object Null]" ||
Object.prototype.toString.call(arg) === "[object Undefind]"
}
Function.prototype.mycall = function (thisArgs, ...args) {
/* 获取函数的调用者 */
var fun = this;
/* 通过隐式调用改变函数内部的this指向 */
/* 如果参数不是对象(基本类型) 则需要将参数基本类型转换为包装类 */
thisArgs = !isNull(thisArgs) ? Object(thisArgs) : window;
thisArgs.fun = fun;
var result = thisArgs.fun(...args);
return result;
}
function sum(num1, num2) {
console.log(num1, num2, this); // 0 1 { name: "张三" }
return num1 + num2;
}
var aaaa = sum.mycall({ name: "张三" }, 0, 1)
console.log("result", aaaa) // 1
这里通过
prototype 原型连
的方式,将mycall函数方法挂载到Function构造器
上,这样不同的函数都会继承该方法
!一旦
其它函数调用mycall方法
时,就会隐式绑定一个this
, 这个this就是调用者
!根据
调用者
传递的thisArgs
将其转换为对象,并且把调用者函数以属性值的方式赋值给thisArgs对象中
然后
通过隐式调用方式
,改变调用者函数内部的this指向
!
考虑到
thisArgs
在调用者传递时,可能不为对象
,可能是基本类型
,可能是null undefind
,当为基本类型时,需要转换为基本类型的包装类对象
,若是undefind
或者是null
时,需要将this指向为 window!
Object(val)
用来将基本类型转换为包装类!
apply
与
call
类似,就是参数传递格式不一致,apply需要传递数组类型
!
Function.prototype.myapply = function (thisArgs, argArr) {
/* 获取函数的调用者 */
var fun = this;
/* 通过隐式调用改变函数内部的this指向 */
/* 如果参数不是对象(基本类型) 则需要将参数基本类型转换为包装类 */
thisArgs = thisArgs ? Object(thisArgs) : window;
console.log("args", ...argArr)
argArr = !!argArr ? argArr : [];
thisArgs.fun = fun;
var result = thisArgs.fun(...argArr);
return result;
}
function sum(num1, num2) {
console.log(num1, num2, this);
return num1 + num2;
}
var aaaa = sum.myapply("张三", [3, 5])
console.log("result", aaaa)
bind
bind
需要返回一个新的函数
,并且参数传递方式也不一致,返回新的函数参数需要和调用者函数参数进行合并
!
Function.prototype.mybind = function (thisArgs, ...args) {
var fun = this;
return function (...innerArgs) {
var newArgs = [...args, ...innerArgs]
console.log('newArgs', newArgs)
thisArgs = !isNull(thisArgs) ? Object(thisArgs) : window;
thisArgs.fun = fun;
var result = thisArgs.fun(...newArgs);
return result;
}
}
var obj2 = {
name: "李四",
age: 22,
weight: 144,
}
function sum(num1, num2) {
console.log(num1, num2, this);
return num1 + num2;
}
var bindResult = sum.mybind(obj2, 2)
console.log("result", bindResult(1))
箭头函数
箭头函数是ES6新的概念,从书写上变的更加便利了,但是与普通函数是有具体区别的!
箭头函数没有 this 指向,通常this指向会是它的父级,同样也没有 argments 参数对象!
如果在箭头函数中使用arguments参数对象,则会找父级作用域
!
箭头函数不能做构造函数通过new关键字来实现!
箭头函数使用显示this绑定
(apply call bind
)无效!
const arg = () => { ... }
一个参数时可以省略括号
const arg = item => { ... }
多个参数一定需要指定括号
const arg = (item, index) => { ... }
函数体内返回一个对象时
// const arg = () => { a:"111", b: "222" } 这中写法,在做词法分析时,无法认出 { ... } 内是代码块,还是一个对象!
const arg = () => { return { a: "111", b: "222" } }
const arg = () => ( { a: "111", b: "222" } ) // ( ... ) 内可以表达是一个整体,表示{ ... }是对象表达式
函数式编程
arguments
arguments
是函数中的参数数组对象
,当我们传递参数时
,该参数数组对象会帮我们收集成一个数组
,虽然有数组的特性
,但不是真正的数组!arguments
也有length
属性,但是没有数组所特有的方法!
function argumentTest() {
console.log('arguments', arguments, arguments.length);
Array.from(arguments).forEach(item => {
console.log(item)
})
var newArr = Array.prototype.slice.call(arguments);
}
argumentTest(1, 2, 3, 5, 6, 7)
arguments 无法直接使用 array中数组方法 map filter forEach,需要通过其它方法将 arguments 转换为数组后才能使用!
Array.from()
: 是Array
中的一个静态方法
,用来将字符串、DOM集合、NodeList
等转换为数组
!
Array.prototype.slice.call(arguments)
: 内部其实通过for循环
,将arguments
中的每一项添加
到一个新的数组并返回
!
纯函数
纯函数是指满足以下两个条件的函数:
无副作用(Side Effects):纯函数在执行过程中不会改变外部环境的状态,也就是说,它不会修改任何外部变量或对象的状态,也不会进行任何I/O操作(如打印到控制台、网络请求等)。
引用透明性(Referential Transparency):给定相同的输入,纯函数总是返回相同的输出,并且不依赖于且不会修改任何外部状态。这意味着你可以将纯函数调用替换为它的返回值,而不会改变程序的行为。
var arr = [1, 34, 6, 53, 32, 43]
var newArr = arr.slice(2, 4)
var splice = arr.splice(3)
console.log("newarr", newArr, arr)
如以上代码,
splice
操作后会影响原数组的变化
,而slice
它操作后会返回一个新的数组!
// 相同输入 相同输出
// 对外部作用域没有副作用,不会操作函数外的变量,以及事件操作,或者IO操作
function add(num1, num2){
return num1 + num2;
}
// 修改了外界的变量
var name = "张三"
function modify(){
console.log("name", name)
name = "王五";
}
函数柯里化
多个参数拆分,形成多个函数调用的过程就是柯里化
/*
柯里化
*/
function sum1(a) {
return function (b) {
return function (c) {
return a + b + c;
}
}
}
console.log('柯里化' + sum1(1)(2)(3))
// 简化写法
var sum2 = a => b => c => {
return a + b + c;
}
console.log('柯理化2 ' + sum2(3)(5)(2))
为什么使用柯里化
柯里化
遵守了设计模式之一(单一原则
),避免将所有的逻辑交给一个函数处理
,将复杂逻辑分成处理,每一个函数有自己专一处理的逻辑!
var log = date => type => message => {
console.log(`${date.getMinutes()}:${date.getSeconds()} ${type} ${message}`);
}
var newLog = log(new Date())('DEBUG');
newLog('debug信息2');
newLog('debug信息3');
newLog('debug信息4');
// [21:29:04.089] 29:4 DEBUG debug信息2
// [21:29:04.089] 29:4 DEBUG debug信息3
// [21:29:04.089] 29:4 DEBUG debug信息4
以上代码,做了
参数复用
,第一次输出date
和type
后,只需要关心message
即可!
柯里化函数的封装
/*
柯里化函数封装
只要参数个数满足后 才会真正调用函数
*/
function currification(fun) {
function currifi(...args) {
// args 接受的参数满足 所有参数个数时调用函数
if (args.length >= fun.length) {
console.log('currification length', args.length, fun.length)
fun.apply(this, args)
} else {
// 如果不满足 继续返回函数
return function (...args2) {
return currifi.apply(this, [...args, ...args2])
}
}
}
return currifi;
}
function add(x, y, z) {
console.log("currification add", x + y + z);
return x + y + z;
}
var testCurri = currification(add);
testCurri(1)(2, 3)
面向对象
对象
是对某个事物具体的一个抽象和描述
,比如生活中常见的汽车,汽车是具体的物,而汽车品牌,颜色,款式
,则是汽车本身的一些特性具体描述
!
var Car = {
brand: "理想",
color: "blue",
type: "suv",
spreed: "128/h"
}
面向对象的特性
封装
: 属性 和 方法的代码封装!
继承
: 子类继承父类对象的属性和方法,对代码进行复用!
多态
: 同一个方法,可以在不同地方,执行不同结果!
创建对象的方式
new Object()
var car = new Object(); car.brand = "理想"; car.color = "blue", car.type = "suv", car.spreed = "128/h"
字面量形式
var Person = { name: "张三", say: function (){ } }
对象属性的操作
var Car = {
brand: "理想",
color: "blue",
type: "suv",
spreed: "128/h"
}
// 添加属性
Car.style = "2022款"
// 修改属性
Car.brand = "长城炮"
// 删除属性
delete Car.color
对象属性的限制
通过
Object.defineProperty
来限制
对象的读取,遍历
等操作!
- 参数1:
obj 目标对象
,针对此对象进行对象属性控制 - 参数2:
obj 对象属性
, 如果在对象中不存在该属性
的话,则向该对象中新增属性
,并配置该属性的读取遍历相关限制! - 参数3:
{} 配置对象
,根据配置,来针对属性上的访问控制
!
属性配置
属性 | 属性值 | 描述 |
---|---|---|
configurable |
true / false |
是否可以 delete 修改属性 |
enumerable |
true / false |
该属性是否可枚举,可遍历 ,为false 时,该属性无法被读取和遍历 ! |
writable |
true / false |
该属性是否可修改 写入 ! |
value |
任意类型 | 属性值 |
var car = {
brand: "理想",
// color: "blue",
type: "suv",
spreed: "128/h"
}
// 添加属性
car.style = "2022款"
// 修改属性
car.brand = "长城炮"
// 删除属性
// delete Car.color
// 定义一个新的属性
Object.defineProperty(car, 'color', {
value: "SkyBlue",
configurable: true,
writable: true,
enumerable: true
})
console.log("Car", car)
getter 和 setter
配置对象中,
value
属性 和getter
,以及writable
和setter
不能同时存在!
var car = {
brand: "理想",
// color: "blue",
type: "suv",
spreed: "128/h",
_color: "blue"
}
// 添加属性
car.style = "2022款"
// 修改属性
car.brand = "长城炮"
// 删除属性
// delete Car.color
// 定义一个新的属性
Object.defineProperty(car, 'color', {
configurable: true,
enumerable: true,
// value: "SkyBlue", 和 get 方法不能同时存在
// writable: true, 和 set 方法不能同时存在
get() {
console.log("get color", this._color)
return this._color;
},
set(value) {
console.log("set color value", value)
this._color = value
}
})
car.color = "rose red"
console.log("Car", car.color)
定义多个属性
Object.defineProperties 可以帮我们定义多个属性!
var person = {
name: "张三",
_age: 18,
_say: function () {
console.log(`我是${this.name}`)
}
}
Object.defineProperties(person, {
weghit: {
configurable: true,
writable: true,
enumerable: true,
value: 130
},
age: {
configurable: true,
enumerable: true,
get() {
console.log("get age~")
return this._age;
},
set(value) {
this.age = value
}
}
})
console.log("person", person, person.age)
gettter 和 setter 简写方式
var person = {
name: "张三",
_age: 18,
_say: function () {
console.log(`我是${this.name}`)
},
// 效果类似于下面配置代码
get age(){
return this._age;
},
set age( value ){
this._age = value;
}
}
Object.defineProperties(person, {
weghit: {
configurable: true,
writable: true,
enumerable: true,
value: 130
},
/* age: {
configurable: true,
enumerable: true,
get() {
console.log("get age~")
return this._age;
},
set(value) {
this.age = value
}
} */
})
console.log("person", person, person.age)
获取属性配置项
/* 获取对象中指定属性的配置项 */
Object.getOwnPropertyDescriptor( person, "name" )
/* 获取对象中所有属性配置项 */
Object.getOwnPropertyDescriptors( person )
// 获取结果 {"value":130,"writable":true,"enumerable":true,"configurable":true}
禁止对对象属性进行扩展
禁止
对该对象进行属性添加
Object.preventExtensions(person)
禁止属性配置 / 删除属性
Object.seal(person)
禁止属性修改编辑(冻结)
被冻结的对象,属性且无法被修改!
Object.freeze(person)
构造函数
构造函数也可以称为普通函数,只要通过
new
关键字 创建出来的函数,都是构造函数
!
new 操作符作用
new
操作符会创建一个空的对象
将函数的
prototype
给新建对象内部
的prototype(__proto__)
进行赋值其
构造函数
内部的this
会指向创建的新对象
!执行
函数内部的代码
如果函数内部
没有返回空对象
,则返回创建出来的新对象
function Person( name, age ){
this.name = name;
this.age = age;
this.say = function(){
console.log(`${this.name} say ~`)
}
this.eating = function(){
console.log(`${this.name} eating ~`)
}
this.running = function(){
console.log(`${this.name} running ~`)
}
}
// 空对象的 this 指向 函数 this
// 函数中的 prototype 会赋值给 新对象的 prototype
// 执行函数代码
let person1 = new Person("张三", 19);
let person2 = new Person("李四", 22);
console.log("person1", person1)
原型
每个
对象
中都有一个[prototype
],这个属性可以称之为原型
!
隐式原型(对象)
隐式原型(__proto__)
当我们试图去
获取对象中某一个属性
时,这时会触发[Get]
操作,首先在对象上查找该属性
,若没有,就去原型上查找该属性
,没有则返回Undefind!
每个
对象
都有一个内部属性[[Prototype]]
(在ES5之前,这个属性没有直接访问的方式,但在ES6中引入了Object.getPrototypeOf(obj)
和Object.setPrototypeOf(obj, proto)
来获取和设置对象的原型
)。这个属性指向对象的原型
。在ES5之前,通常使用__proto__
属性来访问这个隐式原型,但请注意__proto__
不是标准属性,不推荐直接使用。
var obj1 = {
name: "张三"
}
// obj1.age = 18 对象本身查找
obj1.__proto__.age = 18 // 在对象原型上查找
console.log("obj1.age __proto__", obj1.__proto__)
console.log("obj1.age", obj1.age)
显式原型(函数)
显示原型(prototype)
每个
函数对象
都有一个特殊的属性叫做prototype
。这个属性是一个指针,指向函数的原型对象
,也就是通过这个函数创建的实例的原型
。
function person(){
// new 关键字创建的新对象
var obj = {}
// 新建对象的隐式原型 会 指向 函数的显示原型!
obj.__proto__ = person.prototype
// 构造函数的this指向 会 新建的空对象
person.apply(obj)
return this // 如果没有返回空对象,则返回创建对象本身
}
var person = new person()
prototype
中的属性
fun.prototype 对象
中存在一个特殊属性(constructor
),这个属性其实就是fun函数本身
!
constructor
这个属性默认是不可枚举的
,所以打印的时候,显示的空对象!
function Person() {
}
console.log(Person.prototype.constructor.prototype.constructor)
prototype
对象属性添加修改
Person.prototype.name = "张三";
Person.prototype.age = 18;
隐式原型对象(__proto__)
指向了显示原型(函数)(prototype),
其内部属性会继承自prototype
属性!
获取对象上的原型
Object.getPrototypeOf(obj)
显示与隐式关系
当你通过一个
构造函数
创建一个新实例时(例如使用new Person())
,这个新创建的对象的隐式原型([[Prototype]])会指向构造函数的显示原型(Person.prototype)
。这意味着,通过
构造函数创建的所有实例
都会共享同一个原型对象上的属性和方法
。
原型链
原型链
: 当从对象中获取某个属性
时,若对象不存在这个属性
,则会去原型上查找
,若还没有,则会沿着原型链找
,直到顶层原型链为止
!
在
JavaScript
中,原型链是实现继承
的一种机制。每个对象都有一个内部链接指向另一个对象
,即它的原型对象
。这个原型对象
自身也有一个原型
,直到达到一个终点——null
。这个终点称为原型链的顶端
(Top of the prototype chain
)。
var fruit = {
name: "苹果",
word: "apple"
}
// fruit.color
console.log("fruit color", fruit.__proto__.__proto__)
顶层原型链: 即是 Object.prototype
,当访问__proto__
返回为null
时,则到了原型链顶端
!
创建对象的方式:
字面量
:var car = { brand: "逍客" };
new 关键字
:var car = new Object();
new Object()
创建的对象,类似于调用构造函数
一样,同样会去做,new 关键字内部创建流程
:
在内部创建一个
空的对象
将函数的
prototype
原型赋值与空对象的隐式原型__proto__
将函数的
this
指向空对象
执行函数体
返回
this
空对象
继承
原型链实现继承
通过父类将
子类的共性提取
出来,子类在创建构造函数时,将子类原型挂载到父类
上,即可完成继承!
function Person( friends ) {
this.friends = friends;
}
Person.prototype.eating = function () {
console.log(`${this.name} 在吃饭 ~`)
}
Person.prototype.running = function () {
console.log(`${this.name} 在吃饭 ~`)
}
function Student(name) {
this.name = name;
}
// 将父类挂在到 子类原型上
Student.prototype = new Person()
Student.prototype.studying = function () {
console.log(`${this.name} 在学习 ~`)
}
var stu1 = new Student("张三")
var stu2 = new Student("李四")
stu1.eating()
stu1.running()
stu1.studying()
原型链实现继承的一些弊端
子类
继承父类
时,继承父类的属性无法打印查看具体属性
!当父类中
有引用类型数组
时,多个子类修改数组项,会相互影响
!
var stu1 = new Student("张三")
var stu2 = new Student("王五")
stu1.friends.push("koko")
console.log(`${stu1.name} 的朋友: ${stu1.friends}`)
console.log(`${stu2.name} 的朋友: ${stu2.friends}`)
当
Person
中 有一个friends 数组
时,一个子类修改此数组时,会影响到其它子类!
原型式继承(对象)
通过创建
新的对象
,使用setPrototypeOf()
将父类原型继承到新的对象
,并返回!
// 父类
var obj = {
name: "张三",
age: 18
}
// 1.
function createObject1(o) {
var newObj = {}
// 将 设置父类原型到 新的对象上
Object.setPrototypeOf(newObj, o)
return newObj;
}
// 2.
function createObject2(o) {
var newObj = {}
// 将 设置父类原型到 新的对象上
// Object.setPrototypeOf(newObj, o)
function Fun() { }
Fun.prototype = o;
return new Fun();
}
// 子类
var info = createObject2(obj)
// 3.
// var info = Object.create(obj) 与上方函数内部实现方式一样
console.log("info", info.__proto__) // log info {"name":"张三","age":18}
寄生式继承
/* 寄生式继承 */
var person = {
running: function (name) {
console.log(name + " running ~ ")
}
}
function createStudent(name) {
var stu = Object.create(person)
stu.name = name;
stu.studying = function () {
console.log(" studying ~ ", stu.name)
}
return stu;
}
var stu1 = createStudent("张三")
var stu2 = createStudent("王五")
stu1.running(stu1.name)
stu2.running(stu2.name)
寄生式组合原型式继承
/* 寄生式组合继承 */
function Person() {
// 这里的属性无法被继承,因为给Student原型赋值的是Person的原型,这里的friends并不存在原型里,只有Person对象自己可以访问
this.friends = [];
}
// 这里原型添加的属性和方法可实现继承! 不过引用类型都是同一个地址,多个子类继承时,push添加项,都会互相影响
Person.prototype.friends = [];
Person.prototype.eating = function () {
console.log("extends person eating fun ~ " + this.name)
}
Person.prototype.running = function () {
console.log("extends person running fun ~" + this.name)
}
function Student(name, age, weight, sno) {
this.name = name;
this.age = age;
this.weight = weight;
this.sno = sno;
}
/*
子类继承父类
create 函数会创建一个新的对象,并且将参数对象的原型赋值与新的对象并返回!
prototype 每个函数中特有的原型属性,且原先内部包含 construtar 函数 会指向 函数本身
这里当把子类原型替换成父类原型后,需要重新将 construtar 构造函数定义成 子类,否则 construtar 会被新的对象替换掉
*/
/* Student.prototype = Object.create(Person.prototype)
Object.defineProperty(Student.prototype, 'constructor', {
value: Student,
enumerable: false,
configurable: true,
writable: true
}) */
// 实现继承工具函数
inheritPrototype(Person, Student);
Student.prototype.studying = function () {
console.log(`${this.name} 正在学习~`)
}
var stu1 = new Student("张三", 18, 120, '0012938')
stu1.eating();
stu1.running();
stu1.studying();
stu1.friends.push('bobo')
var stu2 = new Student("李四", 22, 130, '0012939')
stu2.eating();
stu2.running();
stu2.studying();
stu2.friends.push('lolo')
console.log("stu1.friends", stu1.friends)
console.log("stu2.friends", stu2.friends)
// friends 子类操作 相互会有影响
// stu1.friends ["bobo","lolo"]
// stu2.friends ["bobo","lolo"]
Student.prototype, 'constructor'
这里
Student函数
原有的prototype
内部的constuctor
函数是指向Student函数本身
的,由于继承Person prototype
后,就指向的新对象
,因此新对象不存在 constructor 这个属性,则会寻着原型链查找,最终指向了Person
,因此需要手动将 constructor 指向 Student函数
!
添加继承原型工具函数
function inheritPrototype(superObj, subObj) {
subObj.prototype = Object.create(superObj.prototype)
Object.defineProperty(subObj.prototype, 'constructor', {
value: subObj,
enumerable: false,
configurable: true,
writable: true
})
}
对象方法补充
create(obj)
创建一个新的对象,两个参数,一个是
目标对象
,一个是配置对象
!根据
目标对象
,会创建一个新的对象
,并把目标对象赋值与新对象的__proto__原型对象上
!配置对象,可
添加属性以及配置
,并将属性加入创建的新对象
而不是原型
!
var aaaa = {
test: "sdaasdwa"
}
var adsainfo = Object.create(aaaa, {
address: {
value: "address ip",
enumerable: true,
configurable: true,
writable: true
}
})
console.log("adsainfo obj", adsainfo) // adsainfo obj {"address":"address ip"}
console.log("adsainfo __proto__", adsainfo.__proto__) // __proto__ {"test":"sdaasdwa"}
判断对象中有没有该属性
hasOwnProperty
hasOwnProperty
方法,可根据参数判断目标对象中是否存在该属性
,有则返回true
相反为false!
Object.hasOwnProperty("test") // false test属性是复刻目标对象的属性被存放在新对象的原型当中
Object.hasOwnProperty("address") // true address属性在对象中创建
in操作符
in
操作符也可以判断是否存在该属性
!
console.log('test' in adsainfo); // true 不管在当前对象 或者是 在原型对象上都是为 true
for .... in
无论是在本身对象
或者 是在对象原型
上,都会被遍历出来!
hasOwnProperty
: 该方法只判断本身对象中是否存在该属性
,即便是原型上存在,也为false
!in 操作符:
该操作符 无论是本身对象
或者是原型上
的属性,都会被作为参考!
instanceof
instanceof
用来判断函数prototype原型
是否存在构造函数原型链
中!
instanceof Obj
后面这个只能是个函数
不能是对象字面量!
function Obj() {
}
var stu1 = new Obj();
console.log("stu1 in obj", stu1 instanceof Obj) // true
isPrototypeOf
isPrototypeOf()
方法用于检查一个对象是否存在于另一个对象的原型链上
。如果调用对象位于另一个对象的原型链中,则返回true
;否则返回false
。
function Obj() {
}
var stu1 = new Obj();
console.log("stu1 in obj", stu1 instanceof Obj)
console.log("stu1 in obj", Obj.prototype.isPrototypeOf(stu1))
var obj = {
name: "张三"
}
var info = Object.create(obj)
// console.log("stu1 in obj", obj instanceof info) 无法判断
console.log("isPrototypeOf in obj", obj.isPrototypeOf(info))
instanceof:
判断函数
prototype
原型是否在构造函数原型链
中!isPrototypeOf:
可以用于任何对象
,包括函数
、数组
、正则表达式
等。
总结
new 操作符的作用
- 通过
new 操作符
来调用函数时,这个函数被称为构造函数
- 首先 会在函数内部
创建一个空对象
- 其次 会将函数中的原型对象
prototype
赋值给对象隐式原型 __proto__
- 之后 会将构造函数中的
this
指向空的对象 - 最后 执行函数体 然后返回对象
- 通过
原型
- 每个
对象
上都有一个[[prototype]]
原型属性,原型可以帮我们实现属性和方法共享
! 隐式原型
: 是对象中
存在的原型对象(__proto__
),显示原型
: 是函数内部
特有的原型对象(prototype
)
- 每个
原型链
- 当试图从某个对象中
get获取属性
时,若此对象中获取不到,则会去对象的prototype原型
中查找,若还是查不到,则会向上个原型
进行查找,直到Object.prototype
查找为null
为止! - 每个
对象内部都有一个原型
,每一个原型都会指向另一个对象
, 而这个原型自身也有一个原型
,必然形成一个链接!
- 当试图从某个对象中
ES6相关
面向对象
class 类
类的声明
class 类
是ES6新出的声明对象
的一个语法糖
,通过babel代码转换
后,其实内部也是通过函数对象
和原型链实现
的!
// 声明一个 Teacher类
class Teacher {
}
// 声明一个 Teacher函数(类)
function Teacher(){
}
var teacher = new Teacher();
console.log("Teacher class", typeof Teacher) // log Function
console.log("Teacher class", Teacher.prototype) // log {}
console.log("Teacher class", Teacher.prototype.__proto__) // log {}
console.log("Teacher class", Teacher.prototype.contructor) // 指向 Teacher()构造函数
console.log("Teacher class", Teacher.prototype === teacher.__proto__) // true
new
关键字创建了Teacher
类,其内部会帮我们做以下处理:
创建一个新对象
,并将Teacher.prototype
赋值给空对象隐式原型(___proto__)
随后会将函数this指向新建的对象
执行函数体
如果没有返回对象的话,则返回这个新建绑定的对象!
类的构造函数
通过类的
contructor构造函数
进行参数传递!
class Teacher {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
var teacher = new Teacher("张三", 18);
console.log("Teacher class", teacher) // {"name":"张三","age":18}
类中的方法和访问器
class Teacher {
constructor(name, age) {
this.name = name;
this.age = age;
// 下划线代表属性私有访问
this._lectureTime = new Date().toLocaleDateString()
}
// 内部方法其实也定义在了 Teacher 类的原型对象prototype上
teaching() {
console.log(`${this.name} 在授课~`)
}
get lectureTime() {
console.log("get 获取 lectureTime 属性拦截")
return this._lectureTime;
}
set lectureTime(newValue) {
console.log("set 设置 lectureTime 属性拦截")
this._lectureTime = newValue
}
}
// 函数中的写法
/* function Teacher(name, age) {
this.name = name;
this.age = age;
}
Teacher.prototype.teaching = function () {
console.log(`${this.name} 在授课~`)
} */
var teacher = new Teacher("张三", 18);
// teacher.teaching();
console.log(`${teacher.name} 授课时间为 ${teacher.lectureTime}`)
静态方法
在定义方法之前加入前缀
static
则 该函数表示为静态方法
,在不用实例化
的情况下,可以通过类名直接访问静态方法
!
class Teacher {
constructor(name, age) {
this.name = name;
this.age = age;
// 下划线代表属性私有访问
this._lectureTime = new Date().toLocaleDateString()
}
// 内部方法其实也定义在了 Teacher 类的原型对象prototype上
teaching() {
console.log(`${this.name} 在授课~`)
}
// 访问器
get lectureTime() {
console.log("get 获取 lectureTime 属性拦截")
return this._lectureTime;
}
set lectureTime(newValue) {
console.log("set 设置 lectureTime 属性拦截")
this._lectureTime = newValue
}
// 静态方法 可以直接通过类来访问此方法,无需通过new创建对象后访问
static staticTeacherFun() {
console.log("调用了静态方法 staticTeacherFun ~")
}
}
// 静态方法
Teacher.staticTeacherFun();
var teacher = new Teacher("张三", 18);
// teacher.teaching();
console.log(`${teacher.name} 授课时间为 ${teacher.lectureTime}`)
继承
在
class
中继承很简单,通过extends关键字
可实现继承父类!
class Animals {
constructor(name, type) {
this.name = name;
this.type = type;
}
eating() {
console.log(`${this.name} in eating ~`)
}
running() {
console.log(`${this.name} in running ~`)
}
sleep() {
console.log(`${this.name} in sleep ~`)
}
}
class Dog extends Animals {
constructor(name, type) {
// 在 this 之前 调用 super 父级构造函数, 否则会报错
super(name, type);
}
}
class Cat extends Animals {
constructor(name, type) {
// 在 this 之前 调用 super 父级构造函数, 否则会报错
super(name, type);
}
}
var dog = new Dog("柯基犬", "犬科");
var cat = new Cat("波斯猫", "猫科");
dog.eating();
cat.running();
上面代码,一个
父类(Animals)动物
,两个子类(Dog)狗
和(Cat)猫
通过
extends Animals
方式继承了父类在子类
constructor
中使用了super函数
,super
是调用了父类的构造函数
,必须在this之前调用
!
方法重写
如果父类中的方法不满足业务需求时,我们可以在子类中重写父类方法!
class Cat extends Animals {
constructor(name, type) {
// 在 this 之前 调用 super 父级构造函数, 否则会报错
super(name, type);
}
// 方法重写
sleep(){
super.sleep(); // 通过 super 可以直接调用父类方法
// 加入自己的逻辑处理
}
}
Babel ES6转换ES5
如以下
class
代码,babel
会帮我们创建Animals函数
,并将函数中的方法挂载到这个函数的原型对象上
!
class Animals {
constructor(name, type) {
this.name = name;
this.type = type;
}
eating() {
console.log(`${this.name} in eating ~`)
}
running() {
console.log(`${this.name} in running ~`)
}
sleep() {
console.log(`${this.name} in sleep ~`)
}
}
对象字面量增强
属性和方法简写以及属性名计算
var name = "张三"
var age = 22
// 以前写法
var obj = {
name: name,
age: age,
eating: function (){}
}
obj[name + 123] = "属性名计算"
// 现在写法
var obj = {
name,
age,
eating() {},
running: () => {},
[name + 123]: "属性名计算"
}
数据结构
数组
数组是根据
顺序
进行结构
var arr = ["张三", "李四", "王五"]
// 结构三项
var [item1, item2, item3] = arr
console.log(item1, item2, item3) // 张三 李四 王五
// 结构后两项
var [, item2, item3] = arr
console.log(item2, item3) // 李四 王五
// 结构第一项 后一项结构为数组
var [item1, ...newArr] = arr
console.log(item1, newArr) // 张三 ["李四","王五"]
// 结构中的默认值 若 结构中的值不存在,则给默认值
var [item1, item2, item3, item4 = "齐六"] = arr
console.log(item1, item2, item3, item4) // 张三 李四 王五 齐六
// 在函数中使用
function _objFun([, item2, item3]) {
console.log("_objFun", item2, item3) // 李四 王五
}
_objFun(arr)
对象
对象是根据
key
值进行结构
var _obj = { name: "张三", age: 22, weghit: 122 }
var { name, age, weghit } = _obj
console.log(name, age, weghit) // 张三 22 122
// 取部分属性值
var { age, weghit } = _obj
console.log(age, weghit) // 22 122
// 默认值
var { age, weghit, sno = "0123" } = _obj
console.log(age, weghit, sno) // 22 122 0123
// 别名,避免与其它命名冲突
var { name: newName } = _obj
console.log(newName) // 张三
// 在函数中使用
function _objFun({ name, age, weghit }) {
console.log("_objFun", name, age, weghit) // 张三 22 122
}
_objFun(_obj)
let 和 const
let
和const
以及var
关键字都是用来声明变量
的一种方式,除了声明变量以外,也是有点差别的!
var:
是函数作用域
,存在变量提升
,容易污染变量
,并且可以重复定义变量
值,后面会覆盖前面已声明过的变量!
let:
是块级作用域
,不存在变量提升,不能在声明变量前进行访问,存在暂时性死区,不能重复定义变量
!
const:
是块级作用域
,与let的区别
是,const是常量声明
,声明变量时必须有初始值
,且值一旦被赋值,则无法不能被修改
!
注意:
const
虽然值不能被修改
,但是对象的属性值
是可以改的,内存中对象引用地址
不可改!
在
没有let和const之前
,通过var声明的变量
都会保存在window全局对象
当中!
let 和 const
之后,就出现了variableMap
对象!
块级作用域
ES6中块级作用域
的出现,防止了var声明
时可以在块级之外访问
的问题!在块级内部
通过var声明
的变量,作用域是无效的
,可以在块级之外访问定义的变量
!常见的块级有:
if switch for
等表达式,都是块级的表现
!
let const class
都是受块级作用域限制
的,var在块级作用域中无效
!
{
// 块级代码
var age = 12;
let foo = "aaa"
function fun() { }
class obj { }
}
console.log("var ~", age) // var ~ 12
console.log("let ~", foo) // 报错 foo is not defined
console.log("fun ~", fun)
console.log("class ~", obj)
if 块级作用域
if (true) {
var age = 12;
let foo = "aaa"
}
console.log("var ~", age) // 12
console.log("let ~", foo) // 报错 foo is not defined
switch 块级作用域
switch ("a") {
case "a":
var age = 12;
let foo = "aaa"
}
console.log("var ~", age) // 12
console.log("let ~", foo) // 报错 foo is not defined
for 块级作用域
for (var i = 0; i <= 5; i++) {
// var i = 0; 实际上在这里定义了 i 的变量
// i++
}
console.log(i) // 6
还有一个弊端就是,
for循环体中使用function函数
其内部访问了 i 的属性
,那么这个函数,会随着作用域向上全局查找 i 的属性
,这时i 的属性值已经累加到 4
,且不是随着 i 属性的累加 依次获取到的值
!
for (var i = 0; i <= 5; i++) {
function foo(){
console.log(i) // 6 获取的不是 0 1 2 3 4
}
}
console.log(i) // 6
通过立即执行函数,闭包的形式获取
for (var i = 0; i < 5; i++) {
(function(n){
function foo(){
console.log(n) //获取是 0 1 2 3 4
}
})(i)
}
暂时性死区
当
在块级作用域中
通过let 或者 const 声明变量
时,在声明变量之前访问
,则会报错
,这种情况称之为暂时性死区
!
var foo = "name"
if( true ) {
// 访问报错
console.log("foo",foo)
let foo = "asda"
}
模版字符串
基础用法
let _name = "张三";
let _age = 18;
let _weghit = 127;
// 最初的写法
console.log("名字是" + _name + "年龄是" + _age + "体重是" + _weghit)
// 模版字符串 (基础写法)
console.log(`名字是${_name} 年龄是${_age} 体重是${_weghit}`)
// 模版字符串 (属性计算)
console.log(`年龄加2${_age + 2}`)
// 模版字符串 (函数调用)
console.log(`年龄加2${computedAge()}`)
function computedAge() {
return _age + 2;
}
标签模版字符串
以函数的形式进行调用!
let _name = "张三";
let _age = 18;
/*
参数一: 字符串数组,会以${}方式进行拆分!
参数二: ${name} 第一个变量
参数三: ${age} 第二个变量
*/
function _foo(a, b, c) {
console.log("_foo", a, b, c)
}
_foo`Hello${_name}World${_age}` // _foo ["Hello","World",""] 张三 18
函数的默认参数
参数只有为
undefind
,或者参数不传
的时候,才会有默认值!
function _foo(name = "张三", age = 18) {
console.log(name, age)
}
_foo("李四") // 李四 18 age 不传 默认 18
对象参数结构默认值
function _fooObj({ name = "张三", age = 18 }) {
console.log("_fooObj", name, age)
}
_fooObj({})
function _fooObj({ name = "张三", age = 18 } = {}) {
console.log("_fooObj", name, age)
}
function _fooObj({ name = "张三", age = 18, id } = { id: 0910923 }) {
console.log("_fooObj", name, age, id)
}
_fooObj()
通常有默认值的参数放最后,可以省略默认参数传递!
注意: 函数默认值,会影响函数中
length
属性长度大小,有默认值的参数 包括后面的参数都不会算在length
当中!
function _fooObj(name, age = 18, id) {
console.log("_fooObj", name, age)
}
_fooObj.length // 1 [age = 18, id] 不算在长度范围内
函数中剩余参数
function _foo(m, n, ...args) {
console.log("_foo", m, n, args) // _foo 1 23 [3,54,5,2,3,2]
console.log("_foo", arguments) // _foo {"0":1,"1":23,"2":3,"3":54,"4":5,"5":2,"6":3,"7":2}
}
_foo(1, 23, 3, 54, 5, 2, 3, 2)
剩余参数
...args
与arguments
对象相似,
args:
会将多余的参数收集成一个数组
,且不会影响原有的参数
!
arguments:
会将所有的参数
,收集成一个arguments对象
!
箭头函数的补充
箭头函数中因为
不能使用构造函数
,也不能通过new关键字进行实例化
,所以没有自己的原型prototype
,也没有自己的this绑定
!箭头函数中
没有 this
,默认从上级获取
箭头函数中
没有arguments
,默认从上级获取
箭头函数
不能通过 new 关键字实例化构造函数
展开运算符
const names = ["张三", "李四", "王五"];
const desc = "描述内容";
const obj123 = { name: "张三", age: 18 };
// 展开 names 中的数据
console.log("names", ...names); // names 张三 李四 王五
// 展开 names 中的数据放到一个新的数组中
console.log("newNames", [...names]); // newNames ["张三","李四","王五"]
// 展开 字符串
console.log("desc", ...desc); // desc 描 述 内 容
// 展开 对象浅复制
console.log("obj123", { ...obj123, newName: "newName" }); // obj123 {"name":"张三","age":18,"newName":"newName"}
浅拷贝:
当展开对象
时,会对该对象的属性进行浅复制
,在修改新对象中的复制过来的属性
时,不会影响到原对象中的属性值
!
如果对象中
包含引用类型对象
的话,只会复制该对象的引用地址
,其在新对象中修改复制过来对象的属性
时,是会具体影响到原有对象的数据
!
数值(进制)表示
const num = 100; // 十进制
const num1 = 0b100; // 二进制
const num2 = 0o100; // 八进制
const num3 = 0x100; // 十六进制
Symbol基本使用
Symbol
是ES6中新增的基本数据类型
,翻译为符号
!
Symbol数据类型是独一无二的,用来解决对象中,属性命名冲突的问题!
const _name = Symbol("张三");
const _age = Symbol(18);
console.log("Symbol", _name.description, _age.description)
// Symbol 张三 18
作为
对象 key 值
的使用
const _s1 = Symbol();
const _s2 = Symbol();
const _s3 = Symbol();
const _obj11 = {
[_s1]: "李四",
[_s2]: 22
}
_obj11[_s3] = 122
console.log("symbol", _obj11[_s3])
Symbol
不能使用.
操作符来获取Symbol
数据,例如:obj11.s3
,这样是不允许的,因为.
操作符,内部会将获取的key作为字符串来进行处理!
Symbol
作为对象中的key
时,无法通过Object.keys()
来获取对象中的key
值,也无法通过Object.getOwnPropertyNames()
获取!
Object.getOwnPropertySymbols()
可以帮我们获取对象中所有的Symbol值
,并返回一个数组
,然后通过for...of遍历key获取!
let symbols = Object.getOwnPropertySymbols(_obj11)
for (key of symbols) {
console.log("_obj11[key]", _obj11[key])
}
在
Symbol
中如何定义相同值
,需要使用Symbol.for
来实现!
const s1 = Symbol.for("aaa")
const s2 = Symbol.for("aaa")
// 获取s1的值
const getS1 = Symbol.keyFor(s1);
console.log(s1 === s2) // true
数据结构(Set Map)
数据结构,就是
存储数据
的地方,最初的存储方式,是数组
和对象形式
,如今新加了Set Map
以及WeakSet
和WeakMap存储方式
!
强引用
一个对象被一个变脸所引用时,这种赋值的方式便是
强引用
!
let obj = { name: 'example' }; // obj 是一个强引用
只要
obj
变量存在,它所引用的对象就不会被垃圾回收机制回收
。
弱引用
在 ES6 中,引入了
WeakMap
和WeakSet
这两种数据结构,它们的键(key)
就是弱引用
。弱引用,当一个
对象没有被指向时
,此时便可随时可被垃圾回收
!
const weakMap = new WeakMap();
const obj = { name: 'example' };
weakMap.set(obj, 'some value'); // obj 作为键是弱引用
Set 数组结构
Set
是存放不可重复
的一组数据的集合
!
Set
可存放基本类型
和引用类型
!
Set
存放的引用类型属于 强引用
!
const _arr = [12.33, 12, 45, 12]
// set 可以接受数组参数来转换成 一组唯一的 set 数据集合
const _set = new Set(_arr);
_set.add(22) // 添加数据
_set.add(34)
_set.add(42)
console.log("_set.size", _set, _set.size) // 获取集合存储长度
_set.delete(42) // 删除指定数据
_set.has(42) // 判断有没有包含
_set.clear() // 清除集合
使用
for...of
或者forEach
可遍历set集合
数据!
_set.forEach(item => {
console.log(item)
})
for (let item of _set) {
console.log(item)
}
将set集合转换为数组!
const _newArr = Array.from(_set);
const _newArr = [..._set]
console.log("newArr", _newArr)
WeakSet 结构
WeakSet
也是一组不可重复数据
的数据集合!
WeakSet
只能存放对象
,不能存放基本类型!
WeakSet
是弱引用
,随时会被垃圾回收!
WekSet
是不可遍历
的,防止结合内部引用不能正常回收的问题!
const arrObj = [{ name: "张三"}, {name: "李四" }]
const _set = new WeakSet(arrObj);
Map 对象结构
Map 是键值对
存储结构,与普通对象的区别是:
普通对象
: 无法以对象作为 key
,当key为引用对象
时,会将转换为[Object object]字符串
形式来作为 key值
!
Map集合
: 内部key 可以将对象用作键
来进行存储 !
const zhangsan = {
name: "张三",
age: 18
}
const lisi = {
name: "李四",
age: 18
}
const _map = new Map()
// 添加 set key value
_map.set(zhangsan, "张三")
_map.set(lisi, "李四")
_map.set("_mapObj", "okopwdw")
// 获取 get key value
console.log(_map.get(lisi)) // 李四
// 判断 key 是否存在 map结构中
_map.has(zhangsan)
// 根据 key 删除某项
_map.delete(zhangsan)
// 清除
_map.clear()
Map 结构遍历:
也是通过for...of
和forEach
形式去遍历!
_map.forEach((value, key) => {
console.log("foreach", value, key)
/*
log foreach 张三 {"name":"张三","age":18}
log foreach 李四 {"name":"李四","age":18}
log foreach okopwdw _mapObj
*/
})
for (let item of _map) {
/*
返回一个数组
[{"name":"张三","age":18},"张三"]
[{"name":"李四","age":18},"李四"]
["_mapObj","okopwdw"]
*/
console.log("for...of", item[0], item[1])
}
for (let [key, value] of _map) {
/*
返回一个数组
[{"name":"张三","age":18},"张三"]
[{"name":"李四","age":18},"李四"]
["_mapObj","okopwdw"]
*/
console.log("for...of", key, value)
}
WeakMap 结构
weakMap
与map
一样,存储键值对
,区别如下:
map:
键 可存储基本类型
与引用类型
, 且引用类型为强引用
,并且可遍历
!
weakMap:
键只能存储 对象引用类型
,且引用类型为弱引用
,其内部元素是不可遍历
的,没有forEach遍历
方法,也没有clear
方法!
const _weakMap = new WeakMap(); // 其方法与map方法一样 没有 clear 和 forEach方法
Promise异步任务处理
promise
是异步任务处理
,主要在异步任务处理完成时做出的一个结果反馈
!
function request(res, successCallback, errCallback) {
setTimeout(() => {
let { success, data } = res
if (success) {
successCallback(data)
} else {
errCallback({ errMsg: "err..." })
}
}, 100)
}
let _data = { success: true, data: { message: "", id: "1283923351" } };
request(_data, (data) => {
console.log("success", data)
}, (err) => {
console.log("err", err)
})
早期使用了
回调函数
来处理异步函数内部返回结果!替换成
promise
function request(res) {
return new Promise((resolve, reject) => {
setTimeout(() => {
let { success, data } = res
if (success) {
resolve(data)
} else {
reject({ errMsg: "err..." })
}
}, 100)
})
}
let _data = { success: true, data: { message: "", id: "1283923351" } };
request(_data).then((data) => {
console.log("success data", data)
}).catch(e => {
console.log("error e", e)
})
promise
中有两个回调函数,一个是resolve
和reject
!
resolve
: 当结果成功执行
时,调用该回调函数,会自动帮我们调用.then方法!
reject
: 当结果执行失败
时,调用该回调函数,会自动帮我们调用.catch方法!
Promise三种状态
promise
中有三种执行状态
pending
:在代码结果为确定之前!
then: fulfilled
和resolve
为确定结果时!
catch
:reject
驳回 拒绝状态!
new Promise((resolve, reject) => {
// pending 等待状态
}).then(data => {
// resolve 确定
}, e => {
// reject 失败 驳回 拒绝
})
resolve参数
如果
resolve
参数中是普通值
或者对象
,则状态由pending状态
变为fufilled 确定
状态!如果参数传入一个
新的 promise 对象
时,则状态交给新的 promise 来决定
!如果传入一个
对象
,并且对象中有 then 方法
,那么状态交 then 方法
来决定!
const newPromise = new Promise(( resolve, reject )=> {
reject("err");
})
new Promise((resolve, reject) => {
// pending 等待状态
// resolve(newPromise) // 这里状态由新的 promise 来确定
resolve({
then(resolve, reject){
resolve("") // 状态交给 then 来处理
}
})
}).then(data => {
// resolve 确定
}, e => {
// reject 失败 驳回 拒绝
})
对象方法
promise
对象中的方法都是挂载到prototype
上面,包括(then catch finally
)
then
同一个
promise
下then
方法可以调用多次
,当resolve
时,多个then
方法会同时被执行
!
then方法
可以有返回值
,不管是promise
还是普通值
,返回结果都是promise 包装后的值
!
链式调用then时
,则then
中的参数值
,则是上一个then中返回的值
!如果一个
then 没有返回值
的时候,则是undefind
!如果返回一个
对象
,且对象内实现了thenable
!
const newPromise = new Promise((resolve, reject) => {
// reject("err");
resolve("true")
})
newPromise.then((res) => {
console.log("多个 then 同时被执行!", res)
})
newPromise.then((res) => {
console.log("多个 then 同时被执行!", res)
})
newPromise.then((res) => {
console.log("多个 then 同时被执行!", res)
return "猜猜我是谁"
}).then(res => {
console.log("上一个 then 的 返回值res", res)
return {
then(resolve, reject) {
resolve("对象中实现了 thenable ~")
}
}
}).then(res => {
console.log("获取到了对象内thenable中返回到值", res)
})
catch
异常捕获
: 当promise
被reject
时 或者promise
中throw
抛出异常时,会被catch
捕获!
const promise2 = new Promise((resolve, reject) => {
// reject("error");
throw new Error("捕获到 throw 异常~")
}).then(data => {
// resolve 确定
}, e => {
// reject 失败 驳回 拒绝
// console.log("捕获到 reject", e)
console.log("捕获到 throw", e)
})
catch
捕获异常时,会从上往下
依次捕获异常!
如 捕获promise2
-> 其次then
(会返回一个新的promise
)
finally
无论是
resolve
和reject
状态下都会执行该方法!
const promise2 = new Promise((resolve, reject) => {
// reject("error");
// throw new Error("捕获到 throw 异常~")
resolve("true")
})
promise2.then(res => {
}).catch(e => {
}).finally(()=>{
console.log("最终都会执行该代码!")
})
类方法
通过
Promise类
直接调用方法!
resolve
const promise = Promise.resolve({"aaa": "aaaa"})
reject
const promise = Promise.reject({"aaa": "aaaa"})
all
all方法
会接受一个包含多个promise的数组
,当所有promise
返回结果为resolve
时则会执行then
,其中有一个promise
为reject
时,则执行catch
方法!
const p1 = new Promise((resolve, reject) => {
resolve("asdadas")
})
const p2 = new Promise((resolve, reject) => {
resolve("asdadas")
})
const p3 = new Promise((resolve, reject) => {
resolve("asdadas")
})
Promise.all([p1, p2, p3]).then(res => {
console.log("p1, p2, p3", res) // p1, p2, p3 ["asdadas","asdadas","asdadas"]
})
allSettled
all
方法当其中一个promise
中reject
时,则立刻会进行处理,则其它为resolve
的promise无
法获取到!
allSettled
方法,无论是resolve
或者是reject
时都会返回结果!
const p1 = new Promise((resolve, reject) => {
resolve("asdadas")
})
const p2 = new Promise((resolve, reject) => {
reject("asdadas")
})
const p3 = new Promise((resolve, reject) => {
resolve("asdadas")
})
Promise.allSettled([p1, p2, p3]).then(res => {
/* [
{"status":"fulfilled","value":"asdadas"},
{"status":"rejected","reason":"asdadas"},
{"status":"fulfilled","value":"asdadas"}
]*/
console.log("p1, p2, p3", res)
}).catch(e => {
console.log("e", e)
})
race
接受多个
promise
,返回其中一个先执行完的promise
,且结果为resolve fuillled状态
的!但是,如果
promise
其中一个为reject
时,则race 也会将reject结果返回出去
!
const p1 = new Promise((resolve, reject) => {
setTimeout(() => { resolve(1111) }, 1000)
})
const p2 = new Promise((resolve, reject) => {
setTimeout(() => { resolve(2222) }, 2000)
})
const p3 = new Promise((resolve, reject) => {
setTimeout(() => { resolve(3333) }, 3000)
})
Promise.race([p1, p2, p3]).then(res => {
console.log("p1, res) // 1111
}).catch(e => {
console.log("e", e)
})
any
与
race
类似,区别就是,any
会等promise
结果为resolve
时,才会返回结果!
const p1 = new Promise((resolve, reject) => {
setTimeout(() => { resolve(1111) }, 4000)
})
const p2 = new Promise((resolve, reject) => {
setTimeout(() => { reject(2222) }, 2000)
})
const p3 = new Promise((resolve, reject) => {
setTimeout(() => { resolve(3333) }, 3000)
})
Promise.any([p1, p2, p3]).then(res => {
console.log("p1", res) // 3333
}).catch(e => {
console.log("e", e)
})
ES7相关
数组相关
includes
在数组中
是否包含指定元素
console.log("aaa", [12, 123, 32, 12].includes(32)) // true
第二个参数为索引值,意思是从什么地方开始
console.log("aaa", [12, 123, 32, 12].includes(32,3)) // false
指数运算方法
const result = Math.pow(3, 3) // 27 3的3次方
console.log("3**3", 3 ** 3) // 27
ES8相关
Object.values
获取
对象中所有的 value(值)
, 如果是数组
则返回数组本身
,若是字符串
时,则拆分生数组
!
console.log("object.values 对象", Object.values({ name: "张三", age: 22 })) // 对象中所有的 value 对象 ["张三",22]
console.log("object.values 数组", Object.values([12, 32, 44, 11])) // 返回数组本身 [12,32,44,11]
console.log("object.values 字符串", Object.values("abcs")) // 将字符串拆分成数组 ["a","b","c","s"]
Object.entries
可以
将对象
形式转换为键值对二维数组!
console.log("Object.entries", Object.entries({ name: "张三", age: 22 }))
// Object.entries [["name","张三"],["age",22]]
console.log("Object.entries 数组", Object.entries(["张三", "李四", "王五"]))
// 数组 [["0","张三"],["1","李四"],["2","王五"]]
console.log("Object.entries 字符串", Object.entries("ajs"))
// [["0","a"],["1","j"],["2","s"]]
padStart & padEnd
字符串填充通过
padStart
和padEnd
方式来进行前后填充
!第一个参数: 填充当前字符串后生成的字符串的长度。
如果此参数小于当前字符串的长度,则将按原样返回当前字符串
。
console.log("padStart", "Hello world".padStart(15, "*")) // padStart ****Hello world
console.log("padEnd", "Hello world".padEnd(15, "-")) // padEnd Hello world----
getOwnPropertyDescriptors
获取对象中所有属性描述符!
let __obj = {
name: "张三"
}
console.log("getOwnPropertyDescriptors", Object.getOwnPropertyDescriptors(__obj))
// {"name":{"value":"张三","writable":true,"enumerable":true,"configurable":true}}
Async Function
ES9相关
ES10相关
flat & flatMap
flat
对多维数组进行降维
操作,第二个参数可以设置降维的层次
!
console.log("flat", [12, 23, 52, [12, 34, 55], [22, 12, [23, 66]]].flat())
// [12,23,52,12,34,55,22,12,[23,66]] 看到会将二维数组进行降维
console.log("flat", [12, 23, 52, [12, 34, 55], [22, 12, [23, 66]]].flat(2))
// [12,23,52,12,34,55,22,12,23,66] 第二个参数设置降维的层次
flatMap
对多维数组进行降维
操作,并且可以通过回调函数,更加灵活的处理数据
!
const __arr = ["Hello World", "is it this~"]
const __newArr = __arr.flatMap(item => {
console.log("flatMap item", item)
return item.split(" "); // 这里分割后 [["hello","world"]] 二维数组, flatMap 会自动降维
})
console.log("__newArr", __newArr) // __newArr ["Hello","World","is","it","this~"]
fromEntries
fromEntries
可以将二维数组
转换为对象格式
!
let __obj = {
name: "张三",
age: 18
}
console.log(Object.entries(__obj)) // [["name","张三"],["age",18]]
console.log(Object.fromEntries(Object.entries(__obj))) // {"name":"张三","age":18}
查询字符串场景应用
let queryString = "https://www.baidu.com?name=akjdksa&age=18&id=982183921123"
let queryParam = new URLSearchParams(queryString.split("?")[1])
console.log("queryParam", Object.fromEntries(queryParam))
// {"name":"akjdksa","age":"18","id":"982183921123"}
trimStart & trimEnd
空格去除 首部 和 尾部
console.log("trim", " hello world ".trimStart().trimEnd())
ES11相关
BigInt
bigint
用来解决超出安全数字范围
,导致结果显示的问题
!
console.log("最大安全数据范围内 MAX_SAFE_INTEGER", Number.MAX_SAFE_INTEGER) // 9007199254740991
以上就是
安全整数范围
内,当数字大于该数字
时,且无法保证数字显示正确
问题!使用
bigInt时 数字后面加 n 标识这个数字为BigInt,否则当类型不一致时,BigInt 和 Number 累加会报错!
let bigInt = 9007199254740991n
// let addResult = 9007199254740991n + 2 // 错误的
let addResult = 9007199254740991n + 2n
let addResult2 = addResult + BigInt(2)
console.log("bigInt", addResult, addResult2)
空值运算符(??
)
当某个变量不存在时, 可以设置默认值!
let __a = 0
console.log(" 空值运算符 ", __a ?? "avc") // 0
console.log(" 空值运算符 ", __a || "avc") // avc
可选链运算符(?.
)
可选链操作符
,常用在对象获取某个属性
时,当这个对象不存在或者undefind时
,获取该对象属性就会避免报错
的问题!
undefined.a
VM509:1 Uncaught TypeError: Cannot read properties of undefined (reading 'a')
at <anonymous>:1:11
(匿名) @ VM509:1
使用可选链
undefined?.a // 避免报错,且不会调用一个不存在对象的属性
GlobalThis全局对象
兼容
浏览器
和node 环境
下获取全局对象的方法!
console.log("global", globalThis)
ES12相关
监听对象销毁
FinalizationRegistry
通过该类可以实现帮我们监听某个对象的销毁!
let testObj = {}
let final = new FinalizationRegistry(( value ) => {
console.log("监听到销毁的对象", this, value) // value => "params"
})
final.register(testObj, "params")
testObj = null
WeakRef弱引用
通过
weakRef
接受对象,可以返回一个弱引用
对象!通过
deref
方法可以获取target
对象!
let testObj = { name: "张三" }
let weakObj = new WeakRef(testObj).deref()
console.log(weakObj.name)
逻辑赋值运算
逻辑或运算(||=
)
当变量值为0时,
返回的是默认值
,因为0转换为布尔值为false!
let __message = undefined
// message = message || "default value!"
__message ||= "default value!"
console.log("__message", __message)
逻辑与运算(&&=
)
let bbb = {
name: "张三"
}
bbb &&= bbb.name
逻辑赋值运算(??=
)
当变量值为0时,
则返回0
,而不是返回默认值!
__message ??= "default value!"
对象属性监听
defineProperty
基本使用
Object.defineProperty
可以帮我们配置对象属性操作限制
,也可以通过get
和set
方法实现对对象属性的监听
!
defineProperty
是对原有目标对象属性进行操作和修改!
let __obj1 = {
name: "张三",
age: 18
}
Object.defineProperty(__obj1, "name", {
get() {
console.log("读取了 name 属性!")
},
set(val) {
console.log(`设置了 name 属性 ${val}`)
}
})
__obj1.name // 读取属性 触发get方法
__obj1.name = "李四" // 设置属性 触发set方法
监听多个属性
实现多个
对对象多个属性进行监听
Object.keys(__obj1).forEach(key => {
let value = __obj1[key]
Object.defineProperty(__obj1, key, {
get() {
console.log(`读取了 ${key} 属性! ${value}`)
return value;
},
set(val) {
console.log(`设置了 ${key} 属性 ${value}`)
value = val;
},
})
})
__obj1.name = "李四"
__obj1.name
__obj1.age = 22
__obj1.age
缺点
defineProperty
的设计初衷,是用来设置对象属性描述符的
!
1. 无法监听对象中新添加(obj.xxx = xxx)或者删除(delete obj.xx)的属性!
2. 无法监听数组中属性的变化!
proxy
proxy
是一个代理对象
,可以将目标对象生成一个代理对象
,对代理对象属性
进行访问修改操作
时,proxy内部会通过捕获器进行监听
!
在修改和访问对象属性时
,是对代理对象本身
的一个操作,其代理对象会间接性修改目标原有对象的属性!
基本使用
let __obj1 = {
name: "张三",
age: 18
}
let proxy_obj = new Proxy(__obj1, {
// get 捕获器
get(target, key) {
console.log(`get proxy obj ${target} ${key} ${target[key]}`);
return target[key];
},
// set 捕获器
set(target, key, value) {
console.log(`set proxy obj ${target} ${key} ${value}`);
target[key] = value;
}
});
proxy_obj.name
proxy_obj.age
proxy_obj.age = 22
proxy_obj.age
捕获器
除了
get
和set
捕获器
以外 还有11 中捕获器
!操作的是
代理对象
,而不是目标对象
!
in操作符监听
"name" in proxy_obj
// in 操作符监听
has(target, key) {
console.log(`监听了 in 操作 ${target[key]} ${key}`)
return key in target;
},
delete操作监听
delete proxy_obj.age
// delete 操作符监听
deleteProperty(target, key) {
console.log(`监听了 delete 操作 ${target[key]} ${key}`)
return target[key];
}
对函数监听
proxy
可以对函数的一个监听
,如apply
和new
操作符!
function __foo() {
}
let proxy_fun = new Proxy(__foo, {
apply(target, thisArgs, argArr) {
console.log(`apply 对 函数调用的监听 ${thisArgs} ${argArr}`)
return target.apply(thisArgs, argArr)
},
// 构造函数 对 new 操作符的一个监听
construct(target, argArr) {
console.log(`construct 对 new 操作符的监听 ${argArr}`)
return new target(...argArr);
}
})
proxy_fun.apply({}, ['name', 'id'])
new proxy_fun("张三");
receiver 参数的作用
receiver
是proxy代理对象
中捕获器函数中的一个参数
,它其实就是proxy的一个实例
receiver
可以改变目标对象this指向
,以确保目标对象能够正确指向代理对象
!
let __obj2 = {
_name: "王五",
age: 22,
get name(){
// 确保 this 指向的是 proxy 而不是 obj ,若是 obj 则代理对象且无法触发监听作用!
return this._name;
},
set name( val ){
this._name = val;
}
}
let proxy_obj2 = new Proxy(__obj2, {
get(target, key, receiver) {
// Reflect get: {"name":"王五","age":22} name
console.log("Reflect get: ", target, key)
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log("Reflect set: ", target, key, value)
let isSet = Reflect.set(target, key, value, receiver)
if (isSet) {
// 设置成功
}
},
})
proxy_obj2.name
proxy_obj2.name = "齐六"
Reflect
Object对象的一个规范化
,基本Object对象
所拥有的方法,Reflect对象上也会有
,也是与proxy捕获器对应的方法一一对应
!
Reflect
主要用于Proxy
的handler
对象中,它允许你定义如何拦截和修改对象的操作。
let __obj2 = {
name: "王五",
age: 22
}
let proxy_obj2 = new Proxy(__obj2, {
get(target, key) {
// Reflect get: {"name":"王五","age":22} name
console.log("Reflect get: ", target, key)
return Reflect.get(target, key);
},
set(target, key, value) {
// Reflect set: {"name":"王五","age":22} name 齐六
console.log("Reflect set: ", target, key, value)
let isSet = Reflect.set(target, key, value)
if (isSet) {
// 设置成功
}
},
})
proxy_obj2.name
proxy_obj2.name = "齐六"
construct
Reflect.construct
可以将子类的原型替换成父类!
function Student1(name, age) {
this.name = name;
this.age = age;
}
function Teacher1() {
}
Teacher1.prototype.teaching = function () {
console.log("teaching~")
}
// 执行 student 函数 生成 teacher 对象
let teacher1 = Reflect.construct(Student1, ['张三', 12], Teacher1)
console.log(" Reflect.construct", teacher1.__proto__ === Teacher1.prototype) // true
teacher1.teaching()
/*Teacher1*/ {
"name": "张三",
"age": 12
}
响应式
所谓的
响应式
,就是对象属性值发生变化时
,会自动触发更新逻辑处理
!
vue3 响应式
响应式对象
通过
proxy
将普通对象转换为代理对象
,通过捕捉器监听属性
的变化!
// 响应式对象
const refObj = {
name: "张三", // depend
age: 22 // depend
}
// 响应式 代理对象
const proxyObj = new Proxy(refObj, {
// 收集依赖
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
},
// 派发更新
set(target, key, value, receiver) {
// 根据对象 和 对象属性获取依赖对象
const depend = getDepend(target, key);
console.log("触发更新 set depend", depend.reactives)
Reflect.set(target, key, value, receiver);
// 派发更新
depend.notify();
},
})
依赖类封装(Depend)
// 依赖收集对象
class Depend {
constructor() {
// 收集响应式函数
this.reactives = []
}
// 依赖收集
addDepend(fun) {
this.reactives.push(fun);
console.log("addDepend this.reactives", this.reactives, fun)
}
// 派发通知触发更新
notify() {
this.reactives.forEach(fun => {
// 执行响应式函数
fun();
})
}
}
监听函数
监听函数
watchFun
,当对象属性发生变化
时,watchFun会将响应式函数添加到依赖Deped对象属性集合
中!
let activeFunction = null
// 响应式函数封装
function watchFun(fun) {
activeFunction = fun;
fun(); // 执行 fun 是为了函数体内访问了代理对象,当执行后会被代理对象捕获器捕获,然后进行依赖收集!
activeFunction = null;
}
对象依赖结构存储
每一个
对象的属性都对应一个依赖函数
,当对象属性触发更新
时,则会找到对象属性与之对应的依赖对象
进行数据更新
!将对象中的每一个
属性
和依赖
对象通过Map结构
进行存储!随后通过
WeakMap
将对象作为key
而 存储过属性和依赖的Map作为值
进行存储!之后就可以通过 获取
对象
在获取属性
且对应的依赖进行依赖更新
!const depend = weakMap.get(obj).get("name")
let obj = {
name: "张三", // depend 依赖对象
age: 22 // depend 依赖对象
}
const objMap = new Map();
objMap.set("name", "nameDependObj");
objMap.set("age", "ageDependObj");
let info = {
address: "北京市" // depend 依赖对象
}
const infoMap = new Map();
infoMap.set("address", "addressDependObj");
// 将 obj 和 info 对象与之对应的 Map 存储在 weakMap 中,
const weakMap = new WeakMap();
weakMap.set(obj, objMap);
weakMap.set(info, infoMap);
const depend = weakMap.get(obj).get("name")
depend.notify(); // 找到对应对象的对应属性获取依赖对象进行派发更新!
完整的代码
// 响应式对象
/* const refObj = {
name: "张三", // depend
age: 22 // depend
} */
let activeFunction = null
// 依赖收集对象
class Depend {
constructor() {
// 收集响应式函数
this.reactives = new Set();
}
addDepend(fun) {
this.reactives.add(fun);
}
depend() {
if (activeFunction) {
this.reactives.add(activeFunction);
}
}
// 派发通知触发更新
notify() {
this.reactives.forEach(fun => {
// 执行响应式函数
fun();
})
}
}
// 响应式函数封装
function watchFun(fun) {
activeFunction = fun;
fun(); // 执行 fun 是为了函数体内访问了代理对象,当执行后会被代理对象捕获器捕获,然后进行依赖收集!
activeFunction = null;
}
function reactive(obj) {
return new Proxy(obj, {
// 收集依赖
get(target, key, receiver) {
const depend = getDepend(target, key);
// 把收集好的响应式函数集中到依赖对象里
activeFunction && depend.addDepend(activeFunction);
// depend.depend();
return Reflect.get(target, key, receiver);
},
// 派发更新
set(target, key, value, receiver) {
// 根据对象 和 对象属性获取依赖对象
const depend = getDepend(target, key);
// console.log("触发更新 set depend", depend.reactives)
Reflect.set(target, key, value, receiver);
// 派发更新
depend.notify();
},
})
}
const proxyObj = reactive({
name: "张三", // depend
age: 22 // depend
});
const proxyObj2 = reactive({
address: "北京市", // depend
});
// 响应式 代理对象
/* const proxyObj = new Proxy(refObj, {
// 收集依赖
get(target, key, receiver) {
const depend = getDepend(target, key);
// 把收集好的响应式函数集中到依赖对象里
depend.addDepend(activeFunction);
return Reflect.get(target, key, receiver);
},
// 派发更新
set(target, key, value, receiver) {
// 根据对象 和 对象属性获取依赖对象
const depend = getDepend(target, key);
console.log("触发更新 set depend", depend.reactives)
Reflect.set(target, key, value, receiver);
// 派发更新
depend.notify();
},
}) */
/*
获取依赖函数
target: 目标对象
key: 目标对象属性
desc: watchFun 内部响应式函数 不清楚是哪个对象或者是哪个对象中的属性,由此以来,在触发更新时无法准确去触发依赖更新!
所以我们需要将 每个对象中的属性 和 对应的 属性依赖更新通过 map形式绑定!
之后通过 weakMap 将 对象 与 map 进行绑定
*/
const targetMap = new WeakMap();
function getDepend(target, key) {
// 根据obj 获取 map
let map = targetMap.get(target);
// 如果 weakMap中 根据目标对象target 没有获取到 对应map对时候,就新建一个map 存放在 weakMap中
if (!map) {
map = new Map();
targetMap.set(target, map)
}
// 如果 Map 中 根据对象属性key 去获取 depend 依赖函数时 没获取到
let depend = map.get(key)
if (!depend) {
// 创建依赖函数
depend = new Depend();
// 存放到 Map 结构中
map.set(key, depend)
}
return depend;
}
// 监听响应式函数 将响应式函数添加到 depend 依赖收集类中
watchFun(function () {
console.log(`name 属性触发了更新 ${proxyObj.name}`)
})
watchFun(function () {
console.log(`address 属性触发了更新 ${proxyObj2.address}`)
})
proxyObj.name = "李四"
proxyObj2.address = "青岛"
封装proxy
const refObj = {
name: "张三", // depend
age: 22 // depend
}
function reactive(obj) {
return new Proxy(refObj, {
// 收集依赖
get(target, key, receiver) {
const depend = getDepend(target, key);
// 把收集好的响应式函数集中到依赖对象里
depend.addDepend(activeFunction);
return Reflect.get(target, key, receiver);
},
// 派发更新
set(target, key, value, receiver) {
// 根据对象 和 对象属性获取依赖对象
const depend = getDepend(target, key);
console.log("触发更新 set depend", depend.reactives)
Reflect.set(target, key, value, receiver);
// 派发更新
depend.notify();
},
})
}
// const proxyObj = reactive(refObj);
const proxyObj = reactive({
name: "张三", // depend
age: 22 // depend
});
Vue2 响应式
vue2
使用的是Object.defineProperty
来监听对象属性变化!需要
遍历对象所有属性
,并且添加getter
和setter
方法,然后进行依赖收集和派发更新
!
function reactive(obj) {
Object.keys(obj).forEach(key=> {
let value = obj[key]
Object.definePropertie(obj, key, {
get(){
// 依赖收集
const depend = getDepend(obj, key);
depend.depend();
return value;
},
set( val ){
// 派发更新
value = val;
const depend = getDepend(obj, key);
depend.notify();
}
})
})
return obj;
}
总结
响应式概念:
当对象中某一属性值发生变化
时,会触发一系列自动更新
操作!
vue3中响应式原理:
vue3
中的响应式
是通过proxy 代理对象
实现的,proxy
代理对象内部有13种捕获器
,其中get
和set
方法主要做了对象属性上的依赖收集
与派发更新
!
vue2中响应式原理:
vue2
中的响应式
,主要是通过Object.defineProperty
来实现的,该方法是用来定义对象属性描述
的,其中也是通过getter
和setter
进行对属性依赖收集
和派发更新
!
Object.defineProperty
是有缺陷的,无法对对象新增
的属性或删除的属性
进行监听,同样数组
也是!
JS进程线程
操作系统进程和线程
在
操作系统
中,每打开一个应用
,且都是一个进程
,线程则是进程的一个子集
!而
每一个进程中
,至少会有一个线程在执行
,这个线程则称为主线程
!
通俗的讲,比如有
一个工厂
,则工厂有很多车间
,而每个车间至少会有一个工人以上在处理自己的任务
!
浏览器JS中的进程和线程
1. js 是单线程
,意味着代码只能一行一行执行
,若在代码中出现比较耗时的逻辑
,则会导致线程被阻塞的问题!
2. 浏览器是多进程
,且打开一个tab则会多一个进程
,每一个进程下会有多个线程
,其中包含js线程
!
3. js中比较耗时
的操作,一般都是浏览器帮忙处理
,如网络请求 定时器
等!
4. 网络请求
,浏览器会单独一个线程向服务端发送请求
,请求完毕后,会调用回调函数
将结果返回!
事件循环
1. 事件循环
: 就是js 线程
->其它线程
->事件队列
->js引擎
一个闭环
!
2. js线程:
解析代码遇到setTimeout(()=> {}, 1000), js线程本身不会记时
,会将回调函数通过其它线程
方式来处理!
3. 其它线程:
会将每个回调函数放入事件队列中!
4. 事件队列:
特点便是先进先出
,当队列中的回调函数处理完毕
时,会交给js线程进行执行
!
5. js线程:
从事件队列中取出
已经处理完毕的回调函数并执行
!
在执行代吗中,会碰到
定时器
网络请求
或者是promise.then
以及用户操作事件
等,这些操作都会导致耗时
问题,比如
定时器
,1000毫秒
的时间不可能由js单线程
来完成这个记时操作,否则这个线程会阻塞导致其它代码无法执行,
所以浏览器会通过另一个线程
来完成记时操作
,完成后会通过回调函数
的方式,来表示这个记时操作完成
了!同样
网络请求
也是,网络请求会建立连接
传输数据
断开链接
,那么当浏览器请求从服务端获取到数据
时,会调用回调函数
并表示请求操作已完成
!
宏任务和微任务
宏任务
:setTimeout
、setInterval
、setImmediate
、I/O
、UI rendering
微任务
:Promise.then
、process.nextTick
、MutationObserver
queueMicrotask
在事件队列中,分为
宏任务队列
和微任务队列
,且在执行宏任务队列
时,前提微任务队列以被清空
的情况下,才能执行
!