# 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 中的异步还是同步行为。 ![1](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/11/23/16740fa4cd9c6937~tplv-t2oaga2asx-watermark.awebp) > 不同的任务源会被分配到不同的 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) } } ```