很长时间没有复习js了 找个前端学习路线图 按照上面 一个一个复习 分成四个大知识点
一、复习函数知识点
函数模块
- this
- 闭包
- 执行上下文
- 原型/原型链
- 作用域/作用域链
1、this绑定
什么是this:
this 是在运行时进行绑定的,它的上下文取决于函数调 用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。当一个函数被调用时,会创建一个执行上下文。这个记录会包 含函数在哪里被调用、函数的调用方式、传入的参数等信息。this 就是这个记 录的一个属性,会在函数执行的过程中用到
1、默认绑定
作为普通函数执行时,this
指向window
。
如果是箭头函数有嵌套就绑定最近一层的this
如果没有就绑定window
2、隐式绑定
当函数作为对象的方法被调用时,this
就会指向该对象
3、显式绑定
Function.prototype上的 apply 、 call 和 bind
调用模式,这三个方法都可以显示的指定调用函数的 this 指向。apply
接收参数的是数组,call
接受参数列表, bind
方法通过传入一个对象,返回一个this
绑定了传入对象的新函数。这个函数的 this
指向除了使用new
时会被改变,其他情况下都不会改变。若为空默认是指向全局对象window。
4、new绑定
构造器调用,this
指向返回的这个对象
2、闭包
什么是闭包:
1、当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时 就产生了闭包。---《你不知道的JavaScript》
2、闭包是指有权访问另一个函数作用域中的变量的函数--《JavaScript高级程序设计》
1、函数嵌套函数
2、内部函数引用外部函数的变量
1、可以写模块
2、可以模仿块级作用域
3、可以阻止其回收
4、私有化变量
1、保护私有变量不被外界干扰
2、可以延长外部函数生命周期
容易引起内存泄漏
3、执行上下文
执行上下文的概念:
变量或函数的上下文决定 了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object), 而这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台 处理数据会用到它
1、执行全局代码 ,创建全局上下文 ,并将全局上下文压入执行上下文栈
2、全局上下文初始化
3、初始化同时 要执行的函数被创建 ,保存作用域链到函数内部属性[[scope]]
4、执行 要执行的函数 ,并且创建执行上下文 ,并将执行上下文压入执行上下文栈
5、执行上下文初始化:
1、复制函数[[scope]]属性创建函数作用域链
2、用arguments创建活动对象
3、初始化函数,加入形参,函数声明, 变量声明
4、将活动对象压入 执行函数作用域链顶端
同时创建f函数 保存作用域链到f函数的内部属性[[scope]]
6、执行f函数,创建f函数执行上下文 并压入执行栈
7、f函数初始化 再走一次步骤5
8、f函数执行沿着作用域链查找scope值 并返回
9、f函数执行完毕 执行上下文栈弹出f函数, 然后要执行的函数执行完毕也弹出执行上下文栈
4、原型/原型链
原型的概念:
JavaScript 中的对象有一个特殊的 [[Prototype]] 内置属性,其实就是对于其他对象的引 用。几乎所有的对象在创建时 [[Prototype]] 属性都会被赋予一个非空的值
原型链的概念:
如果在当前原型找不到值就会去相关原型找, 相关原型找不到就去原型的原型找 如果找到就返回值 找不到就返回null
1、写一个具名函数名为Person, 再创造一个构造函数person, 可以通过person.prototype访问到实例原型(Person.prototype)
2、Person可以通过_proto_也可以访问到实例原型
3、实例原型也可以访问到构造函数, 可以通过Person.prototype.constructor访问到构造函数
4、如果在当前原型查找不到, 就会去相关原型查找, 如果还找不到就去原型的原型查找
5、如果原型的原型没有找到就会返回一个null, 步骤4-5就是原型链
5、作用域/作用域链
作用域概念:
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)
作用域链概念:
上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定 了各级上下文中的代码在访问变量和函数时的顺序
1、如果查找的目的是对 变量进行赋值,那么就会使用 LHS
查询
2、如果目的是获取变量的值,就会使用 RHS
查询。
3、LHS
和 RHS
查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到 所 需的标识符)
,就会向上级作用域继续查找目标标识符,这样每次上升一级作用域,最后抵达全局作用域,无论找到或没找到都将停止。
1.全局作用域:代码在程序的任何地方都能被访问,window 对象的内置属性都拥有全局作用域。
2.函数作用域:在固定的代码片段才能被访问
异步模块
- 单线程
- 异步队列
- 回调函数
- promise
- async/await
1、单线程
js单线程的来源:
JavaScript作为浏览器脚本语言主要用途是和用户进行交互, 为什么js是单线程的呢? 如果js为多线程, 当页面更新时 用户又进行交互, 这时候线程的同步问题会变得非常复杂 还有就是 如果多线程 同时执行多段js 如果这些js都修改了dom 那么就会发生冲突, 所以js采用了单线程的形式
1、内存堆
---进行内存分配的区域
2、调用栈
---代码执行中栈中的位置
1、同一时间只能做一件事
2、JS和DOM渲染共用同一个线程,因为JS可修改DOM结构
2、异步队列
1、函数入栈, 当stack执行到异步任务时, 就将他丢给Web Api, 接着执行同步任务 直到stack为空
2、在这个时候 Web Api完成这个事件 把这个回调函数放进任务队列(宏任务在宏任务队列,微任务在微任务队列)
3、当执行栈为空 Event Loop 就会先把微任务清空(为什么微任务比宏任务执行的早?1、微任务是 ES6 语法规定的(直接被压入微任务队列), 2、宏任务是浏览器规定的(通过Web Api压入宏任务队列),3、宏任务一般执行事件比较长,4、宏任务执行前一定伴随着Event Loop循环结束,微任务是在Event Loop结束之前执行的
)
5、微任务队列清空后,进入宏任务队列, 取队列第一项任务 放入stack栈中,执行完成后 继续查看微任务队列如果有就执行清空一直重复步骤5直到清空所有任务
3、回调函数
什么是回调函数:
简单的定义
:回调函数就是另一个函数执行完成后要执行的函数复杂的定义
:在js中函数是作为对象, 因此函数可以将函数作为参数传递, 并且由其他函数返回, 执行此操作的函数作为高阶函数, 任何作为参数传递的函数都称为回调函数
1、回调函数作为参数传递在函数的某一个位置执行
2、回调函数也可以获取函数的变量
1、异步调用(例如读取文件,进行HTTP请求,等等)
2、setTimeout和setInterval方法
3、精简代码
4、promise
Promise 必须为以下三种状态之一:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)。一旦Promise 被 resolve 或 reject,不能再迁移至其他任何状态。
1、初始化promise的状态为等待态(pending)
2、立即执行promise中传递fn函数, 将promise内部resolve、reject函数传递给fn, 按事件机制时机处理
3、执行.then(...)注册回调处理数组
5、Promise里的关键是要保证,then方法传入的参数 onFulfilled 和 onRejected,必须在then方法被调用的那一轮事件循环之后的新执行栈中执行
真正的链式Promise是指在当前promise达到fulfilled状态后,即开始进行下一个promise.
5、async/await
async/await的用处:
用同步的方式 执行异步的操作
1、await
必须要搭配async
使用, 不然会报错
2、async
返回的是一个promise
函数有无值要看return
出值没有
3、await
后面最好是接Promise
,虽然接其他值也能达到排队效果async/await
作用是用同步方式,执行异步操作
ES6模块
- 模块
- class类
- symbol
- 箭头函数
- 解构赋值
- rest参数
- promise
- set/map
- var/let/const
- 模板字符串
- 扩展运算符
- Async/Await
1.模块
为什么要使用模块:
1.可复用性:在日常的开发中有许多代码都是重复的 可以给他们封装到一个模块然后直接用import引入就行了
2.增加可维护性:由于每个模块都是独立的,互不影响,在维护的时候很好排查是那个模块出错
3.避免命名污染:在 javascript 脚本中,所有的 js 文件的顶级作用域创建的变量,会被添加到共享的全局作用域,这就会导致不同的人开发的代码可能会有相同的变量名,导致变量名污染
导出模块所用的命令是 export, 最简单的导出方式就是在声明的变量、函数、类前面加一个 export。 创建一个demo.js
// 导出变量
export let name = '桃翁';
// 导出函数
export function fn(){
console.log("导出模块")
}
// 导出类
exprot class demo{
constructor(name){
this.name = name
}
}
// 私有函数
function private(){
console.log("外部不可访问")
}
导入模块的命令是 import
import {name,fn,demo} from './demo.js'
// 然后通过命名 直接使用就行
2、class类
class是一个语法糖,其底层还是通过
构造函数
去创建的。所以它的绝大部分功能,ES5 都可以做到。新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已
ES6语法实现
function Demo(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayName = function() {
return this.name;
}
const xiaohua = new Demo('小花', 100);
console.log(xiaohua.sayName())
class实现上面的语法
class Demo {
constructor(name,age){
this.name = name
this.age = age
}
sayName(){
return this.name = name
}
}
const xiaohua = new Demo('小花',100)
console.log(xiaohua.sayName())
constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
class A { }
// 等同于
class A { constructor() {} }
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。 如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为"静态方法"。
Class 可以通过extends关键字实现继承
class Animal {}
class Cat extends Animal { };
super这个关键字,既可以当作函数使用,也可以当作对象使用
1、当函数使用:
super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。
class A {}
class B extends A {
constructor() {
super();
}
}
2.super作为对象调用:
super作为对象调用在普通方法中,指向父类的原型对象
在静态方法中,指向父类
3、symbol
symbol 是原始值,且符号实例是唯一、不可变的。symbol 用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
1、只要创建就是一个唯一的记号
let asym = Symbol('nanlv')
let bsym = Symbol('nanlv') console.log(asym==bsym) ;
// 输出 false
2、创建即唯一:只要创建 Symbol()实例并将其用作对象的新属性,就可以保证它不会覆盖已有的对象属性。
3、不能与 new 一起使用
4、箭头函数
箭头函数作为ES6中新加入的语法,以其简化了我们的代码和让开发人员摆脱了“飘忽不定”的this指向等特点
1、箭头函数与普通函数相比,缺少了caller,arguments,prototype
2、声明方式(只能声明匿名函数 但是可以通过表达式的方式让箭头函数具名
)、this指向不同(箭头函数里面根本没有自己的this,而是引用的上层作用域中this
)
3、箭头函数没有原型prototype
5、箭头函数不能当成一个构造函数
4、箭头函数没有自己的arguments(可以用rest参数代替
)
5、解构赋值
解构是ES6提供的语法糖,其实内在是针对
可迭代对象
的Iterator接口
,通过遍历器
按顺序获取对应的值进行赋值
1、对线结构
// 对象属性解构
let {f1,f2} = {f1:'a',f2:'b'}
conlole.log(f1,f2) // a,b
// 解构对象重命名
let {f1:fa,f2} = {f1:'a',f2:'b'}
console.log(fa,f2)// a,b
2、数组结构
let [a, b, c] = [1, 2, 3]
console.log(a,b,c) // 1,2,3
6、rest参数
1、返回函数多余参数
2、以数组的形式存在,之后不能再有其他参数
3、代替Arguments对象
7、promise
Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行
8、set/map
set:类似数组,但是成员唯一,不能有重复的值
map:类似对象,依旧是键值的存在,但是键值是一一对应的关系,而不是像对象,键名只能是字符串
-
1、去重:new set([数组])
2、size:set的length
3、add:添加 set.add()
4、delete: 删除 set.delete()
5、clear:清除 set.clear()
6、这些操作是实时的,会影响操作之前的set -
1、增:map.set('a', 1);
2、查:let data = map.has('a');
3、改:map.set('a', 2);
4、删:map.delete('a');
9、var/let/const
ES6之前创建变量用的是var,之后创建变量用的是let/const
1、var
变量没有块概念 可以跨块访问 但不能跨函数访问。let
变量只允许在块作用域访问 不允许跨块访问。const
常量定义就要赋值,只能在块访问 且不能改变常量
2、var
有变量提升,可以先使用后声明 而let
要先声明后使用
3、var 可以在相同作用域下重复定义变量名称 let const不可以
4、全局上下文中let 跟全局对象GO和全局变量没有任何关系 var跟GO呈映射关系
5、let const 有暂时性死区
10、模板字符串
let a = "tom"
let b = "jack"
es6之前写法: a+"爱上了"+b
es6之后写法
`${a}爱上了${b}`
11、扩展运算符
var arr = [1, 2, 3]
1.数组深拷贝-数组中的元素为基本类型,若为object,依然会拷贝引用地址:
var arr2 = arr
var arr3 = [...arr]
console.log(arr === arr2) // true 说明引用了同一引用地址
console.log(arr === arr3) // false 说明在堆内存开辟了新的空间
2、把一个数组插入另一个数组字面量: var arr4 = [...arr, 4, 5, 6]; // arr4 [1,2,3,4,5,6]
3、字符串转数组: var str = 'abcd'; var arr5 = [...str]
4、函数调用:
function num(x,y){
return x + y
}
const number = [8,4]
num(...number)
12、async/await
async/await的用处:
用同步的方式 执行异步的操作
1、await
必须要搭配async
使用, 不然会报错
2、async
返回的是一个promise
函数有无值要看return
出值没有
3、await
后面最好是接Promise
,虽然接其他值也能达到排队效果async/await
作用是用同步方式,执行异步操作
js一些底层问题
- 编译原理
- 内存管理
- 垃圾回收
1、编译原理
-
1、语法分析(先扫一边 看看有没有语法错误)
2、执行前一刻 进行预编译(变量 声明提升 函数提升)
3、解释执行(解释一行执行一行) -
1、创建AO对象(执行期上下文)
2、找形参和变量声明, 将形参和变量作为AO属性名, 值为undefined
3、将实参值和形参统一
4、在函数体里找函数声明, 值赋予函数体 -
1、创建GO对象(全局对象)
2、找变量声明 , 将变量声明作为GO对象的属性名 值为undefinde
3、在全局里找函数声明, 将函数名做为GO对象的属性名 值赋予函数体
2、内存管理
JS 环境中分配的内存有如下声明周期:
1、内存分配:当我们申明变量、函数、对象的时候系统会自动帮我们分配内存
2、内存使用:读写内存 也就是当我们使用变量、函数的时候
3、内存回收:使用完毕、由垃圾回收机制自动帮我们回收不需要的内存
3、垃圾回收
-
GC
即Garbage Collection
,程序工作过程中会产生很多垃圾
,这些垃圾是程序不用的内存或者是之前用过了,以后不会再用的内存空间,而GC
就是负责回收垃圾的,因为他工作在引擎内部 -
1、标记清除算法: 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0然后从各个根对象开始遍历,把不是垃圾的节点改成1清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收
2、引用计数算法:当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1 , 如果同一个值又被赋给另一个变量,那么引用数加 1 , 如果该变量的值被其他的值覆盖了,则引用次数减 1 , 当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存 -
1、标记清除算法优点:标记清除算法的优点只有一个,那就是实现比较简单,打标记也无非打与不打两种情况,这使得一位二进制位(0和1)就可以为其标记,非常简单
2、引用计数算法优点:引用计数算法的优点我们对比标记清除来看就会清晰很多,首先引用计数在引用值为 0 时,也就是在变成垃圾的那一刻就会被回收,所以它可以立即回收垃圾
而标记清除算法需要每隔一段时间进行一次,那在应用程序(JS脚本)运行过程中线程就必须要暂停去执行一段时间的GC
,另外,标记清除算法需要遍历堆里的活动以及非活动对象来清除,而引用计数则只需要在引用时计数就可以了
3、标记清除算法缺点:标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了内存碎片
,并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题
4、引用计数算法缺点:首先它需要一个计数器,而此计数器需要占很大的位置,因为我们也不知道被引用数量的上限,还有就是无法解决循环引用无法回收的问题,这也是最严重的