# js收藏集
**Repository Path**: djxu/js_view
## Basic Information
- **Project Name**: js收藏集
- **Description**: js
- **Primary Language**: JavaScript
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 5
- **Forks**: 0
- **Created**: 2022-02-11
- **Last Updated**: 2022-05-20
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# JS 知识体系
## 1、声明
### 1. 函数声明
(1).Function()构造器
```
var f =new Function()
```
(2).函数声明(函数声明提升)
```
function f (){
console.log(xdj);
}
```
(3).函数表达式
```
var f = function() {
console.log(1);
}
```
(4).自调用函数
```
(function() {
console.log(1);
})()
```
(5).箭头函数(箭头函数会默认帮我们绑定外层 this 的值)
```
const x = (x, y) => x * y;
```
### 2. JS 变量声明
- var 声明的变量会挂载在 window 上,而 let 和 const 声明的变量不会
- var 声明变量存在变量提升,let 和 const 不存在变量提升(严格来说,let 也存在)
- let 和 const 声明形成块作用域
- let 存在暂存死区
- const 声明必须赋值
### 3. JS 为什么要进行变量提升
(1). 提高性能
> 在 JS 代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。
(2).容错性更好
```
a = 1;var a;console.log(a);
```
> 如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。 虽然,在可以开发过程中,可以完全避免这样写,但是有时代码很复杂的时候。可能因为疏忽而先使用后定义了,这样也不会影响正常使用。由于变量提升的存在,而会正常运行
## 2、数据类型的分类
### 1. 基本类型
- string
- boolean
- number(精度在(2^53-1)范围)
- symbol(代表创建后独一无二且不可变的数据类型,是为了解决可能出现的全局变量冲突的问题。)
- null
- undefined
- BigInt(表示任意精度格式的整数)
- 注意:NaN 不是数据类型
**`注:NaN不是数据类型`**
```
typeof NaN === 'number' //true
NaN==NaN //false
```
### 2. 引用类型
(1).内置对象
> String、Number、Boolean、Array、Date、RegExp、Math、 Error、 Object、Function、 Global
(2).宿主对象
> 1. BOM 对象: Window、Navigator、Screen、History、Location
> 2. DOM 对象:Document、Body、Button、Canvas 等
(3).自定义对象
- 直接创建
```
var obj1 = {};
var obj2 = {x:0,y:0};
var obj3 = {name:‘Mary’,age:18}
```
- 工厂模式
```
function createPerson(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
return o;
}
var person1 = createPerson('zhang',30,'java');
```
- 构造函数模式
```
function Person(name,age,job){
this.name= name;
this.age = age;
this.job = job;
}
var person1 = new Person('zhang',30,'java');
```
- 原型模式
```
function Person(){}
Person.prototype.name = 'zhang';
Person.prototype.age = '22';
Person.prototype.job = 'html5';
var person1 = new Person();
```
### 3、数据类型的判断
1. typeof
> typeof 操作符来判断一个值属于哪种基本类型,无法分辨对象类型
```
typeof 'seymoe' // 'string'
typeof true // 'boolean'
typeof 10 // 'number'
typeof Symbol() // 'symbol'
typeof null // 'object' 无法判定是否为 null
typeof undefined // 'undefined'
typeof {} // 'object'
typeof [] // 'object'
typeof(() => {}) // 'function'
```
**`为什么typeof null为object`**
> js 在底层存储变量的时候,会在变量的机器码的低位 1-3 位存储其类型信息,由于 null 的所有机器码均为 0,因此直接被当做了对象来看待。
* 000:对象
* 010:浮点数
* 100:字符串
* 110: 布尔值
* 1:整数
* null的所有机器码均为0
* undefined:用 −2^30 整数来表示
2. instanceof
> 测试构造函数的 prototype 是否出现在被检测对象的原型链上,无法判断一个值到底属于数组还是普通对象
```
[] instanceof Array // true
({}) instanceof Object // true
(()=>{}) instanceof Function // true
let arr = []
let obj = {}
arr instanceof Array // true
arr instanceof Object // true
obj instanceof Object // true
在这个例子中,arr 数组相当于 new Array() 出的一个实例,
所以 arr.__proto__ === Array.prototype,
又因为 Array 属于 Object 子类型,
即 Array.prototype.__proto__ === Object.prototype,
所以 Object 构造函数在 arr 的原型链上
```
> 判断不了原始类型
```
console.log(true instanceof Boolean);// false
console.log(undefined instanceof Object); // false
console.log(arr instanceof Array); // true
console.log(null instanceof Object); // false
console.log({} instanceof Object); // true
console.log(function(){} instanceof Function);// true
```
> 手写实现
```
function myInstanceof(left, right) {
let prototype = right.prototype
left = left.__proto__
while (true) {
if (left === null || left === undefined)
return false
if (prototype === left)
return true
left = left.__proto__
}
}
```
3. Object.prototype.toString.call()
> 任何类型的值都能返回对应准确的对象类型
```
Object.prototype.toString.call({}) // '[object Object]'
Object.prototype.toString.call([]) // '[object Array]'
Object.prototype.toString.call(() => {}) // '[object Function]'
Object.prototype.toString.call('seymoe') // '[object String]'
Object.prototype.toString.call(1) // '[object Number]'
Object.prototype.toString.call(true) // '[object Boolean]'
Object.prototype.toString.call(Symbol()) // '[object Symbol]'
Object.prototype.toString.call(null) // '[object Null]'
Object.prototype.toString.call(undefined) // '[object Undefined]'
Object.prototype.toString.call(new Date()) // '[object Date]'
Object.prototype.toString.call(Math) // '[object Math]'
Object.prototype.toString.call(new Set()) // '[object Set]'
Object.prototype.toString.call(new WeakSet()) // '[object WeakSet]'
Object.prototype.toString.call(new Map()) // '[object Map]'
Object.prototype.toString.call(new WeakMap()) // '[object WeakMap]'
```
- 该方法本质就是依托 Object.prototype.toString() 方法得到对象内部属性 [[Class]]
- 传入原始类型却能够判定出结果是因为对值进行了包装成对象
- null 和 undefined 能够输出结果是内部实现有做处理
> 调用 Object.prototype.toString 时,会进行如下步骤:
- this 是 undefined ,返回 ‘[object Undefined]’
- this 是 null , 返回 ‘[object Null]’
- this 作为参数调用 ToObject 的结果 O
- 处理 O 获得 tag
- 返回由三个字符串 “[object”, tag, “]” 拼接而成的一个字符串。
## 3、内存
### 1.执行上下文
> 当代码运行时,会产生一个对应的执行环境,在这个环境中,所有变量会被事先提出来(变量提升),有的直接赋值,有的为默认值 undefined,代码从上往下开始执行,就叫做执行上下文。
#### (1).执行环境有三种
- 全局环境:代码首先进入的环境
- 函数环境:函数被调用时执行的环境
- eval 函数
#### (2).执行上下文特点
- 单线程,在主进程上运行
- 同步执行,从上往下按顺序执行
- 全局上下文只有一个,浏览器关闭时会被弹出栈
- 函数的执行上下文没有数目限制
- 函数每被调用一次,都会产生一个新的执行上下文环境
#### (3).执行 3 个阶段
- 创建阶段
> 1.生成变量对象 2.建立作用域链 3.确定 this 指向
- 执行阶段
> 1.变量赋值 2.函数引用 3.函数引用
- 销毁阶段
> 1.执行完毕出栈 2.等待回收被销毁
### 2.堆栈
- 栈: 栈会自动分配内存空间,它由系统自动释放;存放基本类型,简单的数据段,占据固定大小的空间
- 堆: 动态分配的内存,大小不定也不会自动释放。存放引用类型,那些可能由多个值构成的对象,保存在堆内存中
## 4、垃圾回收机制
> 从 2012 年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对于 js 垃圾回收算法的改进都是基于标记-清除算法的改进
### 1.什么是垃圾
> 一般来说,没有被引用的对象就是垃圾,就是要才清除的。但有个例外,如果几个对象相互引用形成一个环,但根访问不到他们,他们也是垃圾(引用计数法,无法清除他们)
### 2.垃圾回收的几种算法
#### 1.引用计数法
> 记录有多少“程序”在引用自己,当引用的数值为 0 时,就开始清除它。
#### 优势:
- 可马上回收垃圾,当被引用数值为 0 时,对象马上会把自己作为空闲空间连到空闲链表上,也就是说。在变成垃圾的时候就立刻被回收。
- 因为是即时回收,那么‘程序’不会暂停去单独使用很长一段时间的 GC,那么最大暂停时间很短
- 不用去遍历堆里面的所有活动对象和非活动对象
#### 劣势:
- 计数器需要占很大的位置,因为不能预估被引用的上限,打个比方,可能出现 32 位即 2 的 32 次方个对象同时引用一个对象,那么计数器就需要 32 位
- 最大的劣势是无法解决循环引用无法回收的问题 这就是前文中 IE9 之前出现的问题
#### 2.标记清除法
> 1. 标记阶段:把所有活动对象做上标记。
> 2. 清除阶段:把没有标记(也就是非活动对象)销毁。
#### 优势:
- 实现简单,打标记也就是打或者不打两种可能,所以就一位二进制位就可以表示
- 解决了循环引用问题
#### 劣势:
- 造成碎片化(有点类似磁盘的碎片化)
- 再分配时遍次数多,如果一直没有找到合适的内存块大小,那么会遍历空闲链表(保存堆中所有空闲地址空间的地址形成的链表)一直遍历到尾端
#### 3.复制算法
- 将一个内存空间分为两部分,一部分是 From 空间,另一部分是 To 空间
- 将 From 空间里面的活动对象复制到 To 空间
- 释放掉整个 From 空间
- 再将 From 空间和 To 空间的身份互换,那么就完成了一次 GC。
## 5、内存泄漏
> 申请的内存没有及时回收掉,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果
### 1.内存泄漏发生的场景
#### (1)意外的全局变量
```
function leaks(){
leak = 'xxxxxx';//leak 成为一个全局变量,不会被回收
}
```
#### (2)遗忘的定时器
> setTimeout 和 setInterval 是由浏览器专门线程来维护它的生命周期,如果在某个页面使用了定时器,当销毁页面时,没有手动去释放清理这些定时器的话,那么这些定时器还是存活着的
#### (3)使用不当的闭包
```
var leaks = (function(){
var leak = 'xxxxxx';// 被闭包所引用,不会被回收
return function(){
console.log(leak);
}
})()
```
#### (4)遗漏的 DOM 元素
```
$('#container').bind('click', function(){
console.log('click');
}).remove();//dom移除了,但是js还持有对它的引用
```
解决:
```
$('#container').bind('click', function(){
console.log('click');
}).off('click').remove();
//把事件清除了,即可从内存中移除
```
#### (5) 网络回调
> 某些场景中,在某个页面发起网络请求,并注册一个回调,且回调函数内持有该页面某些内容,那么,当该页面销毁时,应该注销网络的回调,否则,因为网络持有页面部分内容,也会导致页面部分内容无法被回收
### 2.如何监控内存泄漏
https://www.cnblogs.com/dasusu/p/12200176.html
## 6、作用域
> 变量或者函数的有效作用范围
> 作用域链:我们需要查找某个变量值,会先在当前作用域查找,如果找不到会往上一级查,如果找到的话,就返回停止查找,返回查找的值,这种向上查找的链条关系,叫作用域
## 7、this
### 1.this 的指向
#### ES5 中
> this 永远指向最后调用它的那个对象
#### ES6 箭头函数
> 箭头函数的 this 始终指向函数定义时的 this,而非执行时
### 2. 怎么改变 this 的指向
- 使用 ES6 的箭头函数
- 在函数内部使用 \_this = this
- 使用 apply、call、bind
- new 实例化一个对象
#### 例 1:
```
var name = "windowsName";
var a = {
name : "Cherry",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1()
},100);
}
};
a.func2() // this.func1 is not a function
```
> 在不使用箭头函数的情况下,是会报错的,因为最后调用 setTimeout 的对象是 window,但是在 window 中并没有 func1 函数。可以看做 window.setTimeout
#### 例 2:
```
var webName="long";
let func=()=>{
console.log(this.webName);
}
func();//long
```
> 箭头函数在全局作用域声明,所以它捕获全局作用域中的 this,this 指向 window 对象
#### 例 3:
```
var webName = "long";
function wrap(){
let func=() => {
console.log(this.webName);
}
func();
}
wrap();//long
```
> wrap 函数执行时,箭头函数 func 定义在 wrap 中,func 会找到它最近一层非箭头函数的 this
> 也就是 wrap 的 this,而 wrap 函数作用域中的 this 指向 window 对象。
### 3. 箭头函数
> 箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值(箭头函数本身没有 this,但是在它声明时可以捕获别人的 this 供自己使用。),如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined”。
#### 特点:
- 没有 this
- 没有 arguments
- 不能通过 new 关键字调用
- 没有 new.target
- 没有原型
- 没有 super
## 8、原型和原型链
### 1.prototype
> 每个函数都会有这个属性,这里强调,是函数,普通对象是没有这个属性的(这里为什么说普通对象呢,因为 JS 里面,一切皆为对象,所以这里的普通对象不包括函数对象)。它是构造函数的原型对象;
### 2._proto_
> 每个对象都有这个属性,这里强调,是对象,同样,因为函数也是对象,所以函数也有这个属性。它指向构造函数的原型对象;
### 3.constructor
> 原型对象上的一个指向构造函数的属性
```
var webName = "long";
// Pig的构造函数
function Pig(name, age) {
this.name = name;
this.age = age;
}
// 创建一个Pig的实例
var Peppa = new Pig('Peppa', 5);
Peppa.__proto__ === Pig.prototype。 //true
Pig.__proto__ === Function.prototype //true
Pig.prototype.constructor === Pig //true
```
## 9、深拷贝
```
function deepCopy(obj) {
if (typeof obj === 'object') {
var result = obj.constructor === Array ? [] : {};
for (var i in obj) {
result[i] = typeof obj[i] === 'object' ? deepCopy(obj[i]) : obj[i];
}
} else {
var result = obj;
}
return result;
}
```
## 10、对象
### 1.判断一个对象是不是空对象
(1)
```
var data = {};
var b = (JSON.stringify(data) == "{}"); //true
```
(2)
```
var obj = {};
var b = function() {
for(var key in obj) {
return false;
}
return true;
}
b();//true
```
(3) jquery 的 isEmptyObject 方法
```
var data = {};
var b = $.isEmptyObject(data);
alert(b);//true
```
(4) Object.getOwnPropertyNames()方法
```
var data = {};
var arr = Object.getOwnPropertyNames(data);
alert(arr.length == 0);//true
```
(5) ES6 的 Object.keys()方法
```
var data = {};
var arr = Object.keys(data);
alert(arr.length == 0);//true
```
## 11、new
> 1.创建了一个全新的对象。 2.这个对象会被执行[[Prototype]](也就是**proto**)链接。 3.生成的新对象会绑定到函数调用的 this。 4.通过 new 创建的每个对象将最终被[[Prototype]]链接到这个函数的 prototype 对象上。 5.如果函数没有返回对象类型 Object(包含 Functoin, Array, Date, RegExg, Error),那么 new 表达式中的函数调用会自动返回这个新的对象。
- new 操作符会返回一个对象,所以我们需要在内部创建一个对象
- 这个对象,也就是构造函数中的 this,可以访问到挂载在 this 上的任意属性
- 这个对象可以访问到构造函数原型上的属性,所以需要将对象与构造函数链接起来
- 返回原始值需要忽略,返回对象需要正常处理
```
function create(Con, ...args) {
let obj = {}
Object.setPrototypeOf(obj, Con.prototype)
let result = Con.apply(obj, args)
return result instanceof Object ? result : obj
}
```
**`注:setPrototypeOf相当于a._proto_ = b.prototype`**
**`注:也可以用a = Object.create(b.prototype)来实现`**
## 12、bind
### 例 1
```
var obj = {};
console.log(obj);
console.log(typeof Function.prototype.bind); // function
console.log(typeof Function.prototype.bind()); // function
console.log(Function.prototype.bind.name); // bind
console.log(Function.prototype.bind().name); // bound
```
### 结论 1
> 1.bind 是 Functoin 原型链中 Function.prototype 的一个属性,每个函数都可以调用它
> 2.bind 本身是一个函数名为 bind 的函数,返回值也是函数,函数名是 bound。
### 例 2
```
function original(a, b){
console.log(this.name);
console.log([a, b]);
return false;
}
var bound = original.bind(obj, 1);
var boundResult = bound(2); // '若川', [1, 2]
console.log(boundResult); // false
console.log(original.bind.name); // 'bind'
console.log(original.bind.length); // 1
console.log(original.bind().length); // 2 返回original函数的形参个数
console.log(bound.name); // 'bound original'
console.log((function(){}).bind().name); // 'bound '
console.log((function(){}).bind().length); // 0
```
### 结论 2
> 1.调用 bind 的函数中的 this 指向 bind()函数的第一个参数。 2.传给 bind()的其他参数接收处理了,bind()之后返回的函数的参数也接收处理了,也就是说合并处理了。 3.并且 bind()后的 name 为 bound + 空格 + 调用 bind 的函数名。如果是匿名函数则是 bound + 空格
> 4.bind 后的返回值函数,执行后返回值是原函数(original)的返回值。
> 5.bind 函数形参(即函数的 length)是 1。bind 后返回的 bound 函数形参不定,根据绑定的函数原函数(original)形参个数确定。
#### 基于结论 1,2 实现 bind
```
Function.prototype.bindFn = function bind(thisArg){
if(typeof this !== 'function'){
throw new TypeError(this + 'must be a function');
}
// 存储函数本身
var self = this;
// 去除thisArg的其他参数 转成数组
var args = [].slice.call(arguments, 1);
var bound = function(){
// bind返回的函数 的参数转成数组
var boundArgs = [].slice.call(arguments);
// apply修改this指向,把两个函数的参数合并传给self函数,并执行self函数,返回执行结果
return self.apply(thisArg, args.concat(boundArgs));
}
return bound;
}
// 测试
var obj = {
name: 'dj',
};
function original(a, b){
console.log(this.name);
console.log([a, b]);
}
var bound = original.bindFn(obj, 1);
bound(2); // 'dj', [1, 2]
```
### 例 3
#### new 来实例化的 bind()返回值函数实现(稍微有点麻烦,后期补充)
#### bound.name length 实现(后期补充)
## 13、call 和 apply
### 相同点
- 1、call 和 apply 的第一个参数 thisArg,都是 func 运行时指定的 this。而且,this 可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
**`严格模式下,函数的this值就是call和apply的第一个参数thisArg,非严格模式下,thisArg值被指定为 null 或 undefined 时this值会自动替换为指向全局对象,原始值则会被自动包装,也就是new Object()`**
**`例:doSth.call('0', 1, {a: 1}); // this 是 String('0') // [1, {a: 1}]`**
- 2、都可以只传递一个参数。
### 不同点:
> apply 只接收两个参数,第二个参数可以是数组也可以是类数组,其实也可以是对象,后续的参数忽略不计。call 接收第二个及以后一系列的参数。
### 手写实现 apply
```
Function.prototype.apply_new = function (thisObj) {
//调用的不是函数抛出异常
if (typeof this !== 'function') {
throw new TypeError(`${this} is not function`)
}
//第一个参数的this指向不传在非严格模式下为window
if (thisObj === null || typeof thisObj === 'undefined') {
thisObj = window
}
let args = arguments[1]
let result
thisObj.fn = this;
if(typeof args !=='object'){
throw new TypeError(`参数不是数组`)
}
if (args) {
result = thisObj.fn(...args)
}
else {
result = thisObj.fn()
}
delete thisObj.fn
return result
}
```
### 手写实现 call
```
Function.prototype.apply_new = function (thisObj) {
//调用的不是函数抛出异常
if (typeof this !== 'function') {
throw new TypeError(`${this} is not function`)
}
//第一个参数的this指向不传在非严格模式下为window
if (thisObj === null || typeof thisObj === 'undefined') {
thisObj = window
}
let args = [...arguments].slice(1)
let result
thisObj.fn = this;
result = thisObj.fn(...args)
delete thisObj.fn
return result
}
```
## 14、模块化
> 解决命名冲突, 提供复用性, 提高代码可维护性
### 1.立即执行函数
> 通过函数作用域解决了命名冲突、污染全局作用域的问题
```
(function(globalVariable){
globalVariable.test = function() {}
// ... 声明各种变量、函数都不会污染全局作用域
})(globalVariable)
```
### 2.AMD 和 CMD
```
// AMD
define(['./a', './b'],
function(a, b) {
// 加载模块完毕可以使用
a.do()
b.do()
})
// CMD
define(function(require, exports, module) {
// 加载模块
// 可以把 require 写在函数体的任意地方实现延迟加载
var a = require('./a')
a.doSomething()
})
```
**`目前这两种实现方式已经不用`**
### 3.CommonJS
> CommonJS 最早是 Node 在使用,目前也仍然广泛使用,比如在 Webpack 中你就能见到它
```
// a.js
module.exports = {
a: 1
}
// or
exports.a = 1
// b.js
var module = require('./a.js')
module.a // -> 1
```
```
var module = require('./a.js')
module.a
// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
a: 1
}
// module 基本实现
var module = {
id: 'xxxx', // 我总得知道怎么去找到他吧
exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
// 导出的东西
var a = 1
module.exports = a
return module.exports
};
// 然后当我 require 的时候去找到独特的
// id,然后将要使用的东西用立即执行函数包装下,over
```
### 4.ES Module
> ES Module 是原生实现的模块化方案
#### 与 CommonJS 有以下几个区别
- CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
- CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
- ES Module 会编译成 require/exports 来执行的
###### 三种使用方法
```
// 例1:
// 导出模块 API
export function a() {}
export const b = 1;
// 引入模块 API
import { a,b } from './a.js'
// 例2:
// 导出模块 API(一个文件只能有一个export default)
export default {}
// 引入模块 API(这里引入的变量a为自己定义,因为default导出一个变量)
import a from './a.js'
// 例3:
// 导出模块 API(一个文件只能有一个export default)
export default {}
// 引入模块 API
require('./a.js).default
```
## 15、Proxy
> Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。
```
let p = new Proxy(target, handler)
```
> target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数
### 通过 Proxy 来实现一个数据响应式
```
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver)
},
set(target, property, value, receiver) {
setBind(value, property)
return Reflect.set(target, property, value)
}
}
return new Proxy(obj, handler)
}
let obj = { a: 1 }
let p = onWatch(
obj,
(v, property) => {
console.log(`监听到属性${property}改变为${v}`)
},
(target, property) => {
console.log(`'${property}' = ${target[property]}`)
}
)
p.a = 2 // 监听到属性a改变
p.a // 'a' = 2
```
> Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷可能就是浏览器的兼容性不好了。
## 16、JS 异步编程
### 1.并发和并行的区别
> 并发是宏观概念,我分别有任务 A 和任务 B,在一段时间内通过任务间的切换完成了这两个任务,这种情况就可以称之为并发。
> 并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。
### 2.回调函数
```
ajax(url, () => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2, () => {
// 处理逻辑
})
})
})
```
> 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身
> 嵌套函数一多,就很难处理错误
> 不能使用 try catch 捕获错误,不能直接 return
### 3.Generator(es6)
```
function *foo(x) {
let y = 2 * (yield (x + 1))
let z = yield (y / 3)
return (x + y + z)
}
let it = foo(5)
console.log(it.next()) // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}
```
- Generator 函数调用和普通函数不同,它会返回一个迭代器
- 当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
- 当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 _ 12,所以第二个 yield 等于 2 _ 12 / 3 = 8
- 当执行第三次 next 时,传入的参数会传递给 z,所以 z = 13, x = 5, y = 24,相加等于 42
```
function *fetch() {
yield ajax(url, () => {})
yield ajax(url1, () => {})
yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
```
> 解决回调地狱问题
### 4.Promise(es6)
#### 状态
1. 等待中(pending)
2. 完成 (resolved)
3. 拒绝(rejected)
> 构造 Promise 的时候,构造函数内部的代码是立即执行的
```
new Promise((resolve, reject) => {
console.log('new Promise')
resolve('success')
})
console.log('finifsh')
// new Promise -> finifsh
```
> Promise 实现了链式调用,也就是说每次调用 then 之后返回的都是一个 Promise,并且是一个全新的 Promise,原因也是因为状态不可变。如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装
```
Promise.resolve(1)
.then(res => {
console.log(res) // => 1
return 2 // 包装成 Promise.resolve(2)
})
.then(res => {
console.log(res) // => 2
})
```
> Promise 也很好地解决了回调地狱的问题
```
ajax(url)
.then(res => {
console.log(res)
return ajax(url1)
}).then(res => {
console.log(res)
return ajax(url2)
}).then(res => console.log(res))
```
#### 缺点
> 无法取消 Promise,错误需要通过回调函数捕获。
#### 5.async 及 await
> 一个函数如果加上 async ,那么该函数就会返回一个 Promise
```
async function test() {
return "1"
}
console.log(test()) // -> Promise {: "1"}
test().then(res=>console.log(res)) //1
```
> 相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码,毕竟写一大堆 then 也很恶心,并且也能优雅地解决回调地狱问题
```
async function test() {
// 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
// 如果有依赖性的话,其实就是解决回调地狱的例子了
await fetch(url)
await fetch(url1)
await fetch(url2)
}
```
#### 缺点
> await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低
#### 例子
```
let a = 0
let b = async () => {
a = a + await 10
console.log('2', a) // -> '2' 10
}
b()
a++
console.log('1', a) // -> '1' 1
```
- 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为 await 内部实现了 generator ,generator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
- 因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码
- 同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10
> await 内部实现了 generator,其实 await 就是 generator 加上 Promise 的语法糖,且内部实现了自动执行 generator
### 6.定时器函数
> 常见的定时器函数有 setTimeout、setInterval、requestAnimationFrame
### setTimeout
> 因为 JS 是单线程执行的,如果前面的代码影响了性能,就会导致 setTimeout 不会按期执行
```
var date = new Date();
console.log(121)
console.log(121)
console.log(121)
console.log(121)
console.log(121)
console.log(121)
let x = 4;
if(x>2){
console.log(2)
}
//执行完同步代码后执行
setTimeout(() => {
var currentDate = new Date();
console.log(currentDate - date) //1001(本机测试)
},1000)
```
### setInterval
> 它和 setTimeout 一样,不能保证在预期的时间执行任务。它还存在执行累积的问题
### requestAnimationFrame
## 17、事件循环机制
### 1.进程、线程
> 1. 进程是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的。
> 2. 线程是进程的执行流,是 CPU 调度和分派的基本单位,同个进程之中的多个线程之间是共享该进程的资源的。
### 2.浏览器内核
> 浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程(也不一定,因为多个空白 tab 标签会合并成一个进程),浏览器内核(浏览器渲染进程)属于浏览器多进程中的一种。
> 浏览器内核有多种线程在工作。
>
> > - GUI 渲染线程: 负责渲染页面,解析 HTML,CSS 构成 DOM 树等,当页面重绘或者由于某种操作引起回流都会调起该程和 JS 引擎线程是互斥的,当 JS 引擎线程在工作的时候,GUI 渲染线程会被挂起,GUI 更新被放入在 JS 任务队列中,等待 JS 引擎线程空闲的时候继续执行。
> > - JS 引擎线程: 单线程工作,负责解析运行 JavaScript 脚本。和 GUI 渲染线程互斥,JS 运行耗时过长就会导致页面阻塞
> > - 事件触发线程: 当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到任务队列的队尾,等待 JS 引擎处理。
> > - 定时器触发线程: 浏览器定时计数器并不是由 JS 引擎计数的,阻塞会导致计时不准确。开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待 JS 引擎处理。
> > - http 请求线程: http 请求的时候会开启一条请求线程。请求完成有结果了之后,将请求的回调函数添加到任务队列中,等待 JS 引擎处理。
### 3.执行栈
> 可以执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则
> 当我们使用递归的时候,因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题
### 4.浏览器的 Event loop
> JS 代码的时候其实就是往执行栈中放入函数,当遇到异步的代码时,会被挂起并在需要执行的时候加入到 Task(有多种 Task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。

> 不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task。
```
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function () {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function () {
console.log('promise1')
})
.then(function () {
console.log('promise2')
})
console.log('script end')
// script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout
```
> - 微任务包括 process.nextTick ,promise ,MutationObserver,process.nextTick (Node 独有),
> - 宏任务包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI ,requestAnimationFrame (浏览器独有)
### 4.node 的 Event loop(node 版本 12 一下)
#### node 事件循环的阶段
```
┌───────────────────────────────────────────────────────┐
┌─>│ timers │ setTimeout/setInterval的回调
│ └──────────┬────────────────────────────────────────────┘
│ ↓
│ ┌──────────┴────────────────────────────────────────────┐
│ │ pending callbacks │ 处理网络、流、tcp的错误回调
│ └──────────┬────────────────────────────────────────────┘
│ ↓
│ ┌──────────┴────────────────────────────────────────────┐
│ │ idle, prepare │ 只在node内部使用
│ └──────────┬────────────────────────────────────────────┘
│ ↓ ┌───────────────┐
│ ┌──────────┴────────────────────────────────────────────┐ │ incoming: │
│ │ poll │ 执行poll中的i/o队列,检查定时器是否到时 <------│ connections,
│ └──────────┬────────────────────────────────────────────┘ │ data, etc. │
│ ↓ └───────────────┘
│ ┌──────────┴────────────────────────────────────────────┐
│ │ check │ 存放setImmediate回调
│ └──────────┬────────────────────────────────────────────┘
│ ↓
│ ┌──────────┴────────────────────────────────────────────┐
└──┤ close callbacks │ 关闭的回调(socket.on('close')...)
└───────────────────────────────────────────────────────┘
```
```
console.log(1)
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
})
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
setTimeout(() => {
console.log(9)
new Promise(resolve => {
console.log(11)
resolve()
}).then(() => {
console.log(12)
})
})
// 浏览器中的结果:1、7、8、2、4 , 5、9、11、12
// Node 中的结果:1、7、8、2、4 , 9、11、5、12 (版本12以上运行结果与浏览器一致)
```
> 在浏览器中 macro task 执行完成后,再次循环 宏任务 的回调队列之前,会优先处理 micro 中的任务。因此结果是: 1、7、8、2、4、5、9、11、12
> 在 Node 中有 6 个宏任务队列,事件循环首先进入 poll 阶段。进入 poll 阶段后查看是否有设定的 timers ( 定时器 )时间到达,如果有一个或多个时间到达, Event Loop 将会跳过正常的循环流程,直接从 timers 阶段执行,并执行 timers 回调队列,此时只有把 timers 阶段的回调队列执行完毕后。才会走下一个阶段,这也就是为什么 setTimeout 中有 .then,而没有被立即执行的原因,当 timers 阶段的回调队列执行完毕后,切换到下一个阶段这个过程中去触发 微任务(process.nextTick 和 .then) 。在阶段与阶段的切换之间。
## 18、图片懒加载
### 方式 1:
```
var images = document.getElementsByTagName("img");
window.addEventListener("scroll", (e) => {
changeImgSrc()
});
function changeImgSrc(){
for (let i of images) {
if (i.offsetTop <= window.innerHeight + window.scrollY) {
//获取自定义data-src属性的值
let trueSrc = i.getAttribute("data-src");
//把值赋值给图片的src属性
i.setAttribute("src", trueSrc);
}
}
}
```
### 方式 2:
- getBoundingClientRect().top 节点距离窗口的距离
```
var images = document.getElementsByTagName("img");
window.addEventListener("scroll", (e) => {
changeImgSrc()
});
function changeImgSrc(){
for (let i of images) {
if (i.getBoundingClientRect().top < window.innerHeight) {
//获取自定义data-src属性的值
let trueSrc = i.getAttribute("data-src");
//把值赋值给图片的src属性
i.setAttribute("src", trueSrc);
}
}
}
```
### 方式 3:
- Intersection Observer
- IntersectionObserver.observe()监听元素
- IntersectionObserver.unobserve() 停止监听
```
var images = document.getElementsByTagName("img");
function callback(entries) {
for (let i of entries) {
if (i.isIntersecting) {
let img = i.target;
let trueSrc = img.getAttribute("data-src");
img.setAttribute("src", trueSrc);
observer.unobserve(img);
}
}
}
const observer = new IntersectionObserver(callback);
for (let i of images) {
observer.observe(i);
}
```
## 19.异步加载图片
```
function loadByPromise(src) {
return new Promise((resolve,reject)=>{
const img = new Image()
img.onload=()=>{
resolve(img)
}
img.onerror = ()=>{
reject(new Error(''))
}
img.src = src
})
}
loadByPromise('https://img12.360buyimg.com/babel/s380x300_jfs/t1/152314/13/19839/57522/603e118dE941f0ce9/fdff58457adbef3e.jpg.webp').then(res=>{
document.appendChild(res)
})
```
## 20.防抖节流
### 1.防抖(Debounce)
- 防抖的原理是延迟一段时间吊起我们的函数。如果在这个时间段没有发生什么,函数正常进行,但是有内容发生变更后的一段时间触发函数。这就意味着,防抖函数只会在特定的时间之后被触发
```
function debounce(cb, delay = 250) {
let timeout
return (...args) => {
clearTimeout(timeout)
timeout = setTimeout(() => {
cb(...args)
}, delay)
}
}
const updateOptions = debounce(()=>{
console.log(1)
}, 300)
input.addEventListener("input", e => {
updateOptions(e.target.value)
)}
```
### 2.节流(Throttle )
- throttle 中的回调函数在函数执行后立马被调用,并且回调函数不在定时器函数内。回调函数要做的唯一事情就是将是否在时间内的执行的状态修改
```
function throttle(cb, delay = 250) {
let shouldWait = false
return (...args) => {
if (shouldWait) return
cb(...args)
shouldWait = true
setTimeout(() => {
shouldWait = false
}, delay)
}
}
```
```
//优化waitingargs
function throttle(cb, delay = 250) {
let shouldWait = false
let waitingargs
const timeoutFunc = ()=>{
if(!waitingargs){
shouldWait = false
}
else{
cb(...waitingargs)
waitingArgs = null
setTimeout(timeoutFunc, delay)
}
}
return (...args) => {
if (shouldWait){
waitingargs = args
}
cb(...args)
shouldWait = true
setTimeout(timeoutFunc, delay)
}
}
```