JavaScript高级进阶(更新中)

JavaScript 高级
This 指向规则
案例
function foo() {
console.log(this)
}
// 1 调用方式1
foo();
// 2 调用方式2 放入对象中调用
var obj = {
name: "why",
foo: foo
}
obj.foo()
// 调用方式三 通过 call/apply 调用
foo.call("abc")
指向定义
this 是js 给函数的一个绑定值。
- 函数在调用时 JavaScript会默认给this绑定一个值;
- this的绑定和定义的位置(编写的位置)没有关系;
- this的绑定和调用方式以及调用的位置有关系
- this是在运行时被绑定的
无严格模式下 为 window 如果打开严格模式 则为 udnefined
this 的绑定规则如下:
-
绑定一:默认绑定 PS: 没有绑定到任何对象时 & 函数定义在对象中但是被独立调用 对象也是 window
-
绑定二:隐式绑定 PS:由JS 绑定到调用对象 指向对象
-
绑定三:new绑定
- new 执行过程
- 1 创建空对象
- 2 修改this 指向为空对象
- 3 执行函数体代码
- 没有显示返回非空对象时 默认返回这个对象
-
绑定四 显示绑定
-
如果我们不希望在 对象内部 包含这个函数的引用,同时又希望在这个对象上进行强制调用
-
function foo() { console.log(this) } var obj = { name: "why", foo: foo } foo.call(123) console 输出内容 {name: 'why', foo: ƒ}
-
call/apply 可以帮助我们完成这个效果
-
额外函数补充
Call / Apply 调用方法 两者区别不大 但是又细微差别
apply
:
function foo(name, age, height) {
console.log("foo 函数this 指向", this);
console.log("参数:", name, age, height);
}
// 普通调用 直接入参
foo("why", 18, 1.22)
// apply
// 第一个参数 绑定 this
// 第二个参数 传入额外的实参 以数组的形式
// foo.apply("apply",["why", 18, 1.22])
foo.apply("123", ["why", 18, 1.22])
call
:
function foo(name, age, height) {
console.log("foo 函数this 指向", this);
console.log("参数:", name, age, height);
}
// call
// 第一个参数 绑定 this
// 后续参数以 参数列表形式
foo.call("call", "远目鸟", 18, 12)
两者 相同处 都是调用方法 第一参数都指向this 唯一区别只在后续传入的参数的形势
- apply 为数组
- call 为列表 以
,
分割
bind
:会创建 绑定函数 我们希望调用foo 的时候总是让this 指向 obj
function foo() {
console.log("foo 函数this 指向", this);
}
var obj = {
name: "why"
}
// 需求 调用foo时 总是绑定 obj
var bar = foo.bind(obj)
bar()
在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。 实际开发中 使用不多 作为参考 了解即可
内置函数
一般 对于浏览器 的内置函数 或者是第三方框架的 this 指向 我们只能用经验去判断 一个一个去源码或者文档查看和并不现实
This 优先级
- 默认绑定 优先级最低
- 显式绑定 高于隐式绑定
- new 高于隐式绑定 PS:new不能和 call/apply 一起使用
- new绑定优先级高于bind
- 同显式 bind 优先级高于 call/apply
拓展: 规则之外
**情况一:**如果在显示绑定中,我们传入一个null或者undefined,那么这个显示绑定会被忽略,使用默认规则:
function foo() {
console.log("foo 函数this 指向", this);
}
var obj = {
name: "why"
}
foo.call(obj)
foo.call(null)
foo.call(undefined)
var bar = foo.bind(obj)
bar()
但是打开严格模式 就会可以使用基础属性 直接显示 null 或者 undefined
**情况二:**创建一个函数的 间接引用,这种情况使用默认绑定规则。
- 这种情况 (obj2.foo = obj1.foo) 会使用默认规则 指向 window
var obj1 = {
name: "obj1",
foo: function () {
console.log("foo 函数this 指向", this);
}
}
var obj2 = {
name: "obj2"
};
obj1.foo();
(obj2.foo = obj1.foo)();
**情况三:**箭头函数
- 箭头函数不会绑定this、arguments属性
- 箭头函数不能作为构造函数来使用
// {} 是执行体
var arrFn = () => { }
// 指向的是对象 需要加小括号才可以做到
var arrFn = () => ({ name: "why" })
箭头函数
-
基本写法
-
():函数的参数
-
{}:函数的执行体
-
var foo3 = (name, age) => { console.log("箭头函数的函数体") console.log(name, age) }
-
-
优化写法
-
只有一个参数时, 可以省略()
names.forEach(item => { console.log(item) })
-
只有一行代码时, 可以省略{}
names.forEach(item => console.log(item))
-
只要一行代码时, 表达式的返回值会作为箭头函数默认返回值, 所以可以省略return
var newNums = nums.filter(item => item % 2 === 0) var newNums = nums.filter(item => item % 2 === 0)
-
如果箭头函数默认返回的是对象, 在省略{}的时候, 对象必须使用()包裹 () => ({name: "why"})
var arrFn = () => ["abc", "cba"] var arrFn = () => {} // 注意: 这里是{}执行体 var arrFn = () => ({ name: "why" }) console.log(arrFn())
-
箭头函数不使用this的四种标准规则(也就是不绑定this),而是根据外层作用域来决定this。
我们来看一个模拟网络请求的案例:
- 这里我使用setTimeout来模拟网络请求,请求到数据后如何可以存放到data中呢?
- 我们需要拿到obj对象,设置data;
- 但是直接拿到的this是window,我们需要在外层定义:var _this = this
- _在setTimeout的回调函数中使用_this就代表了obj对象
- 但是如果使用箭头函数根据特性他会向上寻找this 省去了_this = this的操作
var obj = {
data: [],
getData: function () {
request("/11", (res) => {
this.data = [].concat(res)
})
}
}
function request(url, callbackFn) {
var res = ["abc", "cba", "nba"]
callbackFn(res)
}
obj.getData()
总结
- this的指向问题与优先级 是踏入JS的敲门砖,如果不先系统了解之后使用的时候可能会出现奇怪的错误
- 使用ES6的语法 箭头函数 提前熟悉ES6语法可以提升开发效率
浏览器渲染原理
我们通过url 进入到页面拿到资源以及获得返回资源的过程
浏览器内核
常见的浏览器内核有
- Trident ( 三叉戟):IE、360安全浏览器、搜狗高速浏览器、百度浏览器、UC浏览器;
- Gecko( 壁虎) :Mozilla Firefox;
- Presto(急板乐曲)-> Blink (眨眼):Opera
- Webkit :Safari、360极速浏览器、搜狗高速浏览器、移动端浏览器(Android、iOS)
- Webkit -> Blink :Google Chrome,Edge
页面渲染流程:
浏览器的渲染页面过程
HTML解析过程
一般情况下服务器会给浏览器返回 xx.html 文件 解析html 其实就是 Dom 树的构建过程
我们可以根据以下html 结构 来简单的分析出 html 的解析过程
解析CSS 规则树
在解析的过程中,如果遇到CSS的link元素,那么会由浏览器负责下载对应的CSS文件:
PS: 这里下载 CSS 是不会影响到 DOM树的解析的
下载完成后 就会对CSS 文件解析出对应的 规则树 , 案例如下图 :
body{font-size: 16px}
p{font-weight: bold}
span{color: red}
p span{display:none}
img{float: right}
解析步骤 构建 Render Tree
当有了DOM Tree和 CSSOM Tree后,就可以两个结合来构建Render Tree了
需要注意的是:
- link元素不会阻塞DOM Tree的构建过程,但是会阻塞Render Tree的构建过程
- Render Tree和DOM Tree并不是一一对应的关系,比如对于display为none的元素,压根不会出现在render tree中;
解析步骤 布局和绘制
- 渲染树(Render Tree)上运行布局(Layout)以计算每个节点的几何体。
- 渲染树会表示显示哪些节点以及其他样式,但是不表示每个节点的尺寸、位置等信息;
- 布局是确定呈现树中所有节点的宽度、高度和位置信息;
- 将每个节点绘制(Paint)到屏幕上
- 在绘制阶段,浏览器将布局阶段计算的每个frame转为屏幕上实际的像素点;
- 包括将元素的可见部分进行绘制,比如文本、颜色、边框、阴影、替换元素(比如img)
渲染的流程可以参考下图 :
完成以上五步 成功在浏览器渲染出 对应的 xx.html 文件
回流和重绘
回流(reflow)
reflow
:
- 我们渲染出来的节点大小位置 也就是布局时第一次渲染出之后就确定的
- 之后对于节点大小和位置重新计算的行为 叫做回流(reflow)
回流在什么时候会出现 :
- DOM 结构发生变化 (添加 & 移除)
- 改变了 CSS 样式代码 也就是布局
- 修改了 窗口尺寸
- 或者是调用了某些内置函数 获取位置和尺寸信息
重绘 (reprint)
- 我们渲染的第一次,在之前的流程图中叫做 ==printing==
- 在之后需要重新渲染的时候 成为重绘
重绘怎么出现 :
- 修改CSS 如 颜色 文字样式
拓展思路
- 只要出现回流 就一定会引起重绘 其实看到上述的解释 也很容易就发现 回流也是在出发样式代码或者改变的时候触发
- 回流的性能并不好 也很明显 重新渲染整个DOM 很浪费性能
总结
- 修改样式 尽可能减少回流次数 也就是设计好之后,非必要不去改动样式和DOM的结构
- 避免频繁使用 JS 去操作DOM
- 尽可能减少函数获取储存位置的信息
特殊解析 - composite合成
绘制的过程,可以将布局后的元素绘制到多个合成图层中。
会形成新的合成层的属性:
- 3D transforms
- video、canvas、iframe
- opacity 动画转换时
- position: fixed
- will-change
- animation 或 transition 设置了opacity、transform
PS:分层确实可以提高性能,但是它以内存管理为代价,所以不作为性能优化策略来使用
script元素和页面解析的关系
JS 在我们渲染过程中的那一步呢?
- 在渲染html的时候 js 没有继续构造DOM的能力
- 如果需要需要的部分 会先停止构建,下载js 执行脚本
- 把需要构建的东西构建完成后 继续执行构建 DOM
这么做有什么好处?
- JS 有操作和修改DOM的作用
- 为什么会先去执行js脚本? 因为之前提到了 回流时很吃性能的所以最好一次性弄好 减少不必要的回流
代码案例
index.html
<script src="./js/test.js"></script>
<body>
<div class="box"></div>
</body>
<script>
var boxel = document.getElementsByClassName("box")
console.log(boxel);
</script>
test.js
debugger
console.log("hello")
新的问题:
- 在现在的开发模式中 大多都是使用vue和React 作为开发框架 JS 的占比往往很大 处理事件也会变长
- 这也导致了 如果解析阻塞 那么在脚本解析完成之前 可能界面什么都不显示
这里 js 给我们提供了两个属性 来解决这个问题
defer属性
defer 属性告诉浏览器不要等待脚本下载,而继续解析HTML,构建DOM Tree,如果脚本提前下载好就等待加载,等DOM完成 在触发DOMContentLoaded之前执行defer中的代码
PS: defer 按照默认顺序执行 不会影响顺序 且可以操作DOM
<script>
window.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded");
})
</script>
<script>
var boxel = document.getElementsByClassName("box")
console.log(boxel);
</script>
<script defer>
console.log("defer-demo")
</script>
<script>
debugger
console.log("hello")
</script>
建议:
- 将defer放入head中使用 这个属性的特性放在末尾 就本末倒置了
- defer 只对外置脚本有效果
async属性
async 特性与 defer 有些类似,它也能够让脚本不阻塞页面。
它的特性:
- 浏览器不会因 async 脚本而阻塞(与 defer 类似);
- async脚本不能保证顺序,它是独立下载、独立运行,不会等待其他脚本
- async不会能保证在DOMContentLoaded之前或者之后执行
<script>
window.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded");
})
</script>
<script async>
console.log("defer-demo")
</script>
总结
- defer 通常用于文档解析操作DOM且有顺序要求的JS代码
- async 通常用于独立脚本 可以理解为没有什么依赖的脚本 如果有依赖 那么不保证一定能提前加载到
总结
- 首先时了解和认识一些浏览器的内核
- 了解从服务器加载 到渲染页面的流程
- 细化每一步的大致内容
- 发现有问题且探索到问题的一些解决方法
JS运行原理
深入了解V8引擎原理
浏览器内核是由两部分组成的,以webkit为例:
- WebCore:负责HTML解析、布局、渲染等等相关的工作;
- JavaScriptCore:解析、执行JavaScript代码;
官方对V8引擎的定义:
- V8是用C ++编写的Google开源高性能JavaScript和WebAssembly引擎,它用于Chrome和Node.js等
- 它实现ECMAScript和WebAssembly,并在Windows 7或更高版本,macOS 10.12+和使用x64,IA-32,ARM或MIPS处理
器的Linux系统上运行。 - V8可以独立运行,也可以嵌入到任何C ++应用程序中。
V8引擎的架构很复杂 ,我们可以先了解它庞大引擎的一些模块
- Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码
- 如果函数没有被调用,那么是不会被转换成AST
- Parse的V8官方文档:https://v8.dev/blog/scanner
- Ignition是一个解释器,会将AST转换成ByteCode(字节码)
- 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算)
- 如果函数只调用一次,Ignition会解释执行ByteCode
- Ignition的V8官方文档:https://v8.dev/blog/ignition-interpreter
- TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码
- 如果一个函数被多次调用,那么就会被标记为热点函数,它会被TurboFan转换成优化的机器码,提高代码的执行性能
- 机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执
行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码 - TurboFan的V8官方文档:https://v8.dev/blog/turbofan-jit
V8架构解析图 来自官方
解析代码的步骤:
- 获得到代码之后 V8用流输入通过词法分析,分析成token
- 解析/预解析 来生成一个一个执行节点
- 生成 AST 树
- 转成字节码 如果有热点方法就会走turbofan编译器优化成机械码提升性能
全局代码执行过程
js引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)
- 该对象 所有的作用域(scope)都可以访问
- 里面会包含Date、Array、String、Number、setTimeout、setInterval等等
- 其中还有一个window属性指向自己
js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈,
他执行的式全局代码块,它的作用就是:
- 为了执行代码构建一个 Global Execution Context GEC 全局上下文
- 将这个构建的上下文加入到执行栈中 也就是将 GEC 放入 ECS中
GEC被放入到ECS中里面包含两部分内容:
- 在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值
- 在代码执行中,对变量赋值,或者执行其他的函数;
每一个执行上下文会关联一个VO(Variable Object,变量对象),变量和函数声明会被添加到这个VO对象中,当全局代码被执行的时候,VO就是GO对象了
全局上下文三个关键:
- VO(go)
- 作用域链
- This
执行以下代码过程
var message = "Global Message"
function foo() {
var message = "Foo Message"
}
var num1 = 10
var num2 = 20
var res = num1 + num2
console.log(res);
全局代码执行前
执行代码后
函数代码执行过程
在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context,简称FEC),并且压入到EC Stack中
- 当进入一个函数执行上下文时,会创建一个AO对象(Activation Object)
- 这个AO对象会使用arguments作为初始化,并且初始值是传入的参数
- 这个AO对象会作为执行上下文的VO来存放变量的初始化
如下函数执行过程
执行前
执行后
流程为:
- 执行前创建FEC 也就是函数执行上下文
- 创建 AO 对象 name为函数名
- 创建作用域链
- 生成函数对象存放代码
- thisbing(暂无)
- 之后从上到下执行代码
- 执行完成后将name 变为 undefined
作用域和作用域链
当进入到一个执行上下文时,执行上下文也会关联一个作用域链(Scope Chain)
- 作用域链是一个对象列表,用于变量标识符的求值
- 当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象
PS : 作用域会提升 在本身vo没有情况下 会去上层寻找,我们先输出后声明会输出undefined, 这里也印证了
作用域提升小练习
var n = 100
function foo(){
n=200
}
foo()
console.log(n)
N =200
顺序内存查找图如下 :
- 全局代码创建函数 找到 n放入到函数vo中 之后调用foo()
- 在函数调用后找到GO中的n复制
- 函数结束,之后输出n
作用域链也是我们JS闭包的一个重点, js中闭包就是通过作用域链的方式来完成变量可以跨作用域访问的,为我们加快提升了开发的效率 也省去很多麻烦
JS内存管理
内存原理:
任何变成语言在执行的时候都需要操作系统来分配内存,只是有些语言需要手动管理分配的内存有些语言有专门来管理内存的方式 如 JVM
了解以上的概念之后,我们再来了解一下大致的内存周期
- 分配需要的内存
- 使用内存
- 在不使用的时候释放内存
JS 属于自动管理内存的语言
在我们定义数据的时候 JS 会给我们分配内存,但是内存分配的方式有区别
- 对于原始数据内存分配在执行的时候 直接放在栈空间进行分配
- 对于复杂的数据类型 会在堆内存中开辟一块空间 并且将这块空间的指针返回值变量引用
垃圾回收机制算法
概念:
因为内存的大小是有限的,所以当内存不再需要的时候,我们需要对其进行释放,以便腾出更多的内存空间。
对比手动管理内存释放语言 对于开发者的技术要求非常高,一旦操作不但 效果反而会变得很差,这个也形成了高手可以做到性能很高 但是苦于进阶的选手,所以现在大部分高级语言都实现了GC也就是垃圾回收机制/垃圾回收算法
GC怎么知道哪些对象是不再使用的呢?
对于GC的实现是百花齐放的 设计语言的人总能整出花活,这里介绍几个常见的GC算法
常见GC - 引用计数(Reference counting)
- 当一个对象有一个引用指向它时,那么这个对象的引用就+1;
- 当一个对象的引用为0时,这个对象就可以被销毁掉;
PS: 这个算法的弊端就是会产生循环引用 就是加入 a b之间互有属性引用 会出现两个对象哦都无法销毁的问题
常见的GC算法 – 标记清除(mark-Sweep)
这个算法的核心思想是实现可达性
设置一个根对象(root object),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于哪些没有引用到的对象,就认为是不可用的对象
PS:这个算法可以很好的解决循环引用的问题
- 他会从一个根对象去不断查找确认查找之后就会标记对象
- 如果发现找不到 就等于无法引用 那么就会去销毁(如下图)
- 前提是 RO 对象不会被删除 其实就代表我们 js 中的 window对象
拓展
其他的GC算法
- 标记整理算法(Mark-Compact) 回收的时候保留存储对象搬运到灰级连续的内存空间,整合空闲空间,避免内存碎片化
- 分代收集(Generational collection) 对象分为旧 新 两组 有很多对象在完成工作后就会销毁 长期存活的对象变为
老旧
同时他们的检查频次不会那么频繁 - 增量收集(Incremental collection)
- 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟
- 所以引擎试图将垃圾收集工作分成几部分来做,然后将这几部分会逐一进行处理,这样会有许多微小的延迟而不是一个大的延迟
- **闲时收集(Idle-time collection)**垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。
闭包概念
闭包是JavaScript中一个非常容易让人迷惑的知识点
JS 作为高级语言 是支持函数式编程的,这意味着在js中
- 函数操作和使用都非常灵活
- 函数可以作为另外一个函数的参数,也可以作为另外一个函数的返回值来使用
所以JavaScript存在很多的高阶函数,我们可以自己编写高阶函数,也可以使用内置的函数
在未来开源框架中也都是趋向于函数式编程
闭包的定义
- 最早出现的闭包是 Scheme
- 闭包实际上是一种存储了函数和关联环境的结构体
- 他和函数最大的区别就是闭包被捕捉的时候,他的自由变量会被锁定 即使脱离了捕捉时的上下文也可以照常运行
他的作用就是让我们可以在函数中访问到外围的变量,替我们省去了很多繁杂的变量处理
闭包小案例
function createAdder(count){
funtion adder(num){
return count+num
}
return adder
}
var adder5 = createAdder(5)
adder(100) // 100+5
这个例子可以很容易的看出闭包的使用和带来的好处
PS: 使用闭包的时候最好是可以将不需要的函数或者属性置为 null 来帮助GC回收释放对象 ,否则内存泄露会加大内存的占用
浏览器对于闭包的优化: 使用闭包的时候 浏览器会将我们没有使用的多余属性释放来增加性能
JS增强
JS函数增强
函数属性
JavaScript中函数也是一个对象,那么对象中就可以有属性和方法,他有一些默认的属性
- name 函数名
- length 函数参数个数(ES6
...
语法不会被算在内) - arguments 类似数组对象 可以i用索引来获取对象
- rset
PS: 箭头函数不绑定 Arguments 对象
arguments 转为数组对象常见方法
普通的方法 就是将内容一个一个迭代到新数组了
let newArray = []
// arguments
function foo1(m, n) {
for (var arg of arguments) {
newArray.push(arg)
}
// arguments类似数组的对象(它可以通过索引来获得对象)
console.log(newArray)
}
foo1(1, 2)
ES6 中的方法
- Array.form() 传入一个可迭代对象就可以转为数组
- 对象结构
...
的方式来复制
// 方法2
var newArray1 = Array.from(arguments)
// 方法3
var newArray = [...arguments]
rset
如果最后一个参数是 ... 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组
function foo1(m, n, ...arg)
- arguments 对象包含了传给函数的所有实参但是不是数组对象 需要转换
- rest参数是一个真正的数组,可以进行数组的所有操作
- arguments是早期为了方便去获取所有的参数提供的数据结构,rest参数是ES6中提供并且希望替代arguments的方案
纯函数理解和应用
副作用:
执行函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储
纯函数的理解
- 输入 相同值的时候产生同样的输出 所以纯函数不能通过闭包的特性调用上层属性,因为会随着上层属性变化函数输出内容
- 函数的输出和输入值以外的信息无关和设备的外部输出也无关
- 这个函数不能有语义上可观察到的 “副作用”
纯函数辨别案例
- slice:slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组
- splice:splice截取数组, 会返回一个新的数组, 也会对原数组进行修改
var names = ["abc", "nba", "nbc", "cbd"]
var newNames = names.slice(0, 2)
var newNames1 = names.splice(0, 2)
console.log(newNames);
console.log(newNames1);
纯函数的优势
- 稳定,可以放心使用
- 保证函数的纯度 简单的实现自己的业务逻辑,和外置的各种因素依赖关系少
- 用的时候需要保证输入的内容不被任意篡改,并且需要确定输入一定会有确定的输出
柯里化的理解和应用
函数式编程重要概念,他是一个作用于函数的高阶技术,在其他的编程语言也有使用
只传递函数部分参数来调用,让它返回一个函数去处理剩余的参数这个过程就被成为柯里化
// 普通的函数
function foo(x, y, z) {
console.log(x + y + z);
}
foo(10, 20, 30)
// 柯里化的结果
function kelifoo(x) {
return function (y) {
return function (z) {
console.log(x + y + z);
}
}
}
kelifoo(10)(20)(30)
//箭头函数写法
var foo2 = x => y => z => { console.log(x + y + z) }
自动柯里化函数
// 需要转化的例子
function sum(num1, num2) {
console.log(num1 + num2);
return num1 + num2
}
// 自动柯里化函数
function hyCurrying(fn) {
// 1 继续返回一个新的函数 继续接受函数
// 2 直接执行 fn 函数
function curryFun(...args) {
if (args.length >= fn.length) {
// 执行第二种操作
return fn.apply(this, args)
} else {
return function (...newArgs) {
return curryFun.apply(this, args.concat(newArgs))
}
}
}
return curryFun
}
// 对其他函数柯里化
var sumCurry = hyCurrying(sum)
sumCurry(10)(5)
sumCurry(10, 5)
柯里化函数只有在某些特殊的场景才需要使用。他得性能并不高也可能引起闭包的内存泄漏所以使用的时候需要注意。
组合函数理解和应用
当我们需要嵌套调用两个函数的时候,为了方便复用,我们可以写一个组合函数
var sum = pow(double(12))
我们可以编写一个通用的组合函数来让我们使用组合函数更加的便捷,其实思路就是很简单的将函数放入数组判断边界顺序执行
function sum(num) {
return num * 2
}
function pow(num) {
return num ** 2
}
function composeFn(...fns) {
// 边界判断
var length = fns.length
if (length < 0) {
return
}
for (let i = 0; i < length; i++) {
var fn = fns[i]
if (typeof fn != "function") {
throw new Error(`index postion ${i} must be function`)
}
}
//轮流执行函数 返回结果对象
return function (...args) {
var result = fns[0].apply(this, args)
for (let i = 1; i < length; i++) {
var fn = fns[i]
result = fn.apply(this, [result])
}
return result
}
}
var newfn = composeFn(sum, pow)
console.log(newfn(5)); //100
with语句、eval函数(拓展知识)
with
语句 扩展一个语句的作用域链,不推荐使用有兼容性问题
eval
允许执行一个代码字符串。他是一个特殊函数可以将传入的字符串当作js代码执行
- 可读性差
- 有注入风险
- 必须经过解释器 不会得到引擎的优化
严格模式的使用
js的局限性 :
- JavaScript 不断向前发展且并未带来任何兼容性问题;
- 新旧代码该新模式对于向下兼容有帮助但是也有问题出现
- 就是创造者对于js的不完善之处会一直保留
ES5标准中提出了严格模式的概念,以更加严格的方式对代码进行检测和执行
只需要在代码的开头或者函数的开头 加入use strict
就可以开启严格模式
JS对象增强
数据属性描述符
我们的属性一般定义在对象的内部或者直接添加到对象内部,但是这种方式我们就不能对属性进行一些限制,比如这个属性是否是可以通过delete删除,是否可以for-in遍历的时候被遍历出来等等
PS: 一个属性进行比较精准的操作控制,就可以使用属性描述符。
- 通过属性描述符可以精准的添加或修改对象的属性
- Object.defineProperty 来对属性进行添加或者修改
这个方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
Object.defineProperty()
属性描述符分类
分为两类:
- 数据属性
- 存取属性
数据属性描述符
Configurable
:表示属性是否可以通过delete删除属性,是否可以修改它的特性
- 使用对象定义属性的时候为true
- 使用属性描述符来定义的时候 默认为false
Enumerable
:表示属性是否可以通过for-in或者Object.keys()返回该属性;
- 直接对象内定义的时候 为true
- 通过属性描述符定义为false
Writable
:表示是否可以修改属性的值;
- 直接对象内定义的时候 为true
- 通过属性描述符定义为false
value
:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改
- 默认情况下这个值是undefined
使用案例
var obj = {
name: "whit",
age: 12
}
Object.defineProperty(obj, "name", {
configurable: false,
enumerable: false,
writable: false,
value: "1234"
})
存取属性描述符
Configurable&Enumerable
也是存取属性描述符
get
:获取属性时会执行的函数。默认为undefined
set
:设置属性时会执行的函数。默认为undefined
同时定义多个属性
// 多个属性调用
Object.defineProperties(obj, {
name: {
configurable: false,
enumerable: false
},
age: {
enumerable: false,
writable: false,
}
})
对象方法补充
获取对象的属性描述符:
- getOwnPropertyDescriptor
- getOwnPropertyDescriptors
禁止对象扩展新属性:preventExtensions
- 给一个对象添加新的属性会失败(在严格模式下会报错);
密封对象,不允许配置和删除属性:seal
- 实际是调用preventExtensions
- 并且将现有属性的configurable:false
冻结对象,不允许修改现有属性: freeze
- 实际上是调用seal
- 并且将现有属性的writable: false
代码案例
// 阻止对象的拓展
Object.preventExtensions(obj)
obj.address = 12
//密封对象 不能进行配置
Object.seal(obj)
delete obj.name
// 冻结对象
Object.freeze(obj)
obj.name = "ske"
ES5&ES6对象特性
ES5
对象和函数的原型
JS中每一个对象都有一个特殊的内置属性,这个特殊的对象可以指向其他的对象
- 我们通过引用对象的属性key来获取一个value时,它会触发 Get 的操作
- 首先检查该对象是否有对应的属性,如果有的话就使用对象内的
- 如果对象中没有属性,那么会访问对象的
prototype
- 每一个对象都有一个原型属性
使用方式有两种:
- 通过对象的
_proto_
属性可以获取到(浏览器自己添加的,存在一定的兼容性问题) - 通过 Object.getPrototypeOf 方法可以获取
prototype属性是函数特有的属性 我们的对象可以通过
Object.getPrototypeOf
或__proto__
来查看原型。
var obj = {
}
function foo() {
}
console.log(foo.prototype);
当我们这个对象有对多个共同值的时候,可以把相同的东西当如原型里,这样每次创建这个对象的时候,就可以直接调用而不是重新创建。
function Student(name, age) {
this.name = name
this.age = age
// 如果我们每个对象都创建那么这两个方法会出现很多的冗余
// this.running = function () {
// console.log(this.name + "running");
// }
// this.eating = function () {
// console.log(this.name + "eating");
// }
}
Student.prototype.running = function () {
console.log(this.name + "running");
}
Student.prototype.eating = function () {
console.log(this.name + "eating");
}
var stu1 = Student("jjj", 12)
var stu2 = Student("hhh", 18)
Constructor属性
原型对象上面是有一个属性的:constructor,默认情况下原型都会有一个叫constructor
指向当前的对象
function Person() { }
var PersonProtype = Person.prototype
console.log(PersonProtype);
console.log(PersonProtype.constructor);
console.log(PersonProtype.constructor == Person);
原型对象是可以重写的,当我们需要给原型添加更多的属性的时候一般我们会选择重写原型对象
我们也可以改变原型对象中constructor的指向的使用
//改变指向对象
Person.prototype={
constructor:Person
}
//修改枚举类型
Object.defineProperty(Person.prototype,"constructor",{
enumerable:false
})
这里要注意的是原生的constructor是不可枚举的,但是修改constructor的时候会让constructor的特性被设置为true这个时候需要修改一下对象默认属性设置
创建对象的内存表现:
如果我们向对象加入属性在之后的变化:
原型对象默认创建的时候,__proto__都是指向object的的proto的
多种继承方式
继承
面向对象有三大特性:封装、继承、多态
- 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程
- 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中)
- 多态:不同的对象在执行时表现出不同的形态
这里主要将JS中的继承,在了解继承之前我们需要了解JS中的原型链机制,这个是之后理解的关键
原型链
在js中我们不断的获取原型对象,原型链最顶层的原型对象就是Object的原型对象
[Object: null prototype] {}
这种提示一般有两个情况:
- 该对象有原型,且这个原型的属性指向null或者最顶层了
- 这个对象有很多的默认属性方法
ps:Object是所有类的父类
我们也可以对原型链做一些自定义操作,比如这样:
var obj = {
}
obj.__proto__ = {
}
obj.__proto__.__proto__ = {
}
obj.__proto__.__proto__.__proto__ = {
name: "小冷"
}
原型链实现继承
function Person(){
this.name = "l"
}
var p = new Person()
stu.prototype = p
//name == l
stu.prototype.studying = function(){
console.log(this.name+"studying")
}
我们可以通过赋值原型的形式来实现继承,但是有一些弊端
- 直接打印对象是看不到属性的
- 这个属性会被多个对象共享,如果是引用类型就会造成问题
- 不能给父类传递参数,没法定制化
借用构造函数继承
为了解决原型链继承中存在的问题,constructor stealing
应运而生 ,借用继承的做法非常简单:在子类型构造函数的内部调用父类型构造函数
- 因为函数可以任意调用
- 因此通过apply和call也可以再新创建的对象上实行构造函数
function Person(name, age, height, address) {
this.name = name
this.age = age
this.height = height
this.address = address
}
function Student(name, age, height, address, sno, score) {
Person.call(this,name, age, height, address)
this.sno = sno
this.score = score
}
可以使用父类的构造函数来实现创造,解决之前原型链的问题 在ES6之前一直是保持的这个方式,但是这个继承方式依然不是很完美
- 无论在什么情况下,都会调用两次父类构造函数。 一次是创建子类原型,一次是构造函数
- 所有的子类都会有两份父类的属性
继承最终方案
在继续的发展中, JSON的创立者道格拉斯, 提到了新的继承方法,这也是目前es5 阶段最合适的继承方案 寄生组合继承
- 结合原型类继承和工厂模式
- 创建一个封装继承过程的函数,在这个函数的内部来增强对象,最后将这个对象返回
function Person(name, age, height, address) {
this.name = name
this.age = age
this.height = height
this.address = address
}
Person.prototype.running = function () {
console.log(this.name + " running");
}
function Student(name, age, height, address, sno, score) {
Person.call(this, name, age, height, address)
this.sno = sno
this.score = score
}
// 原型继承
var obj = Object.create(Person.prototype)
console.log(obj.__proto__ === Person.prototype);
Student.prototype = obj
// 上到真是环境 会封装用 为了兼容性可以多一个创造类的方法
function object(o){
function F(){}
F.prototype = o
return new F()
}
function inherit(Subtype, Supertype) {
Subtype.prototype = object(Supertype.prototype)
// 需要构造方法
Object.defineProperty(Subtype, "constructor", {
enumerable: false,
configurable: this,
writable: true,
value: Subtype
})
}
inherit(Student, Person)
Student.prototype.eating = function () {
console.log(this.name + "eating");
}
var stu = new Student("小明");
stu.eating()
对象方法补充
hasOwnProperty : 对象是否有某一个属于自己的属性
in/for in 操作符: 判断某个属性是否在对象或者对象的原型上
instanceof : 用于检测构造函数的原型,是否出现在某个实例对象的圆形脸上
isPrototypeOf:用于检测某个对象,是否出现在某个实例对象的原型链上
ES6
class定义关键字
这个关键字主要是用于区别代码的编写方式的,之前编写中创建类和构造函数过于相似,而且代码并不容易理解
- 在ES6的标准中使用了class关键字来直接定义类
- 类本质上依然是前面所讲的构造函数、原型链的语法糖
- 比较少用 一般情况遇不到
使用方法
class Person{}
var Student = class{
}
类的构造函数
在创建对象的时候给类传递一些参数
- 每个类都可以有一个自己的构造函数(方法),这个方法的名称是固定的constructor
- 当我们通过new操作符,操作一个类的时候会调用这个类的构造函数constructor
- 构造函数时唯一的 不能出现多个
通过new关键字操作类的时候,会调用这个constructor函数
- 在内存中创建一个新的空对象
- 对象内部的[[prototype]]属性会被赋值为该类的prototype属性
- 构造函数内部的this,会指向创建出来的新对象
- 执行构造函数的内部代码
- 如果构造函数没有返回非空对象,则返回创建出来的新对象
语法使用
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
running() {
console.log(this.name + "running");
}
eating() {
console.log(this.age + "eating");
}
}
var p1 = new Person("123", 123)
console.log(p1.name, p1.age)
class 创建的类和function直接创造构造方法创建的类时没有什么区别的
定义访问器
对象的属性描述符时有讲过对象可以添加setter和getter函数的,类同样也是可以的
var ojb = {
_name:"why",
set name(value){
this._name = value
},
get name(){
return this._name
}
}
类的静态方法
静态方法通常用于定义直接使用类来执行的方法,不需要有类的实例,使用static关键字来定义
class Person{
constructor(age){
this.age = age
}
static create(){
return new Person(Math.floor(Math.random()*100))
}
}
extends 继承
ps : js的继承属于只支持单继承
之前我们在es5 中经过原型链继承,组合继承等等操作才解决继承的一些问题,但是在ES6 中 他给我们提供了一个关键字 : extends
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
running() {
console.log(this.name + "running");
}
eating() {
console.log(this.age + "eating");
}
}
class Teacher extends Person {
constructor(name, age, title) {
// this.name = name
// this.age = age
super(title)
this.title = title
}
Teaching() {
console.log(this.name + "Teaching");
}
}
class Student extends Person {
constructor(name, age, sno) {
// this.name = name
// this.age = age
super(name, age)
this.sno = sno
}
studying() {
console.log(this.name + "studying");
}
}
var p1 = new Student("123", 123, "123")
console.log(p1.name, p1.age)
p1.eating()
我们只需要在类之后用extends 指向需要被继承的类 就可以实现继承
Super 关键字
Class为我们的方法中还提供了super关键字
- 执行 super.method(...) 来调用一个父类方法
- 执行 super(...) 来调用一个父类 constructor(只能在我们的 constructor 中)
- super使用的位置有三个 子类构造函数,实例方法,静态方法
PS: 在子类的构造函数中使用this或者返回默认对象之前,必须先通过super调用父类的构造函数
在JS 中 我们子类也是可以重写父类的方法的,但是当我们既想让子类有自己的操作,还想复用父类的实现,就可以使用super 关键字
class Animal {
running() {
console.log("running");
}
}
class dog extends Animal {
running() {
console.log("dog four jio");
super.running()
}
}
var dog = new dog()
dog.running()
继承内置类
同样 我们可以继承内置类,比如数组 Array 类 加入我们想给数组加一些拓展比如获得第一位 获得最后一位 我们就可以继承数组对象添加我们想要定义的拓展
// 继承内置类
class CodeArray extends Array {
get lasItem() {
return this[this.length - 1]
}
get firstItem() {
return this[0]
}
}
var arr = new CodeArray(12, 14, 15, 16, 20)
console.log(arr);
console.log(arr.lasItem());
类的混入mixin
JavaScript的类只支持单继承,混入的思想可以帮助我们利用函数的方式实现嵌套继承,
- 它可以通过编写混入函数以返回新对象的方式实现多个继承
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
swimming() {
console.log(first)
}
}
function mixinRunner(BaseClass) {
return class extends BaseClass {
running() {
console.log(this.name + " running~")
}
}
}
function mixinEater(BaseClass) {
return class extends BaseClass {
eating() {
console.log(this.name + " eating~")
}
}
}
class NewPerson extends mixinEater(mixinRunner(Person)) {
}
var NP = new NewPerson();
NP.swimming()
ES6新的语法糖
ES6中对 对象字面量 进行了增强,称之为 Enhanced object literals
主要包括:
- 属性的简写:Property Shorthand
- 方法的简写:Method Shorthand
- 计算属性名:Computed Property Names
属性的简写
当属性和变量名字完全一样的时候 可以将他省略 之后生成的代码还是和之前无差
/*
1. 属性的简写
*/
var name = "123"
var age = 12
var obj = {
name,
age
}
方法的简写
/*
1. 属性的增强
*/
var obj = {
running:function(){},
//简写为
running(){}
}
计算属性名
var address = "us"
var obj = {
[address]: "北京"
}
解构 Destructuring
ES6中新增了一个从数组或对象中方便获取数据的方法,它是一种特殊语法
数组的解构:
- 基本解构过程
- 顺序解构
- 解构出数组:…语法
- 默认值: undefind
var names = ["nnn","qqq","lll","fff"]
var [names1,names2,...names] = names
//输出结果 nnn,qqq,[lll,fff]
对象的解构:
- 基本解构过程
- 任意顺序
- 重命名
- 默认值
var obj = {name:"uuu",age:12,height:1.88}
var {height,name} = obj
//需要重命名
var {height:hei,name} = obj
默认值
var {name,age,grilfrineds:gf = "lucy"} = obj
对象解构的应用:
function getPostion({x,y}){
var a = x+y
}
getPostion({x:10,y:20});
实现函数 apply /call / bind
实现函数内容
首先我们要遵循封装的思想 , apply 和 call 方法 其实就是调用的方式不同而已
所以我们可以将这两个方法调用的共同点封装成一个函数 接下来只需要用不同的方式调用就可以了
function foo(name,
) {
console.log(this, name, age);
}
// foo.apply("aaa", ["hyc", 12])
// foo.call("aaa", "lebron", 38)
function execFn(thisArg, otherArgs, fn) {
// 1、 获取 thisArg 确保是一个对象类型
thisArg = (thisArg === null || thisArg === undefined) ? window : Object(thisArg)
// thisArg 传入的第一参数是要绑定的this
Object.defineProperty(thisArg, "fn", {
enumerable: false,
configurable: true,
value: fn
})
thisArg.fn(...otherArgs)
delete thisArg.fn
}
Function.prototype.hycApply = function (thisArg, otherArgs) {
execFn(thisArg, otherArgs, this)
}
Function.prototype.hyccall = function (thisArg, ...otherArgs) {
execFn(thisArg, otherArgs, this)
}
foo.hycApply({ name: "hyc" }, ["james", 25])
foo.hyccall(123, "why", 18)
Function.prototype.hycbind = function (thisArg, ...otherArgs) {
thisArg = (thisArg === null || thisArg === undefined) ? window : Object(thisArg)
Object.defineProperty(thisArg, "fn", {
enumerable: false,
configurable: true,
writable: false,
value: this
})
return (...newArgs) => {
// var allArgs = otherArgs.concat(newArgs)
var allArgs = [...otherArgs, ...newArgs]
thisArg.fn(...allArgs)
}
}
var newFoo = foo.hycbind("abc", "hyc", 30)
// newFoo(1.88,"广州")、
newFoo()
ES6-ES13
es6
JS代码执行过程中需要了解的ECMA文档的术语
- 执行上下文栈:Execution Context Stack,用于执行上下文的栈结构;
- 执行上下文:Execution Context,代码在执行之前会先创建对应的执行上下文;
- 变量对象:Variable Object,上下文关联的VO对象,用于记录函数和变量声明;
- 全局对象:Global Object,全局执行上下文关联的VO对象;
- 激活对象:Activation Object,函数执行上下文关联的VO对象;
- 作用域链:scope chain,作用域链,用于关联指向上下文的变量查找;
let/const基本使用
ES6开始新增了两个关键字可以声明变量:let、const
- let、const不允许重复声明变量
- let 不会作用域提升
- let、const在执行声明代码前是不刻意访问的
var、let、const的选择
- 在未来的开发中 很少会使用var 来声明变量来开发了
- let const 比较推荐在开发中使用
- 推荐优先 使用 const 保证数据的安全性不会被随意的篡改
- 只有需要重复赋值的时候才使用 let
模板字符串
ES6允许我们使用字符串模板来嵌入JS的变量或者表达式来进行拼接,使用 ``` `符号来编写字符串,称之为模板字符串
可以在模板字符串的时候用 ${}
来嵌入动态内容
``` ` 符号还可以调用方法自动传入参数
const name = "hyc"
const age = 18
function foo(...args){
console.log("111",args)
}
foo `my name is ${name},age is ${age},height is ${1.88}`
函数默认值
ES6 之后 函数允许给参数一个默认值
function test(x = 10, y = 10)
可以使用这种方式来给函数的参数加入默认值,允许我们使用表达式比如
x = 1 || x > 1
Symbol的基本使用
在ES6之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突
- 比如在新旧值同名的情况下混入 会有一个值被覆盖掉
- 比如之前有的fn 属性 如果新添加一个fn 属性 内部原有的fn 会怎么样?
Symbol就是为了解决上面的问题,用来生成一个独一无二的值。
- Symbol值是通过Symbol函数来生成的,生成后可以作为属性名
- 在ES6中,对象的属性名可以使用字符串,也可以使用Symbol值
const s1 = Symbol()
const s2 = Symbol()
const obj = {
[s1]: "aaa",
[s2]: "aaa"
}
// 获取 symbol 值 对应的key
console.log(Object.getOwnPropertySymbols(obj));
Set的基本使用
在ES6之前,我们存储数据的结构主要有两种:数组、对象。
在ES6中新增了另外两种数据结构:Set、Map,以及它们的另外形式WeakSet、WeakMap。Set 中数据是不能重复的
常用方法
set 支持for of
Set常见的属性:
- size:返回Set中元素的个数;
Set常用的方法:
- add(value):添加某个元素,返回Set对象本身;
- delete(value):从set中删除和这个值相等的元素,返回boolean类型;
- has(value):判断set中是否存在某个元素,返回boolean类型;
- clear():清空set中所有的元素,没有返回值;
- forEach(callback, [, thisArg]):通过forEach遍历set;
// 创建 set
const set1 = new Set()
set1.add(11)
set1.add(12)
set1.add(13)
set1.add(11)
console.log(set1.size);
WeakSet使用
WeakSet和Set有什么区别
- WeakSet 不可以遍历
- WeakSet中只能存放对象类型,不能存放基本数据类型
- WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC可以对该对象进行回收;
let obj1 = { name: "why" }
let obj2 = { name: "hyc" }
const weakset = new WeakSet()
weakset.add(obj1)
weakset.add(obj2)
Map基本使用
Map,用于存储映射关系
之前我们可以使用对象来存储映射关系,他们有什么区别
- 在之前的学习中对象存储映射关系只能用字符串(ES6新增了Symbol)作为属性名(key)
- 某些情况下我们可能希望通过其他类型作为key,比如对象,这个时候会自动将对象转成字符串来作为key;
这个时候就可以使用 map
const info = { name: "hyc" }
const map = new Map()
map.set(info, "aaaa")
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(callback, [, thisArg]):通过forEach遍历Map;
WeakMap的使用
WeakMap,也是以键值对的形式存在的。
WeakMap和 map 的区别
- WeakMap的key只能使用对象,不接受其他的类型作为key;
- WeakMap的key对对象想的引用是弱引用
- WeakMap不能遍历
WeakMap常见的方法有四个:
- set(key, value):在Map中添加key、value,并且返回整个Map对象;
- get(key):根据key获取Map中的value;
- has(key):判断是否包括某一个key,返回Boolean类型;
- delete(key):根据key删除一个键值对,返回Boolean类型;
拓展知识
数值的表示
允许使用二进制 八进制来赋值
const num1 = 100
const num2 = 0b100
ES2021 新增数字过长可以用 _ 来连接
const num1 = 100_000_000