网站建设的核心是免费搭建手机网站源码
- 作者: 五速梦信息网
- 时间: 2026年04月20日 07:52
当前位置: 首页 > news >正文
网站建设的核心是,免费搭建手机网站源码,七台河新闻联播2021,大良外贸网站设计类型及检测方式
- JS内置类型 JavaScript 的数据类型有下图所示 其中#xff0c;前 7 种类型为基础类型#xff0c;最后 1 种#xff08;Object#xff09;为引用类型#xff0c;也是你需要重点关注的#xff0c;因为它在日常工作中是使用得最频繁#xff0c;也是需要…类型及检测方式
- JS内置类型
JavaScript 的数据类型有下图所示 其中前 7 种类型为基础类型最后 1 种Object为引用类型也是你需要重点关注的因为它在日常工作中是使用得最频繁也是需要关注最多技术细节的数据类型 JavaScript一共有8种数据类型其中有7种基本数据类型Undefined、Null、Boolean、Number、String、Symboles6新增表示独一无二的值和BigIntes10新增1种引用数据类型——ObjectObject本质上是由一组无序的名值对组成的。里面包含 function、Array、Date等。JavaScript不支持任何创建自定义类型的机制而所有值最终都将是上述 8 种数据类型之一。 引用数据类型: 对象Object包含普通对象-Object数组对象-Array正则对象-RegExp日期对象-Date数学函数-Math函数对象-Function 在这里我想先请你重点了解下面两点因为各种 JavaScript 的数据类型最后都会在初始化之后放在不同的内存中因此上面的数据类型大致可以分成两类来进行存储 原始数据类型 基础类型存储在栈内存被引用或拷贝时会创建一个完全相等的变量占据空间小、大小固定属于被频繁使用数据所以放入栈中存储。引用数据类型 引用类型存储在堆内存存储的是地址多个引用指向同一个地址这里会涉及一个“共享”的概念占据空间大、大小不固定。引用数据类型在栈中存储了指针该指针指向堆中该实体的起始地址。当解释器寻找引用值时会首先检索其在栈中的地址取得地址后从堆中获得实体。
JavaScript 中的数据是如何存储在内存中的 在 JavaScript 中原始类型的赋值会完整复制变量值而引用类型的赋值是复制引用地址。 在 JavaScript 的执行过程中 主要有三种类型内存空间分别是代码空间、栈空间、堆空间。其中的代码空间主要是存储可执行代码的原始类型(Number、String、Null、Undefined、Boolean、Symbol、BigInt)的数据值都是直接保存在“栈”中的引用类型(Object)的值是存放在“堆”中的。因此在栈空间中(执行上下文)原始类型存储的是变量的值而引用类型存储的是其在堆空间中的地址当 JavaScript 需要访问该数据的时候是通过栈中的引用地址来访问的相当于多了一道转手流程。
在编译过程中如果 JavaScript 引擎判断到一个闭包也会在堆空间创建换一个“closure(fn)”的对象这是一个内部对象JavaScript 是无法访问的用来保存闭包中的变量。所以闭包中的变量是存储在“堆空间”中的。
JavaScript 引擎需要用栈来维护程序执行期间上下文的状态如果栈空间大了话所有的数据都存放在栈空间里面那么会影响到上下文切换的效率进而又影响到整个程序的执行效率。通常情况下栈空间都不会设置太大主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大所以这一类数据会被存放到堆中堆空间很大能存放很多大的数据不过缺点是分配内存和回收内存都会占用一定的时间。因此需要“栈”和“堆”两种空间。 题目一初出茅庐 let a {name: lee,age: 18
}
let b a;
console.log(a.name); //第一个console
b.name son;
console.log(a.name); //第二个console
console.log(b.name); //第三个console这道题比较简单我们可以看到第一个 console 打出来 name 是 ‘lee’这应该没什么疑问但是在执行了 b.name‘son’ 之后结果你会发现 a 和 b 的属性 name 都是 ‘son’第二个和第三个打印结果是一样的这里就体现了引用类型的“共享”的特性即这两个值都存在同一块内存中共享一个发生了改变另外一个也随之跟着变化。 你可以直接在 Chrome 控制台敲一遍深入理解一下这部分概念。下面我们再看一段代码它是比题目一稍复杂一些的对象属性变化问题。 题目二渐入佳境 let a {name: Julia,age: 20
}
function change(o) {o.age 24;o {name: Kath,age: 30}return o;
}
let b change(a); // 注意这里没有new后面new相关会有专门文章讲解
console.log(b.age); // 第一个console
console.log(a.age); // 第二个console这道题涉及了 function你通过上述代码可以看到第一个 console 的结果是 30b 最后打印结果是 {name: Kath, age: 30}第二个 console 的返回结果是 24而 a 最后的打印结果是 {name: Julia, age: 24}。
是不是和你预想的有些区别你要注意的是这里的 function 和 return 带来了不一样的东西。 原因在于函数传参进来的 o传递的是对象在堆中的内存地址值通过调用 o.age 24第 7 行代码确实改变了 a 对象的 age 属性但是第 12 行代码的 return 却又把 o 变成了另一个内存地址将 {name: Kath, age: 30} 存入其中最后返回 b 的值就变成了 {name: Kath, age: 30}。而如果把第 12 行去掉那么 b 就会返回 undefined 2. 数据类型检测
1typeof typeof 对于原始类型来说除了 null 都可以显示正确的类型 console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof str); // string
console.log(typeof []); // object []数组的数据类型在 typeof 中被解释为 object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
console.log(typeof undefined); // undefined
console.log(typeof null); // object null 的数据类型被 typeof 解释为 objecttypeof 对于对象来说除了函数都会显示 object所以说 typeof 并不能准确判断变量到底是什么类型,所以想判断一个对象的正确类型这时候可以考虑使用 instanceof 2instanceof instanceof 可以正确的判断对象的类型因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log(str instanceof String); // false
console.log([] instanceof Array); // true console.log(function(){} instanceof Function); // true console.log({} instanceof Object); // true
// console.log(undefined instanceof Undefined); // console.log(null instanceof Null);instanceof 可以准确地判断复杂引用数据类型但是不能正确判断基础数据类型而 typeof 也存在弊端它虽然可以判断基础数据类型null 除外但是引用数据类型中除了 function 类型以外其他的也无法判断 // 我们也可以试着实现一下 instanceof function _instanceof(left, right) {// 由于instance要检测的是某对象需要有一个前置判断条件//基本数据类型直接返回falseif(typeof left ! object || left null) return false;// 获得类型的原型let prototype right.prototype// 获得对象的原型left left.proto// 判断对象的类型是否等于类型的原型while (true) {if (left null)return falseif (prototype left)return trueleft left.proto} }console.log(test, _instanceof(null, Array)) // false console.log(test, _instanceof([], Array)) // true console.log(test, _instanceof(, Array)) // false console.log(test, _instanceof({}, Object)) // true3constructor console.log((2).constructor Number); // true console.log((true).constructor Boolean); // true console.log((str).constructor String); // true console.log(([]).constructor Array); // true console.log((function() {}).constructor Function); // true console.log(({}).constructor Object); // true这里有一个坑如果我创建一个对象更改它的原型constructor就会变得不可靠了 function Fn(){};Fn.prototypenew Array();var fnew Fn();console.log(f.constructorFn); // false console.log(f.constructorArray); // true 4Object.prototype.toString.call() toString() 是 Object 的原型方法调用该方法可以统一返回格式为 “[object Xxx]” 的字符串其中 Xxx 就是对象的类型。对于 Object 对象直接调用 toString() 就能返回 [object Object]而对于其他对象则需要通过 call 来调用才能返回正确的类型信息。我们来看一下代码。 Object.prototype.toString({}) // [object Object] Object.prototype.toString.call({}) // 同上结果加上call也ok Object.prototype.toString.call(1) // [object Number] Object.prototype.toString.call(1) // [object String] Object.prototype.toString.call(true) // [object Boolean] Object.prototype.toString.call(function(){}) // [object Function] Object.prototype.toString.call(null) //[object Null] Object.prototype.toString.call(undefined) //[object Undefined] Object.prototype.toString.call(/123/g) //[object RegExp] Object.prototype.toString.call(new Date()) //[object Date] Object.prototype.toString.call([]) //[object Array] Object.prototype.toString.call(document) //[object HTMLDocument] Object.prototype.toString.call(window) //[object Window]// 从上面这段代码可以看出Object.prototype.toString.call() 可以很好地判断引用类型甚至可以把 document 和 window 都区分开来。实现一个全局通用的数据类型判断方法来加深你的理解代码如下 function getType(obj){let type typeof obj;if (type ! object) { // 先进行typeof判断如果是基础数据类型直接返回return type;}// 对于typeof返回结果是object的再进行如下的判断正则返回结果return Object.prototype.toString.call(obj).replace(/^[object (\S)]\(/, \)1); // 注意正则中间有个空格 } /* 代码验证需要注意大小写哪些是typeof判断哪些是toString判断思考下 */ getType([]) // Array typeof []是object因此toString返回 getType(123) // string typeof 直接返回 getType(window) // Window toString返回 getType(null) // Null首字母大写typeof null是object需toString来判断 getType(undefined) // undefined typeof 直接返回 getType() // undefined typeof 直接返回 getType(function(){}) // function typeof能判断因此首字母小写 getType(/123/g) //RegExp toString返回小结 typeof 直接在计算机底层基于数据类型的值二进制进行检测typeof null为object 原因是对象存在在计算机中都是以000开始的二进制存储所以检测出来的结果是对象typeof 普通对象/数组对象/正则对象/日期对象 都是objecttypeof NaN number instanceof 检测当前实例是否属于这个类的底层机制只要当前类出现在实例的原型上结果都是true不能检测基本数据类型 constructor 支持基本类型constructor可以随便改也不准 Object.prototype.toString.call([val]) 返回当前实例所属类信息 判断 Target 的类型单单用 typeof 并无法完全满足这其实并不是 bug本质原因是 JS 的万物皆对象的理论。因此要真正完美判断时我们需要区分对待: 基本类型(null): 使用 String(null)基本类型(string / number / boolean / undefined) function: - 直接使用 typeof即可其余引用类型(Array / Date / RegExp Error): 调用toString后根据[object XXX]进行判断 - 数据类型转换 我们先看一段代码了解下大致的情况。 123 123 // false or true?null // false or true?0 // false or true? [] 0 // false or true? [] // false or true? [] ![] // false or true? null undefined // false or true? Number(null) // 返回什么 Number() // 返回什么 parseInt(); // 返回什么 {}10 // 返回什么 let obj {Symbol.toPrimitive {return 200;},valueOf() {return 300;},toString() {return Hello;} } console.log(obj 200); // 这里打印出来是多少首先我们要知道在 JS 中类型转换只有三种情况分别是 转换为布尔值转换为数字转换为字符串 转Boolean 在条件判断时除了 undefinednull false NaN 0 -0其他所有值都转为 true包括所有对象 Boolean(0) //false Boolean(null) //false Boolean(undefined) //false Boolean(NaN) //false Boolean(1) //true Boolean(13) //true Boolean(12) //true对象转原始类型 对象在转换类型的时候会调用内置的 [[ToPrimitive]] 函数对于该函数来说算法逻辑一般来说如下 如果已经是原始类型了那就不需要转换了调用 x.valueOf()如果转换为基础类型就返回转换的值调用 x.toString()如果转换为基础类型就返回转换的值如果都没有返回原始类型就会报错 当然你也可以重写 Symbol.toPrimitive该方法在转原始类型时调用优先级最高。 let a {valueOf() {return 0},toString() {return 1},Symbol.toPrimitive {return 2} } 1 a // 3四则运算符 它有以下几个特点 运算中其中一方为字符串那么就会把另一方也转换为字符串如果一方不是字符串或者数字那么会将它转换为数字或者字符串 1 1 // 11 true true // 2 4 [1,2,3] // 41,2,3对于第一行代码来说触发特点一所以将数字 1 转换为字符串得到结果 11对于第二行代码来说触发特点二所以将 true 转为数字 1对于第三行代码来说触发特点二所以将数组通过 toString转为字符串 1,2,3得到结果 41,2,3 另外对于加法还需要注意这个表达式 a b a b // - aNaN因为 b 等于 NaN所以结果为 aNaN你可能也会在一些代码中看到过 1的形式来快速获取 number 类型。那么对于除了加法的运算符来说只要其中一方是数字那么另一方就会被转为数字 4 * 3 // 12 4 * [] // 0 4 * [1, 2] // NaN比较运算符 如果是对象就通过 toPrimitive 转换对象如果是字符串就通过 unicode 字符索引来比较 let a {valueOf() {return 0},toString() {return 1} } a -1 // true在以上代码中因为 a 是对象所以会通过 valueOf 转换为原始类型再比较值。 强制类型转换 强制类型转换方式包括 Number()、parseInt()、parseFloat()、toString()、String()、Boolean()这几种方法都比较类似 Number() 方法的强制转换规则如果是布尔值true 和 false 分别被转换为 1 和 0如果是数字返回自身如果是 null返回 0如果是 undefined返回 NaN如果是字符串遵循以下规则如果字符串中只包含数字或者是 0X / 0x 开头的十六进制数字字符串允许包含正负号则将其转换为十进制如果字符串中包含有效的浮点格式将其转换为浮点数值如果是空字符串将其转换为 0如果不是以上格式的字符串均返回 NaN如果是 Symbol抛出错误如果是对象并且部署了 [Symbol.toPrimitive] 那么调用此方法否则调用对象的 valueOf() 方法然后依据前面的规则转换返回的值如果转换的结果是 NaN 则调用对象的 toString() 方法再次依照前面的顺序转换返回对应的值。 Number(true); // 1 Number(false); // 0 Number(0111); //111 Number(null); //0 Number(); //0 Number(1a); //NaN Number(-0X11); //-17 Number(0X11) //17Object 的转换规则 对象转换的规则会先调用内置的 [ToPrimitive] 函数其规则逻辑如下 如果部署了 Symbol.toPrimitive 方法优先调用再返回调用 valueOf()如果转换为基础类型则返回调用 toString()如果转换为基础类型则返回如果都没有返回基础类型会报错。 var obj {value: 1,valueOf() {return 2;},toString() {return 3},Symbol.toPrimitive {return 4} } console.log(obj 1); // 输出5 // 因为有Symbol.toPrimitive就优先执行这个如果Symbol.toPrimitive这段代码删掉则执行valueOf打印结果为3如果valueOf也去掉则调用toString返回31(字符串拼接) // 再看两个特殊的case 10 {} // 10[object Object]注意{}会默认调用valueOf是{}不是基础类型继续转换调用toString返回结果[object Object]于是和10进行运算按照字符串拼接规则来参考的规则C [1,2,undefined,4,5] 10 // 1,2,,4,510注意[1,2,undefined,4,5]会默认先调用valueOf结果还是这个数组不是基础数据类型继续转换也还是调用toString返回1,2,,4,5然后再和10进行运算还是按照字符串拼接规则参考的第3条规则‘’ 的隐式类型转换规则 如果类型相同无须进行类型转换如果其中一个操作值是 null 或者 undefined那么另一个操作符必须为 null 或者 undefined才会返回 true否则都返回 false如果其中一个是 Symbol 类型那么返回 false两个操作值如果为 string 和 number 类型那么就会将字符串转换为 number如果一个操作值是 boolean那么转换成 number如果一个操作值为 object 且另一方为 string、number 或者 symbol就会把 object 转为原始类型再进行判断调用 object 的 valueOf/toString 方法进行转换。 null undefined // true 规则2 null 0 // false 规则2null // false 规则20 // true 规则4 字符串转隐式转换成Number之后再对比 123 123 // true 规则4 字符串转隐式转换成Number之后再对比 0 false // true e规则 布尔型隐式转换成Number之后再对比 1 true // true e规则 布尔型隐式转换成Number之后再对比 var a {value: 0,valueOf: function() {this.value;return this.value;} }; // 注意这里a又可以等于1、2、3 console.log(a 1 a 2 a 3); //true f规则 Object隐式转换 // 注但是执行过3遍之后再重新执行a3或之前的数字就是false因为value已经加上去了这里需要注意一下‘’ 的隐式类型转换规则 ‘’ 号操作符不仅可以用作数字相加还可以用作字符串拼接。仅当 ‘’ 号两边都是数字时进行的是加法运算如果两边都是字符串则直接拼接无须进行隐式类型转换。 如果其中有一个是字符串另外一个是 undefined、null 或布尔型则调用 toString() 方法进行字符串拼接如果是纯对象、数组、正则等则默认调用对象的转换方法会存在优先级然后再进行拼接。如果其中有一个是数字另外一个是 undefined、null、布尔型或数字则会将其转换成数字进行加法运算对象的情况还是参考上一条规则。如果其中一个是字符串、一个是数字则按照字符串规则进行拼接 1 2 // 3 常规情况 1 2 // 12 常规情况 // 下面看一下特殊情况 1 undefined // 1undefined 规则1undefined转换字符串 1 null // 1null 规则1null转换字符串 1 true // 1true 规则1true转换字符串 1 1n // 11 比较特殊字符串和BigInt相加BigInt转换为字符串 1 undefined // NaN 规则2undefined转换数字相加NaN 1 null // 1 规则2null转换为0 1 true // 2 规则2true转换为1二者相加为2 1 1n // 错误 不能把BigInt和Number类型直接混合相加 1 3 // 13 规则3字符串拼接整体来看如果数据中有字符串JavaScript 类型转换还是更倾向于转换成字符串因为第三条规则中可以看到在字符串和数字相加的过程中最后返回的还是字符串这里需要关注一下 null 和 undefined 的区别 首先 Undefined 和 Null 都是基本数据类型这两个基本数据类型分别都只有一个值就是 undefined 和 null。undefined 代表的含义是未定义 null 代表的含义是空对象其实不是真的对象请看下面的注意。一般变量声明了但还没有定义的时候会返回 undefinednull 主要用于赋值给一些可能会返回对象的变量作为初始化。 其实 null 不是对象虽然 typeof null 会输出 object但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统为了性能考虑使用低位存储变量的类型信息000 开头代表是对象然而 null 表示为全零所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了但是对于这个 Bug 却是一直流传下来。 undefined 在 js 中不是一个保留字这意味着我们可以使用 undefined 来作为一个变量名这样的做法是非常危险的它会影响我们对 undefined 值的判断。但是我们可以通过一些方法获得安全的 undefined 值比如说 void 0。当我们对两种类型使用 typeof 进行判断的时候Null 类型化会返回 “object”这是一个历史遗留的问题。当我们使用双等号对两种类型的值进行比较时会返回 true使用三个等号时会返回 false。 ——————– This 不同情况的调用this指向分别如何。顺带可以提一下 es6 中箭头函数没有 this, arguments, super 等这些只依赖包含箭头函数最接近的函数 我们先来看几个函数调用的场景 function foo() {console.log(this.a) } var a 1 foo()const obj {a: 2,foo: foo } obj.foo()const c new foo()对于直接调用 foo 来说不管 foo 函数被放在了什么地方this 一定是window对于 obj.foo() 来说我们只需要记住谁调用了函数谁就是 this所以在这个场景下 foo 函数中的 this 就是 obj 对象对于 new 的方式来说this 被永远绑定在了 c 上面不会被任何方式改变 this 说完了以上几种情况其实很多代码中的 this 应该就没什么问题了下面让我们看看箭头函数中的 this function a() {return () {return () {console.log(this)}} } console.log(a()()())首先箭头函数其实是没有 this 的箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this。在这个例子中因为包裹箭头函数的第一个普通函数是 a所以此时的 this 是 window。另外对箭头函数使用 bind这类函数是无效的。最后种情况也就是 bind 这些改变上下文的 API 了对于这些函数来说this 取决于第一个参数如果第一个参数为空那么就是 window。那么说到 bind不知道大家是否考虑过如果对一个函数进行多次 bind那么上下文会是什么呢 let a {} let fn function () { console.log(this) } fn.bind().bind(a)() // ?如果你认为输出结果是 a那么你就错了其实我们可以把上述代码转换成另一种形式 // fn.bind().bind(a) 等于 let fn2 function fn1() {return function() {return fn.apply()}.apply(a) } fn2()可以从上述代码中发现不管我们给函数 bind 几次fn 中的 this 永远由第一次 bind 决定所以结果永远是 window let a { name: poetries } function foo() {console.log(this.name) } foo.bind(a)() // poetries以上就是 this 的规则了但是可能会发生多个规则同时出现的情况这时候不同的规则之间会根据优先级最高的来决定 this 最终指向哪里。 首先new 的方式优先级最高接下来是 bind 这些函数然后是 obj.foo() 这种调用方式最后是 foo 这种调用方式同时箭头函数的 this 一旦被绑定就不会再被任何方式所改变。 函数执行改变this 由于 JS 的设计原理: 在函数中可以引用运行环境中的变量。因此就需要一个机制来让我们可以在函数体内部获取当前的运行环境这便是this。 因此要明白 this 指向其实就是要搞清楚 函数的运行环境说人话就是谁调用了函数。例如 obj.fn()便是 obj 调用了函数既函数中的 this objfn()这里可以看成 window.fn()因此 this window 但这种机制并不完全能满足我们的业务需求因此提供了三种方式可以手动修改 this 的指向: call: fn.call(target, 1, 2)apply: fn.apply(target, [1, 2])bind: fn.bind(target)(1,2) ——————– apply/call/bind 原理 call、apply 和 bind 是挂在 Function 对象上的三个方法调用这三个方法的必须是一个函数。 func.call(thisArg, param1, param2, …) func.apply(thisArg, [param1,param2,…]) func.bind(thisArg, param1, param2, …)在浏览器里在全局范围内this 指向window对象在函数中this永远指向最后调用他的那个对象构造函数中this指向new出来的那个新的对象call、apply、bind中的this被强绑定在指定的那个对象上箭头函数中this比较特殊,箭头函数this为父作用域的this不是调用时的this.要知道前四种方式,都是调用时确定,也就是动态的,而箭头函数的this指向是静态的,声明的时候就确定了下来apply、call、bind都是js给函数内置的一些API调用他们可以为函数指定this的执行,同时也可以传参。 let a {value: 1 } function getValue(name, age) {console.log(name)console.log(age)console.log(this.value) } getValue.call(a, poe, 24) getValue.apply(a, [poe, 24])bind 和其他两个方法作用也是一致的只是该方法会返回一个函数。并且我们可以通过 bind 实现柯里化 方法的应用场景 下面几种应用场景你多加体会就可以发现它们的理念都是“借用”方法的思路。我们来看看都有哪些。 判断数据类型 用 Object.prototype.toString 来判断类型是最合适的借用它我们几乎可以判断所有类型的数据 function getType(obj){let type typeof obj;if (type ! object) {return type;}return Object.prototype.toString.call(obj).replace(/^\(/, \)1); }类数组借用方法 类数组因为不是真正的数组所有没有数组类型上自带的种种方法所以我们就可以利用一些方法去借用数组的方法比如借用数组的 push 方法看下面的一段代码。 var arrayLike { 0: java,1: script,length: 2 } Array.prototype.push.call(arrayLike, jack, lily); console.log(typeof arrayLike); // object console.log(arrayLike); // {0: java, 1: script, 2: jack, 3: lily, length: 4}用 call 的方法来借用 Array 原型链上的 push 方法可以实现一个类数组的 push 方法给 arrayLike 添加新的元素 获取数组的最大 / 最小值 我们可以用 apply 来实现数组中判断最大 / 最小值apply 直接传递数组作为调用方法的参数也可以减少一步展开数组可以直接使用 Math.max、Math.min 来获取数组的最大值 / 最小值请看下面这段代码。 let arr [13, 6, 10, 11, 16]; const max Math.max.apply(Math, arr); const min Math.min.apply(Math, arr);console.log(max); // 16 console.log(min); // 6实现一个 bind 函数 对于实现以下几个函数可以从几个方面思考 不传入第一个参数那么默认为 window改变了 this 指向让新的对象可以执行该函数。那么思路是否可以变成给新的对象添加一个函数然后在执行完以后删除 Function.prototype.myBind function (context) {if (typeof this ! function) {throw new TypeError(Error)}var _this thisvar args […arguments].slice(1)// 返回一个函数return function F() {// 因为返回了一个函数我们可以 new F()所以需要判断if (this instanceof F) {return new _this(…args, …arguments)}return _this.apply(context, args.concat(…arguments))} }实现一个 call 函数 Function.prototype.myCall function (context) {var context context || window// 给 context 添加一个属性// getValue.call(a, pp, 24) a.fn getValuecontext.fn this// 将 context 后面的参数取出来var args […arguments].slice(1)// getValue.call(a, pp, 24) a.fn(pp, 24)var result context.fn(…args)// 删除 fndelete context.fnreturn result }实现一个 apply 函数 Function.prototype.myApply function(context window, …args) {// this–func context– obj args– 传递过来的参数// 在context上加一个唯一值不影响context上的属性let key Symbol(key)context[key] this; // context为调用的上下文,this此处为函数将这个函数作为context的方法// let args […arguments].slice(1) //第一个参数为obj所以删除,伪数组转为数组let result contextkey; delete context[key]; // 不删除会导致context属性越来越多return result; }// 使用 function f(a,b){console.log(a,b)console.log(this.name) } let obj{name:张三 } f.myApply(obj,[1,2]) //arguments[1]——————– 变量提升 当执行 JS 代码时会生成执行环境只要代码不是写在函数中的就是在全局执行环境中函数中的代码会产生函数执行环境只此两种执行环境。 b() // call b console.log(a) // undefinedvar a Hello worldfunction b() {console.log(call b) }想必以上的输出大家肯定都已经明白了这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部这其实没有什么错误便于大家理解。但是更准确的解释应该是在生成执行环境时会有两个阶段。第一个阶段是创建的阶段JS 解释器会找出需要提升的变量和函数并且给他们提前在内存中开辟好空间函数的话会将整个函数存入内存中变量只声明并且赋值为 undefined所以在第二个阶段也就是代码执行阶段我们可以直接提前使用 在提升的过程中相同的函数会覆盖上一个函数并且函数优先于变量提升 b() // call b secondfunction b() {console.log(call b fist) } function b() {console.log(call b second) } var b Hello worldvar 会产生很多错误所以在 ES6中引入了 let。let不能在声明前使用但是这并不是常说的 let 不会提升let提升了在第一阶段内存也已经为他开辟好了空间但是因为这个声明的特性导致了并不能在声明前使用 ——————– 执行上下文 当执行 JS 代码时会产生三种执行上下文 全局执行上下文函数执行上下文eval 执行上下文 每个执行上下文中都有三个重要的属性 变量对象VO包含变量、函数声明和函数的形参该属性只能在全局上下文中访问作用域链JS 采用词法作用域也就是说变量的作用域是在定义时就决定了this var a 10 function foo(i) {var b 20 } foo()对于上述代码执行栈中有两个上下文全局上下文和函数 foo 上下文。 stack [globalContext,fooContext ]对于全局上下文来说VO大概是这样的 globalContext.VO globe globalContext.VO {a: undefined,foo: Function, }对于函数 foo 来说VO 不能访问只能访问到活动对象AO fooContext.VO foo.AO fooContext.AO {i: undefined,b: undefined,arguments: } // arguments 是函数独有的对象(箭头函数没有) // 该对象是一个伪数组有 length 属性且可以通过下标访问元素 // 该对象中的 callee 属性代表函数本身 // caller 属性代表函数的调用者对于作用域链可以把它理解成包含自身变量对象和上级变量对象的列表通过 [[Scope]]属性查找上级变量 fooContext.[[Scope]] [globalContext.VO ] fooContext.Scope fooContext.[[Scope]] fooContext.VO fooContext.Scope [fooContext.VO,globalContext.VO ]接下来让我们看一个老生常谈的例子var b() // call b console.log(a) // undefinedvar a Hello worldfunction b() {console.log(call b) }想必以上的输出大家肯定都已经明白了这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部这其实没有什么错误便于大家理解。但是更准确的解释应该是在生成执行上下文时会有两个阶段。第一个阶段是创建的阶段具体步骤是创建 VOJS 解释器会找出需要提升的变量和函数并且给他们提前在内存中开辟好空间函数的话会将整个函数存入内存中变量只声明并且赋值为 undefined所以在第二个阶段也就是代码执行阶段我们可以直接提前使用。 在提升的过程中相同的函数会覆盖上一个函数并且函数优先于变量提升 b() // call b secondfunction b() {console.log(call b fist) } function b() {console.log(call b second) } var b Hello worldvar会产生很多错误所以在 ES6中引入了 let。let不能在声明前使用但是这并不是常说的 let 不会提升let 提升了声明但没有赋值因为临时死区导致了并不能在声明前使用。 对于非匿名的立即执行函数需要注意以下一点 var foo 1 (function foo() {foo 10console.log(foo) }()) // - ƒ foo() { foo 10 ; console.log(foo) }因为当 JS 解释器在遇到非匿名的立即执行函数时会创建一个辅助的特定对象然后将函数名称作为这个对象的属性因此函数内部才可以访问到 foo但是这个值又是只读的所以对它的赋值并不生效所以打印的结果还是这个函数并且外部的值也没有发生更改。 specialObject {};Scope specialObject Scope;foo new FunctionExpression; foo.[[Scope]] Scope; specialObject.foo foo; // {DontDelete}, {ReadOnly}delete Scope[0]; // remove specialObject from the front of scope chain总结 执行上下文可以简单理解为一个对象: 它包含三个部分: 变量对象(VO)作用域链(词法作用域)this指向 它的类型: 全局执行上下文函数执行上下文eval执行上下文 代码执行过程: 创建 全局上下文 (global EC)全局执行上下文 (caller) 逐行 自上而下 执行。遇到函数时函数执行上下文 (callee) 被push到执行栈顶层函数执行上下文被激活成为 active EC, 开始执行函数中的代码caller 被挂起函数执行完后callee 被pop移除出执行栈控制权交还全局上下文 (caller)继续执行 ——————– 作用域 作用域 作用域是定义变量的区域它有一套访问变量的规则这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量标识符进行变量查找作用域链 作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问通过作用域链我们可以访问到外层环境的变量和 函数。 作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前 端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象也就是全局对象始终是作用域链的最后一个对象。 当我们查找一个变量时如果当前执行环境中没有找到我们可以沿着作用域链向后查找作用域链的创建过程跟执行上下文的建立有关… 作用域可以理解为变量的可访问性总共分为三种类型分别为 全局作用域函数作用域块级作用域ES6 中的 let、const 就可以产生该作用域 其实看完前面的闭包、this 这部分内部的话应该基本能了解作用域的一些应用。 一旦我们将这些作用域嵌套起来就变成了另外一个重要的知识点「作用域链」也就是 JS 到底是如何访问需要的变量或者函数的。 首先作用域链是在定义时就被确定下来的和箭头函数里的 this 一样后续不会改变JS 会一层层往上寻找需要的内容。其实作用域链这个东西我们在闭包小结中已经看到过它的实体了[[Scopes]] 图中的 [[Scopes]] 是个数组作用域的一层层往上寻找就等同于遍历 [[Scopes]]。
- 全局作用域 全局变量是挂载在 window 对象下的变量所以在网页中的任何位置你都可以使用并且访问到这个全局变量 var globalName global; function getName() { console.log(globalName) // globalvar name innerconsole.log(name) // inner } getName(); console.log(name); // console.log(globalName); //global function setName(){ vName setName; } setName(); console.log(vName); // setName从这段代码中我们可以看到globalName 这个变量无论在什么地方都是可以被访问到的所以它就是全局变量。而在 getName 函数中作为局部变量的 name 变量是不具备这种能力的当然全局作用域有相应的缺点我们定义很多全局变量的时候会容易引起变量命名的冲突所以在定义变量的时候应该注意作用域的问题。
- 函数作用域 函数中定义的变量叫作函数变量这个时候只能在函数内部才能访问到它所以它的作用域也就是函数的内部称为函数作用域 function getName () {var name inner;console.log(name); //inner } getName(); console.log(name);除了这个函数内部其他地方都是不能访问到它的。同时当这个函数被执行完之后这个局部变量也相应会被销毁。所以你会看到在 getName 函数外面的 name 是访问不到的 3. 块级作用域 ES6 中新增了块级作用域最直接的表现就是新增的 let 关键词使用 let 关键词定义的变量只能在块级作用域中被访问有“暂时性死区”的特点也就是说这个变量在定义之前是不能被使用的。 在 JS 编码过程中 if 语句及 for 语句后面 {…} 这里面所包括的就是块级作用域 console.log(a) //a is not defined if(true){let a 123console.log(a) // 123 } console.log(a) //a is not defined从这段代码可以看出变量 a 是在 if 语句{…} 中由 let 关键词进行定义的变量所以它的作用域是 if 语句括号中的那部分而在外面进行访问 a 变量是会报错的因为这里不是它的作用域。所以在 if 代码块的前后输出 a 这个变量的结果控制台会显示 a 并没有定义 ——————– 闭包 闭包其实就是一个可以访问其他函数内部变量的函数。创建闭包的最常见的方式就是在一个函数内创建另一个函数创建的函数可以 访问到当前函数的局部变量。 因为通常情况下函数内部变量是无法在外部访问的即全局变量和局部变量的区别因此使用闭包的作用就具备实现了能在外部访问某个函数内部变量的功能让这些内部变量的值始终可以保存在内存中。下面我们通过代码先来看一个简单的例子 function fun1() {var a 1;return function(){console.log(a);}; } fun1(); var result fun1(); result(); // 1// 结合闭包的概念我们把这段代码放到控制台执行一下就可以发现最后输出的结果是 1即 a 变量的值。那么可以很清楚地发现a 变量作为一个 fun1 函数的内部变量正常情况下作为函数内的局部变量是无法被外部访问到的。但是通过闭包我们最后还是可以拿到 a 变量的值闭包有两个常用的用途 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包我们可以通过在外部调用闭包函数从而在外部访问到函数内部的变量可以使用这种方法来创建私有变量。函数的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中因为闭包函数保留了这个变量对象的引用所以这个变量对象不会被回收。 其实闭包的本质就是作用域链的一个特殊的应用只要了解了作用域链的创建过程就能够理解闭包的实现原理。 let a 1 // fn 是闭包 function fn() {console.log(a); }function fn1() {let a 1// 这里也是闭包return () {console.log(a);} } const fn2 fn1() fn2()大家都知道闭包其中一个作用是访问私有变量就比如上述代码中的 fn2 访问到了 fn1 函数中的变量 a。但是此时 fn1 早已销毁我们是如何访问到变量 a 的呢不是都说原始类型是存放在栈上的么为什么此时却没有被销毁掉接下来笔者会根据浏览器的表现来重新理解关于原始类型存放位置的说法。先来说下数据存放的正确规则是局部、占用空间确定的数据一般会存放在栈中否则就在堆中也有例外。 那么接下来我们可以通过 Chrome 来帮助我们验证这个说法说法。 上图中画红框的位置我们能看到一个内部的对象 [[Scopes]]其中存放着变量 a该对象是被存放在堆上的其中包含了闭包、全局对象等等内容因此我们能通过闭包访问到本该销毁的变量。 另外最开始我们对于闭包的定位是假如一个函数能访问外部的变量那么这个函数它就是一个闭包因此接下来我们看看在全局下的表现是怎么样的。 let a 1 var b 2 // fn 是闭包 function fn() {console.log(a, b); }从上图我们能发现全局下声明的变量如果是 var 的话就直接被挂到 globe 上如果是其他关键字声明的话就被挂到 Script 上。虽然这些内容同样还是存在 [[Scopes]]但是全局变量应该是存放在静态区域的因为全局变量无需进行垃圾回收等需要回收的时候整个应用都没了。 只有在下图的场景中原始类型才可能是被存储在栈上。 这里为什么要说可能是因为 JS 是门动态类型语言一个变量声明时可以是原始类型马上又可以赋值为对象类型然后又回到原始类型。这样频繁的在堆栈上切换存储位置内部引擎是不是也会有什么优化手段或者干脆全部都丢堆上只有 const 声明的原始类型才一定存在栈上当然这只是笔者的一个推测暂时没有深究读者可以忽略这段瞎想 因此笔者对于原始类型存储位置的理解为局部变量才是被存储在栈上全局变量存在静态区域上其它都存储在堆上。 当然这个理解是建立的 Chrome 的表现之上的在不同的浏览器上因为引擎的不同可能存储的方式还是有所变化的。 闭包产生的原因 我们在前面介绍了作用域的概念那么你还需要明白作用域链的基本概念。其实很简单当访问一个变量时代码解释器会首先在当前的作用域查找如果没找到就去父级作用域去查找直到找到该变量或者不存在父级作用域中这样的链路就是作用域链 需要注意的是每一个子函数都会拷贝上级的作用域形成一个作用域的链条。那么我们还是通过下面的代码来详细说明一下作用域链 var a 1; function fun1() {var a 2function fun2() {var a 3;console.log(a);//3} }从中可以看出fun1 函数的作用域指向全局作用域window和它自己本身fun2 函数的作用域指向全局作用域 window、fun1 和它本身而作用域是从最底层向上找直到找到全局作用域 window 为止如果全局还没有的话就会报错。那么这就很形象地说明了什么是作用域链即当前函数一般都会存在上层函数的作用域的引用那么他们就形成了一条作用域链。由此可见闭包产生的本质就是当前环境中存在指向父级作用域的引用。那么还是拿上的代码举例。 function fun1() {var a 2function fun2() {console.log(a); //2}return fun2; } var result fun1(); result();从上面这段代码可以看出这里 result 会拿到父级作用域中的变量输出 2。因为在当前环境中含有对 fun2 函数的引用fun2 函数恰恰引用了 window、fun1 和 fun2 的作用域。因此 fun2 函数是可以访问到 fun1 函数的作用域的变量。那是不是只有返回函数才算是产生了闭包呢其实也不是回到闭包的本质我们只需要让父级作用域的引用存在即可因此还可以这么改代码如下所示 var fun3; function fun1() {var a 2fun3 function() {console.log(a);} } fun1(); fun3();可以看出其中实现的结果和前一段代码的效果其实是一样的就是在给 fun3 函数赋值后fun3 函数就拥有了 window、fun1 和 fun3 本身这几个作用域的访问权限然后还是从下往上查找直到找到 fun1 的作用域中存在 a 这个变量因此输出的结果还是 2最后产生了闭包形式变了本质没有改变。 因此最后返回的不管是不是函数也都不能说明没有产生闭包 闭包的表现形式 返回一个函数在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中只要使用了回调函数实际上就是在使用闭包。请看下面这段代码这些都是平常开发中用到的形式 // 定时器 setTimeout(function handler(){console.log(1); }1000); // 事件监听 $(#app).click(function(){console.log(Event Listener); });作为函数参数传递的形式比如下面的例子。 var a 1; function foo(){var a 2;function baz(){console.log(a);}bar(baz); } function bar(fn){// 这就是闭包fn(); } foo(); // 输出2而不是1IIFE立即执行函数创建了闭包保存了全局作用域window和当前函数的作用域因此可以输出全局的变量如下所示 var a 2; (function IIFE(){console.log(a); // 输出2 })();IIFE 这个函数会稍微有些特殊算是一种自执行匿名函数这个匿名函数拥有独立的作用域。这不仅可以避免了外界访问此 IIFE 中的变量而且又不会污染全局作用域我们经常能在高级的 JavaScript 编程中看见此类函数。 如何解决循环输出问题 在互联网大厂的面试中解决循环输出问题是比较高频的面试题一般都会给一段这样的代码让你来解释 for(var i 1; i 5; i ){setTimeout(function() {console.log(i)}, 0) }上面这段代码执行之后从控制台执行的结果可以看出来结果输出的是 5 个 6那么一般面试官都会先问为什么都是 6我想让你实现输出 1、2、3、4、5 的话怎么办呢 因此结合本讲所学的知识我们来思考一下应该怎么给面试官一个满意的解释。你可以围绕这两点来回答。 setTimeout 为宏任务由于 JS 中单线程 eventLoop 机制在主线程同步任务执行完后才去执行宏任务因此循环结束后 setTimeout 中的回调才依次执行因为 setTimeout 函数也是一种闭包往上找它的父级作用域链就是 window变量 i 为 window 上的全局变量开始执行 setTimeout 之前变量 i 已经就是 6 了因此最后输出的连续就都是 6。 那么我们再来看看如何按顺序依次输出 1、2、3、4、5 呢 利用 IIFE 可以利用 IIFE立即执行函数当每次 for 循环时把此时的变量 i 传递到定时器中然后执行改造之后的代码如下。 for(var i 1;i 5;i){(function(j){setTimeout(function timer(){console.log(j)}, 0)})(i) }使用 ES6 中的 let ES6 中新增的 let 定义变量的方式使得 ES6 之后 JS 发生革命性的变化让 JS 有了块级作用域代码的作用域以块级为单位进行执行。通过改造后的代码可以实现上面想要的结果。 for(let i 1; i 5; i){setTimeout(function() {console.log(i);},0) }定时器传入第三个参数 setTimeout 作为经常使用的定时器它是存在第三个参数的日常工作中我们经常使用的一般是前两个一个是回调函数另外一个是时间而第三个参数用得比较少。那么结合第三个参数调整完之后的代码如下。 for(var i1;i5;i){setTimeout(function(j) {console.log(j)}, 0, i) }从中可以看到第三个参数的传递可以改变 setTimeout 的执行逻辑从而实现我们想要的结果这也是一种解决循环输出问题的途径 常见考点 闭包能考的很多概念和笔试题都会考。概念题就是考考闭包是什么了。笔试题的话基本都会结合上异步比如最常见的 for (var i 0; i 6; i) {setTimeout(() {console.log(i)}) }这道题会问输出什么有哪几种方式可以得到想要的答案 ——————– New的原理 常见考点 new 做了那些事new 返回不同的类型时会有什么表现手写 new 的实现过程 new 关键词的主要作用就是执行一个构造函数、返回一个实例对象在 new 的过程中根据构造函数的情况来确定是否可以接受参数的传递。下面我们通过一段代码来看一个简单的 new 的例子 function Person(){this.name Jack; } var p new Person(); console.log(p.name) // Jack这段代码比较容易理解从输出结果可以看出p 是一个通过 person 这个构造函数生成的一个实例对象这个应该很容易理解。 new 操作符可以帮助我们构建出一个实例并且绑定上 this内部执行步骤可大概分为以下几步 创建一个新对象对象连接到构造函数原型上并绑定 thisthis 指向新对象执行构造函数代码为这个新对象添加属性返回新对象 在第四步返回新对象这边有一个情况会例外 那么问题来了如果不用 new 这个关键词结合上面的代码改造一下去掉 new会发生什么样的变化呢我们再来看下面这段代码 function Person(){this.name Jack; } var p Person(); console.log(p) // undefined console.log(name) // Jack console.log(p.name) // name of undefined从上面的代码中可以看到我们没有使用 new 这个关键词返回的结果就是 undefined。其中由于 JavaScript 代码在默认情况下 this 的指向是 window那么 name 的输出结果就为 Jack这是一种不存在 new 关键词的情况。那么当构造函数中有 return 一个对象的操作结果又会是什么样子呢我们再来看一段在上面的基础上改造过的代码。 function Person(){this.name Jack; return {age: 18} } var p new Person(); console.log(p) // {age: 18} console.log(p.name) // undefined console.log(p.age) // 18通过这段代码又可以看出当构造函数最后 return 出来的是一个和 this 无关的对象时new 命令会直接返回这个新对象而不是通过 new 执行步骤生成的 this 对象 但是这里要求构造函数必须是返回一个对象如果返回的不是对象那么还是会按照 new 的实现步骤返回新生成的对象。接下来还是在上面这段代码的基础之上稍微改动一下 function Person(){this.name Jack; return tom; } var p new Person(); console.log(p) // {name: Jack} console.log(p.name) // Jack可以看出当构造函数中 return 的不是一个对象时那么它还是会根据 new 关键词的执行逻辑生成一个新的对象绑定了最新 this最后返回出来 因此我们总结一下new 关键词执行之后总是会返回一个对象要么是实例对象要么是 return 语句指定的对象 手工实现New的过程 function create(fn, …args) {if(typeof fn ! function) {throw fn must be a function;}// 1、用new Object() 的方式新建了一个对象obj// var obj new Object()// 2、给该对象的proto赋值为fn.prototype即设置原型链// obj.proto fn.prototype// 1、2步骤合并// 创建一个空对象且这个空对象继承构造函数的 prototype 属性// 即实现 obj.proto constructor.prototypevar obj Object.create(fn.prototype);// 3、执行fn并将obj作为内部this。使用 apply改变构造函数 this 的指向到新建的对象这样 obj 就可以访问到构造函数中的属性var res fn.apply(obj, args);// 4、如果fn有返回值则将其作为new操作返回内容否则返回objreturn res instanceof Object ? res : obj; };使用 Object.create 将 obj 的proto指向为构造函数的原型使用 apply 方法将构造函数内的 this 指向为 obj在 create 返回时使用三目运算符决定返回结果。 我们知道构造函数如果有显式返回值且返回值为对象类型那么构造函数返回结果不再是目标实例 如下代码 function Person(name) {this.name namereturn {1: 1} } const person new Person(Person, lucas) console.log(person) // {1: 1}测试 //使用create代替new function Person() {…} // 使用内置函数new var person new Person(1,2)// 使用手写的new即create var person create(Person, 1,2)new 被调用后大致做了哪几件事情 让实例可以访问到私有属性让实例可以访问构造函数原型constructor.prototype所在原型链上的属性构造函数返回的最后结果是引用数据类型。 ——————– 原型/原型链 proto和prototype关系 proto和constructor是对象独有的。2️⃣prototype属性是函数独有的 在 js 中我们是使用构造函数来新建一个对象的每一个构造函数的内部都有一个 prototype 属性值这个属性值是一个对象这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当我们使用构造函数新建一个对象后在这个对象的内部将包含一个指针这个指针指向构造函数的 prototype 属性对应的值在 ES5 中这个指针被称为对象的原型。一般来说我们是不应该能够获取到这个值的但是现在浏览器中都实现了 proto 属性来让我们访问这个属性但是我们最好不要使用这个属性因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法我们可以通过这个方法来获取对象的原型。 当我们访问一个对象的属性时如果这个对象内部不存在这个属性那么它就会去它的原型对象里找这个属性这个原型对象又会有自己的原型于是就这样一直找下去也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是我们新建的对象为什么能够使用 toString() 等方法的原因。 特点JavaScript 对象是通过引用来传递的我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时与 之相关的对象也会继承这一改变 原型(prototype): 一个简单的对象用于实现对象的 属性继承。可以简单的理解成对象的爹。在 Firefox 和 Chrome 中每个JavaScript对象中都包含一个proto(非标准)的属性指向它爹(该对象的原型)可obj.proto进行访问。构造函数: 可以通过new来 新建一个对象 的函数。实例: 通过构造函数和new创建出来的对象便是实例。 实例通过proto指向原型通过constructor指向构造函数。 以Object为例我们常用的Object便是一个构造函数因此我们可以通过它构建实例。 // 实例 const instance new Object()则此时 实例为instance, 构造函数为Object我们知道构造函数拥有一个prototype的属性指向原型因此原型为: // 原型 const prototype Object.prototype这里我们可以来看出三者的关系: 实例.proto 原型原型.constructor 构造函数构造函数.prototype 原型 // 这条线其实是是基于原型进行获取的可以理解成一条基于原型的映射线 // 例如: // const o new Object() // o.constructor Object – true // o.proto null; // o.constructor Object – false 实例.constructor 构造函数原型链 原型链是由原型对象组成每个对象都有 proto 属性指向了创建该对象的构造函数的原型proto 将对象连接起来组成了原型链。是一个用来实现继承和共享属性的有限的对象链 属性查找机制: 当查找对象的属性时如果实例对象自身不存在该属性则沿着原型链往上一级查找找到时则输出不存在时则继续沿着原型链往上一级查找直至最顶级的原型对象Object.prototype如还是没找到则输出undefined属性修改机制: 只会修改实例对象本身的属性如果不存在则进行添加该属性如果需要修改原型的属性时则可以用: b.prototype.x 2但是这样会造成所有继承于该对象的实例的属性发生改变。 js 获取原型的方法 p.protop.constructor.prototypeObject.getPrototypeOf(p) 总结 每个函数都有 prototype 属性除了 Function.prototype.bind()该属性指向原型。每个对象都有 proto 属性指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]]但是 [[prototype]]是内部属性我们并不能访问到所以使用 _proto_来访问。对象可以通过 proto 来寻找不属于该对象的属性proto 将对象连接起来组成了原型链。 ——————– 继承 涉及面试题原型如何实现继承Class 如何实现继承Class 本质是什么 首先先来讲下 class其实在 JS中并不存在类class 只是语法糖本质还是函数 class Person {} Person instanceof Function // true组合继承 组合继承是最常用的继承方式 function Parent(value) {this.val value } Parent.prototype.getValue function() {console.log(this.val) } function Child(value) {Parent.call(this, value) } Child.prototype new Parent()const child new Child(1)child.getValue() // 1 child instanceof Parent // true以上继承的方式核心是在子类的构造函数中通过 Parent.call(this) 继承父类的属性然后改变子类的原型为 new Parent() 来继承父类的函数。这种继承方式优点在于构造函数可以传参不会与父类引用属性共享可以复用父类的函数但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数导致子类的原型上多了不需要的父类属性存在内存上的浪费 寄生组合继承 这种继承方式对组合继承进行了优化组合继承缺点在于继承父类函数时调用了构造函数我们只需要优化掉这点就行了 function Parent(value) {this.val value } Parent.prototype.getValue function() {console.log(this.val) }function Child(value) {Parent.call(this, value) } Child.prototype Object.create(Parent.prototype, {constructor: {value: Child,enumerable: false,writable: true,configurable: true} })const child new Child(1)child.getValue() // 1 child instanceof Parent // true以上继承实现的核心就是将父类的原型赋值给了子类并且将构造函数设置为子类这样既解决了无用的父类属性问题还能正确的找到子类的构造函数。 Class 继承 以上两种继承方式都是通过原型去解决的在 ES6 中我们可以使用 class 去实现继承并且实现起来很简单 class Parent {constructor(value) {this.val value}getValue() {console.log(this.val)} } class Child extends Parent {constructor(value) {super(value)this.val value} } let child new Child(1) child.getValue() // 1 child instanceof Parent // trueclass 实现继承的核心在于使用 extends 表明继承自哪个父类并且在子类构造函数中必须调用 super因为这段代码可以看成 Parent.call(this, value)。 ES5 和 ES6 继承的区别 ES6 继承的子类需要调用 super() 才能拿到子类ES5 的话是通过 apply 这种绑定的方式类声明不会提升和 let 这些一致 function Super() {} Super.prototype.getNumber function() {return 1 }function Sub() {} Sub.prototype Object.create(Super.prototype, {constructor: {value: Sub,enumerable: false,writable: true,configurable: true} }) let s new Sub() s.getNumber()以下详细讲解几种常见的继承方式 1. 方式1: 借助call function Parent1(){this.name parent1;}function Child1(){Parent1.call(this);this.type child1}console.log(new Child1);这样写的时候子类虽然能够拿到父类的属性值但是问题是父类原型对象中一旦存在方法那么子类无法继承。那么引出下面的方法。 2. 方式2: 借助原型链 function Parent2() {this.name parent2;this.play [1, 2, 3]}function Child2() {this.type child2;}Child2.prototype new Parent2();console.log(new Child2());看似没有问题父类的方法和属性都能够访问但实际上有一个潜在的不足。举个例子 var s1 new Child2(); var s2 new Child2(); s1.play.push(4); console.log(s1.play, s2.play);可以看到控制台 明明我只改变了s1的play属性为什么s2也跟着变了呢很简单因为两个实例使用的是同一个原型对象。 那么还有更好的方式么
- 方式3将前两种组合 function Parent3 () {this.name parent3;this.play [1, 2, 3];}function Child3() {Parent3.call(this);this.type child3;}Child3.prototype new Parent3();var s3 new Child3();var s4 new Child3();s3.play.push(4);console.log(s3.play, s4.play);可以看到控制台 之前的问题都得以解决。但是这里又徒增了一个新问题那就是Parent3的构造函数会多执行了一次Child3.prototype new Parent3();。这是我们不愿看到的。那么如何解决这个问题 4. 方式4: 组合继承的优化1 function Parent4 () {this.name parent4;this.play [1, 2, 3];}function Child4() {Parent4.call(this);this.type child4;}Child4.prototype Parent4.prototype;这里让将父类原型对象直接给到子类父类构造函数只执行一次而且父类属性和方法均能访问但是我们来测试一下 var s3 new Child4(); var s4 new Child4(); console.log(s3)子类实例的构造函数是Parent4显然这是不对的应该是Child4。 5. 方式5(最推荐使用): 组合继承的优化2 function Parent5 () {this.name parent5;this.play [1, 2, 3];}function Child5() {Parent5.call(this);this.type child5;}Child5.prototype Object.create(Parent5.prototype);Child5.prototype.constructor Child5;这是最推荐的一种方式接近完美的继承它的名字也叫做寄生组合继承。
- ES6的extends被编译后的JavaScript代码 ES6的代码最后都是要在浏览器上能够跑起来的这中间就利用了babel这个编译工具将ES6的代码编译成ES5让一些不支持新语法的浏览器也能运行。 那最后编译成了什么样子呢
function _possibleConstructorReturn(self, call) {// …return call (typeof call object || typeof call function) ? call : self;
}function _inherits(subClass, superClass) {// …//看到没有subClass.prototype Object.create(superClass superClass.prototype, {constructor: {value: subClass,enumerable: false,writable: true,configurable: true}});if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.proto superClass;
}var Parent function Parent() {// 验证是否是 Parent 构造出来的 this_classCallCheck(this, Parent);
};var Child (function (_Parent) {_inherits(Child, _Parent);function Child() {_classCallCheck(this, Child);return _possibleConstructorReturn(this, (Child.proto || Object.getPrototypeOf(Child)).apply(this, arguments));}return Child;
}(Parent));核心是_inherits函数可以看到它采用的依然也是第五种方式————寄生组合继承方式同时证明了这种方式的成功。不过这里加了一个Object.setPrototypeOf(subClass, superClass)这是用来干啥的呢 答案是用来继承父类的静态方法。这也是原来的继承方式疏忽掉的地方。
追问: 面向对象的设计一定是好的设计吗 不一定。从继承的角度说这一设计是存在巨大隐患的。 ——————–
面向对象
编程思想
基本思想是使用对象类继承封装等基本概念来进行程序设计优点 易维护 采用面向对象思想设计的结构可读性高由于继承的存在即使改变需求那么维护也只是在局部模块所以维护起来是非常方便和较低成本的 易扩展开发工作的重用性、继承性高降低重复工作量。缩短了开发周期 一般面向对象包含继承封装多态抽象 1. 对象形式的继承
浅拷贝
var Person {name: poetry,age: 18,address: {home: home,office: office,}sclools: [x,z],
};var programer {language: js,
};function extend(p, c){var c c || {};for( var prop in p){c[prop] p[prop];}
}
extend(Person, programer);
programer.name; // poetry
programer.address.home; // home
programer.address.home house; //house
Person.address.home; // house从上面的结果看出浅拷贝的缺陷在于修改了子对象中引用类型的值会影响到父对象中的值因为在浅拷贝中对引用类型的拷贝只是拷贝了地址指向了内存中同一个副本 深拷贝
function extendDeeply(p, c){var c c || {};for (var prop in p){if(typeof p[prop] object){cprop?[]:{};extendDeeply(p[prop], c[prop]);}else{c[prop] p[prop];}}
}利用递归进行深拷贝这样子对象的修改就不会影响到父对象 extendDeeply(Person, programer);
programer.address.home poetry;
Person.address.home; // home利用call和apply继承
function Parent(){this.name abc;this.address {home: home};
}
function Child(){Parent.call(this);this.language js;
}ES5中的Object.create()
var p { name : poetry};
var obj Object.create(p);
obj.name; // poetryObject.create()作为new操作符的替代方案是ES5之后才出来的。我们也可以自己模拟该方法 //模拟Object.create()方法
function myCreate(o){function F(){};F.prototype o;o new F();return o;
}
var p { name : poetry};
var obj myCreate(p);
obj.name; // poetry目前各大浏览器的最新版本包括IE9都部署了这个方法。如果遇到老式浏览器可以用下面的代码自行部署
if (!Object.create) {Object.create function (o) {function F() {}F.prototype o;return new F();};}2. 类的继承 Object.create() function Person(name, age){}
Person.prototype.headCount 1;
Person.prototype.eat function(){console.log(eating…);
}
function Programmer(name, age, title){}Programmer.prototype Object.create(Person.prototype); //建立继承关系
Programmer.prototype.constructor Programmer; // 修改constructor的指向调用父类方法
function Person(name, age){this.name name;this.age age;
}
Person.prototype.headCount 1;
Person.prototype.eat function(){console.log(eating…);
}function Programmer(name, age, title){Person.apply(this, arguments); // 调用父类的构造器
}Programmer.prototype Object.create(Person.prototype);
Programmer.prototype.constructor Programmer;Programmer.prototype.language js;
Programmer.prototype.work function(){console.log(i am working code in this.language);Person.prototype.eat.apply(this, arguments); // 调用父类上的方法
}3. 封装
命名空间 js是没有命名空间的因此可以用对象模拟
var app {}; // 命名空间app //模块1 app.module1 {name: poetry,f: function(){console.log(hi robot);} }; app.module1.name; // poetry app.module1.f(); // hi robot对象的属性外界是可读可写 如何来达到封装的额目的答可通过闭包局部变量来完成 在构造函数内部声明局部变量 和普通方法因为作用域的关系 只有构造函数内的方法才能访问局部变量 而方法对于外界是开放的因此可以通过方法来访问 原本外界访问不到的局部变量 达到函数封装的目的 function Girl(name,age){var love 小明;//love 是局部变量 准确说不属于对象 属于这个函数的额激活对象 函数调用时必将产生一个激活对象 love在激活对象身上 激活对象有作用域的关系 有办法访问 加一个函数提供外界访问this.name name;this.age age;this.say function () {return love;};this.movelove function (){love 小轩; //35}} var g new Girl(yinghong,22);console.log(g); console.log(g.say());//小明 console.log(g.movelove());//undefined 因为35行没有返回 console.log(g.say());//小轩function fn(){function t(){//var age 22;//声明age变量 在t的激活对象上age 22;//赋值操作 t的激活对象上找age属性 找不到 找fn的激活对象….再找到 最终找到window.age 22;//不加var就是操作window全局属性}t(); } console.log(fn());//undefined4. 静态成员 面向对象中的静态方法-静态属性没有new对象 也能引用静态方法属性 function Person(name){var age 100;this.name name; } //静态成员 Person.walk function(){console.log(static); }; Person.walk(); // static5. 私有与公有 function Person(id){// 私有属性与方法var name poetry;var work function(){console.log(this.id);};//公有属性与方法this.id id;this.say function(){console.log(say hello);work.call(this);}; }; var p1 new Person(123); p1.name; // undefined p1.id; // 123 p1.say(); // say hello 1236. 模块化 var moduleA; moduleA function() {var prop 1;function func() {}return {func: func,prop: prop}; }(); // 立即执行匿名函数7. 多态 多态:同一个父类继承出来的子类各有各的形态 function Cat(){this.eat 肉; }function Tiger(){this.color 黑黄相间; }function Cheetah(){this.color 报文; }function Lion(){this.color 土黄色; }Tiger.prototype Cheetah.prototype Lion.prototype new Cat();//共享一个祖先 Catvar T new Tiger(); var C new Cheetah(); var L new Lion();console.log(T.color); console.log(C.color); console.log(L.color);console.log(T.eat); console.log(C.eat); console.log(L.eat);8. 抽象类 在构造器中 throw new Error(); 抛异常。这样防止这个类被直接调用 function DetectorBase() {throw new Error(Abstract class can not be invoked directly!); }DetectorBase.prototype.detect function() {console.log(Detection starting…); }; DetectorBase.prototype.stop function() {console.log(Detection stopped.); }; DetectorBase.prototype.init function() {throw new Error(Error); };// var d new DetectorBase(); // Uncaught Error: Abstract class can not be invoked directly!function LinkDetector() {} LinkDetector.prototype Object.create(DetectorBase.prototype); LinkDetector.prototype.constructor LinkDetector;var l new LinkDetector(); console.log(l); //LinkDetector {}proto: LinkDetector l.detect(); //Detection starting… l.init(); //Uncaught Error: Error——————– 事件机制 涉及面试题事件的触发过程是怎么样的知道什么是事件代理嘛 1. 简介 事件流是一个事件沿着特定数据结构传播的过程。冒泡和捕获是事件流在DOM中两种不同的传播方法 事件流有三个阶段 事件捕获阶段处于目标阶段事件冒泡阶段 事件捕获 事件捕获event capturing通俗的理解就是当鼠标点击或者触发dom事件时浏览器会从根节点开始由外到内进行事件传播即点击了子元素如果父元素通过事件捕获方式注册了对应的事件的话会先触发父元素绑定的事件 事件冒泡 事件冒泡dubbed bubbling与事件捕获恰恰相反事件冒泡顺序是由内到外进行事件传播直到根节点 无论是事件捕获还是事件冒泡它们都有一个共同的行为就是事件传播 2. 捕获和冒泡 div iddiv1div iddiv2/div /divscriptlet div1 document.getElementById(div1);let div2 document.getElementById(div2);div1.onClick function(){alert(1)}div2.onClick function(){alert(2);}/script当点击 div2时会弹出两个弹出框。在 ie8/9/10、chrome浏览器会先弹出”2”再弹出“1”这就是事件冒泡事件从最底层的节点向上冒泡传播。事件捕获则跟事件冒泡相反 W3C的标准是先捕获再冒泡 addEventListener的第三个参数决定把事件注册在捕获true还是冒泡(false) 3. 事件对象 4. 事件流阻止 在一些情况下需要阻止事件流的传播阻止默认动作的发生 event.preventDefault()取消事件对象的默认动作以及继续传播。event.stopPropagation()/ event.cancelBubble true阻止事件冒泡。 事件的阻止在不同浏览器有不同处理 在IE下使用 event.returnValue false在非IE下则使用 event.preventDefault()进行阻止 preventDefault与stopPropagation的区别 preventDefault告诉浏览器不用执行与事件相关联的默认动作如表单提交stopPropagation是停止事件继续冒泡但是对IE9以下的浏览器无效 - 事件注册
通常我们使用 addEventListener 注册事件该函数的第三个参数可以是布尔值也可以是对象。对于布尔值 useCapture 参数来说该参数默认值为 false。useCapture 决定了注册的事件是捕获事件还是冒泡事件一般来说我们只希望事件只触发在目标上这时候可以使用 stopPropagation 来阻止事件的进一步传播。通常我们认为 stopPropagation 是用来阻止事件冒泡的其实该函数也可以阻止捕获事件。stopImmediatePropagation 同样也能实现阻止事件但是还能阻止该事件目标执行别的注册事件
node.addEventListener(click,(event) {event.stopImmediatePropagation()console.log(冒泡)
},false);
// 点击 node 只会执行上面的函数该函数不会执行
node.addEventListener(click,(event) {console.log(捕获 )
},true)6. 事件委托
在js中性能优化的其中一个主要思想是减少dom操作。节省内存不需要给子节点注销事件 假设有100个li每个li有相同的点击事件。如果为每个Li都添加事件则会造成dom访问次数过多引起浏览器重绘与重排的次数过多性能则会降低。 使用事件委托则可以解决这样的问题 原理 实现事件委托是利用了事件的冒泡原理实现的。当我们为最外层的节点添加点击事件那么里面的ul、li、a的点击事件都会冒泡到最外层节点上委托它代为执行事件 ul idulli1/lili2/lili3/li
/ul
scriptwindow.onload function(){var ulEle document.getElementById(ul);ul.onclick function(ev){//兼容IEev ev || window.event;var target ev.target || ev.srcElement;if(target.nodeName.toLowerCase() li){alert( target.innerHTML);}}}
/script——————–
模块化 js 中现在比较成熟的有四种模块加载方案 第一种是 CommonJS 方案它通过 require 来引入模块通过 module.exports 定义模块的输出接口。这种模块加载方案是服务器端的解决方案它是以同步的方式来引入模块的因为在服务端文件都存储在本地磁盘所以读取非常快所以以同步的方式加载没有问题。但如果是在浏览器端由于模块的加载是使用网络请求因此使用异步加载的方式更加合适。第二种是 AMD 方案这种方案采用异步加载的方式来加载模块模块的加载不影响后面语句的执行所有依赖这个模块的语句都定义在一个回调函数里等到加载完成后再执行回调函数。require.js 实现了 AMD 规范第三种是 CMD 方案这种方案和 AMD 方案都是为了解决异步模块加载的问题sea.js 实现了 CMD 规范。它和require.js的区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。第四种方案是 ES6 提出的方案使用 import 和 export 的形式来导入导出模块 在有 Babel 的情况下我们可以直接使用 ES6的模块化 // file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}import {a, b} from ./a.js
import XXX from ./b.jsCommonJS CommonJs 是 Node 独有的规范浏览器中使用就需要用到 Browserify解析了。 // a.js
module.exports {a: 1
}
// or
exports.a 1// b.js
var module require(./a.js)
module.a // - log 1在上述代码中module.exports 和 exports 很容易混淆让我们来看看大致内部实现 var module require(./a.js)
module.a
// 这里其实就是包装了一层立即执行函数这样就不会污染全局变量了
// 重要的是 module 这里module 是 Node 独有的一个变量
module.exports {a: 1
}
// 基本实现
var module {exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports module.exports
var load function (module) {// 导出的东西var a 1module.exports areturn module.exports
};再来说说 module.exports 和exports用法其实是相似的但是不能对 exports 直接赋值不会有任何效果。 对于 CommonJS 和 ES6 中的模块化的两者区别是 前者支持动态导入也就是 require(${path}/xx.js)后者目前不支持但是已有提案,前者是同步导入因为用于服务端文件都在本地同步导入即使卡住主线程影响也不大。而后者是异步导入因为用于浏览器需要下载文件如果也采用同步导入会对渲染有很大影响前者在导出时都是值拷贝就算导出的值变了导入的值也不会改变所以如果想更新值必须重新导入一次。但是后者采用实时绑定的方式导入导出的值都指向同一个内存地址所以导入值会跟随导出值变化后者会编译成 require/exports 来执行的
AMD AMD 是由 RequireJS 提出的 AMD 和 CMD 规范的区别
第一个方面是在模块定义时对依赖的处理不同。AMD推崇依赖前置在定义模块的时候就要声明其依赖的模块。而 CMD 推崇就近依赖只有在用到某个模块的时候再去 require。第二个方面是对依赖模块的执行时机处理不同。首先 AMD 和 CMD 对于模块的加载方式都是异步加载不过它们的区别在于模块的执行时机AMD 在依赖模块加载完成后就直接执行依赖模块依赖模块的执行顺序和我们书写的顺序不一定一致。而 CMD在依赖模块加载完成后并不执行只是下载而已等到所有的依赖模块都加载好后进入回调函数逻辑遇到 require 语句的时候才执行对应的模块这样模块的执行顺序就和我们书写的顺序保持一致了。
// CMD
define(function(require, exports, module) {var a require(./a);a.doSomething();// 此处略去 100 行var b require(./b); // 依赖可以就近书写b.doSomething();// …
});// AMD 默认推荐
define([./a, ./b], function(a, b) {// 依赖必须一开始就写好a.doSomething();// 此处略去 100 行b.doSomething();// …
})AMD requirejs 在推广过程中对模块定义的规范化产出提前执行推崇依赖前置CMD seajs 在推广过程中对模块定义的规范化产出延迟执行推崇依赖就近CommonJs 模块输出的是一个值的 copy运行时加载加载的是一个对象module.exports 属性该对象只有在脚本运行完才会生成ES6 Module 模块输出的是一个值的引用编译时输出接口ES6模块不是对象它对外接口只是一种静态定义在代码静态解析阶段就会生成。
谈谈对模块化开发的理解
我对模块的理解是一个模块是实现一个特定功能的一组方法。在最开始的时候js 只实现一些简单的功能所以并没有模块的概念但随着程序越来越复杂代码的模块化开发变得越来越重要。由于函数具有独立作用域的特点最原始的写法是使用函数来作为模块几个函数作为一个模块但是这种方式容易造成全局变量的污染并且模块间没有联系。后面提出了对象写法通过将函数作为一个对象的方法来实现这样解决了直接使用函数作为模块的一些缺点但是这种办法会暴露所有的所有的模块成员外部代码可以修改内部属性的值。现在最常用的是立即执行函数的写法通过利用闭包来实现模块私有作用域的建立同时不会对全局作用域造成污染。
——————–
Iterator迭代器 Iterator迭代器是一种接口也可以说是一种规范。为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口就可以完成遍历操作即依次处理该数据结构的所有成员。 Iterator语法
const obj {[Symbol.iterator]:function(){}
}[Symbol.iterator] 属性名是固定的写法只要拥有了该属性的对象就能够用迭代器的方式进行遍历。 迭代器的遍历方法是首先获得一个迭代器的指针初始时该指针指向第一条数据之前接着通过调用 next 方法改变指针的指向让其指向下一条数据每一次的 next 都会返回一个对象该对象有两个属性 value 代表想要获取的数据done 布尔值false表示当前指针指向的数据有值true表示遍历已经结束
Iterator 的作用有三个 创建一个指针对象指向当前数据结构的起始位置。也就是说遍历器对象本质上就是一个指针对象。第一次调用指针对象的next方法可以将指针指向数据结构的第一个成员。第二次调用指针对象的next方法指针就指向数据结构的第二个成员。不断调用指针对象的next方法直到它指向数据结构的结束位置。 每一次调用next方法都会返回数据结构的当前成员的信息。具体来说就是返回一个包含value和done两个属性的对象。其中value属性是当前成员的值done属性是一个布尔值表示遍历是否结束。 let arr [{num:1},2,3] let it arrSymbol.iterator // 获取数组中的迭代器 console.log(it.next()) // { value: Object { num: 1 }, done: false } console.log(it.next()) // { value: 2, done: false } console.log(it.next()) // { value: 3, done: false } console.log(it.next()) // { value: undefined, done: true }对象没有布局Iterator接口无法使用for of 遍历。下面使得对象具备Iterator接口 一个数据结构只要有Symbol.iterator属性就可以认为是“可遍历的”原型部署了Iterator接口的数据结构有三种具体包含四种分别是数组类似数组的对象Set和Map结构 为什么对象Object没有部署Iterator接口呢 一是因为对象的哪个属性先遍历哪个属性后遍历是不确定的需要开发者手动指定。然而遍历遍历器是一种线性处理对于非线性的数据结构部署遍历器接口就等于要部署一种线性转换对对象部署Iterator接口并不是很必要因为Map弥补了它的缺陷又正好有Iteraotr接口 let obj {id: 123,name: 张三,age: 18,gender: 男,hobbie: 睡觉 }obj[Symbol.iterator] function () {let keyArr Object.keys(obj)let index 0return {next() {return index keyArr.length ? {value: {key: keyArr[index],val: obj[keyArr[index]]}} : {done: true}}} }for (let key of obj) {console.log(key) }——————– Promise 这里你谈 promise的时候除了将他解决的痛点以及常用的 API 之外最好进行拓展把 eventloop 带进来好好讲一下microtask(微任务)、macrotask(任务) 的执行顺序如果看过 promise 源码最好可以谈一谈 原生 Promise 是如何实现的。Promise 的关键点在于callback 的两个参数一个是 resovle一个是 reject。还有就是 Promise 的链式调用Promise.then()每一个 then 都是一个责任人 Promise 是 ES6 新增的语法解决了回调地狱的问题。可以把 Promise看成一个状态机。初始是 pending 状态可以通过函数 resolve 和 reject将状态转变为 resolved 或者 rejected 状态状态一旦改变就不能再次变化。then 函数会返回一个 Promise 实例并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending 状态其他状态是不可以改变的如果返回的是一个相同实例的话多个 then 调用就失去意义了。 对于 then 来说本质上可以把它看成是 flatMap - Promise 的基本情况 简单来说它就是一个容器里面保存着某个未来才会结束的事件通常是异步操作的结果。从语法上说Promise 是一个对象从它可以获取异步操作的消息 一般 Promise 在执行过程中必然会处于以下几种状态之一。 待定pending初始状态既没有被完成也没有被拒绝。已完成fulfilled操作成功完成。已拒绝rejected操作失败。 待定状态的 Promise 对象执行的话最后要么会通过一个值完成要么会通过一个原因被拒绝。当其中一种情况发生时我们用 Promise 的 then 方法排列起来的相关处理程序就会被调用。因为最后 Promise.prototype.then 和 Promise.prototype.catch 方法返回的是一个 Promise 所以它们可以继续被链式调用 关于 Promise 的状态流转情况有一点值得注意的是内部状态改变之后不可逆你需要在编程过程中加以注意。文字描述比较晦涩我们直接通过一张图就能很清晰地看出 Promise 内部状态流转的情况 从上图可以看出我们最开始创建一个新的 Promise 返回给 p1 然后开始执行状态是 pending当执行 resolve之后状态就切换为 fulfilled执行 reject 之后就变为 rejected 的状态
- Promise 的静态方法
all 方法 语法 Promise.alliterable参数 一个可迭代对象如 Array。描述 此方法对于汇总多个 promise 的结果很有用在 ES6 中可以将多个 Promise.all 异步请求并行操作返回结果一般有下面两种情况。 当所有结果成功返回时按照请求顺序返回成功结果。当其中有一个失败方法时则进入失败方法 我们来看下业务的场景对于下面这个业务场景页面的加载将多个请求合并到一起用 all 来实现可能效果会更好请看代码片段
// 在一个页面中需要加载获取轮播列表、获取店铺列表、获取分类列表这三个操作页面需要同时发出请求进行页面渲染这样用 Promise.all 来实现看起来更清晰、一目了然。//1.获取轮播数据列表
function getBannerList(){return new Promise((resolve,reject){setTimeout(function(){resolve(轮播数据)},300) })
}
//2.获取店铺列表
function getStoreList(){return new Promise((resolve,reject){setTimeout(function(){resolve(店铺数据)},500)})
}
//3.获取分类列表
function getCategoryList(){return new Promise((resolve,reject){setTimeout(function(){resolve(分类数据)},700)})
}
function initLoad(){ Promise.all([getBannerList(),getStoreList(),getCategoryList()]).then(res{console.log(res) }).catch(err{console.log(err)})
}
initLoad()allSettled 方法 Promise.allSettled 的语法及参数跟 Promise.all 类似其参数接受一个 Promise 的数组返回一个新的 Promise。唯一的不同在于执行完之后不会失败也就是说当 Promise.allSettled 全部处理完成后我们可以拿到每个 Promise 的状态而不管其是否处理成功 我们来看一下用 allSettled 实现的一段代码
const resolved Promise.resolve(2);
const rejected Promise.reject(-1);
const allSettledPromise Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {console.log(results);
});
// 返回结果
// [
// { status: fulfilled, value: 2 },
// { status: rejected, reason: -1 }
// ]从上面代码中可以看到Promise.allSettled 最后返回的是一个数组记录传进来的参数中每个 Promise 的返回值这就是和 all 方法不太一样的地方。 any 方法 语法 Promise.anyiterable参数 iterable 可迭代的对象例如 Array。描述 any 方法返回一个 Promise只要参数 Promise 实例有一个变成 fulfilled状态最后 any返回的实例就会变成 fulfilled 状态如果所有参数 Promise 实例都变成 rejected 状态包装实例就会变成 rejected 状态。
const resolved Promise.resolve(2); const rejected Promise.reject(-1); const anyPromise Promise.any([resolved, rejected]); anyPromise.then(function (results) {console.log(results); }); // 返回结果 // 2从改造后的代码中可以看出只要其中一个 Promise 变成 fulfilled状态那么 any 最后就返回这个p romise。由于上面 resolved 这个 Promise 已经是 resolve 的了故最后返回结果为 2 race 方法 语法 Promise.raceiterable参数 iterable 可迭代的对象例如 Array。描述 race方法返回一个 Promise只要参数的 Promise 之中有一个实例率先改变状态则 race 方法的返回状态就跟着改变。那个率先改变的 Promise 实例的返回值就传递给 race 方法的回调函数 我们来看一下这个业务场景对于图片的加载特别适合用 race 方法来解决将图片请求和超时判断放到一起用 race 来实现图片的超时判断。请看代码片段。 //请求某个图片资源 function requestImg(){var p new Promise(function(resolve, reject){var img new Image();img.onload function(){ resolve(img); }img.src http://www.baidu.com/img/flexible/logo/pc/result.png;});return p; } //延时函数用于给请求计时 function timeout(){var p new Promise(function(resolve, reject){setTimeout(function(){ reject(图片请求超时); }, 5000);});return p; } Promise.race([requestImg(), timeout()]) .then(function(results){console.log(results); }) .catch(function(reason){console.log(reason); });// 从上面的代码中可以看出采用 Promise 的方式来判断图片是否加载成功也是针对 Promise.race 方法的一个比较好的业务场景promise手写实现面试够用版 function myPromise(constructor){let selfthis;self.statuspending //定义状态改变前的初始状态self.valueundefined;//定义状态为resolved的时候的状态self.reasonundefined;//定义状态为rejected的时候的状态function resolve(value){//两个pending保证了状态的改变是不可逆的if(self.statuspending){self.valuevalue;self.statusresolved;}}function reject(reason){//两个pending保证了状态的改变是不可逆的if(self.statuspending){self.reasonreason;self.statusrejected;}}//捕获构造异常try{constructor(resolve,reject);}catch(e){reject(e);} } // 定义链式调用的then方法 myPromise.prototype.thenfunction(onFullfilled,onRejected){let selfthis;switch(self.status){case resolved:onFullfilled(self.value);break;case rejected:onRejected(self.reason);break;default: } }——————– Generator Generator 是 ES6中新增的语法和 Promise 一样都可以用来异步编程。Generator函数可以说是Iterator接口的具体实现方式。Generator 最大的特点就是可以控制函数的执行。 function* 用来声明一个函数是生成器函数它比普通的函数声明多了一个*,*的位置比较随意可以挨着 function 关键字也可以挨着函数名yield 产出的意思这个关键字只能出现在生成器函数体内但是生成器中也可以没有yield 关键字函数遇到 yield 的时候会暂停并把 yield 后面的表达式结果抛出去next作用是将代码的控制权交还给生成器函数 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函数我们来分析其执行过程 首先 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 yield实际就是暂缓执行的标示每执行一次next()相当于指针移动到下一个yield位置 总结一下 Generator函数是ES6提供的一种异步编程解决方案。通过yield标识位和next()方法调用实现函数的分段执行 遍历器对象生成函数最大的特点是可以交出函数的执行权 function 关键字与函数名之间有一个星号函数体内部使用 yield表达式定义不同的内部状态next指针移向下一个状态 这里你可以说说 Generator的异步编程以及它的语法糖 async 和 awiat传统的异步编程。ES6 之前异步编程大致如下 回调函数事件监听发布/订阅 传统异步编程方案之一协程多个线程互相协作完成异步任务。 // 使用 * 表示这是一个 Generator 函数 // 内部可以通过 yield 暂停代码 // 通过调用 next 恢复执行 function* test() {let a 1 2;yield 2;yield 3; } let b test(); console.log(b.next()); // { value: 2, done: false } console.log(b.next()); // { value: 3, done: false } console.log(b.next()); // { value: undefined, done: true }从以上代码可以发现加上 *的函数执行后拥有了 next 函数也就是说函数执行后返回了一个对象。每次调用 next 函数可以继续执行被暂停的代码。以下是 Generator 函数的简单实现 // cb 也就是编译过的 test 函数 function generator(cb) {return (function() {var object {next: 0,stop: function() {}};return {next: function() {var ret cb(object);if (ret undefined) return { value: undefined, done: true };return {value: ret,done: false};}};})(); } // 如果你使用 babel 编译后可以发现 test 函数变成了这样 function test() {var a;return generator(function(_context) {while (1) {switch ((_context.prev _context.next)) {// 可以发现通过 yield 将代码分割成几块// 每次执行 next 函数就执行一块代码// 并且表明下次需要执行哪块代码case 0:a 1 2;_context.next 4;return 2;case 4:_context.next 6;return 3;// 执行完毕case 6:case end:return _context.stop();}}}); }——————– async/await Generator 函数的语法糖。有更好的语义、更好的适用性、返回值是 Promise。 await 和 promise 一样更多的是考笔试题当然偶尔也会问到和 promise 的一些区别。await 相比直接使用 Promise 来说优势在于处理 then 的调用链能够更清晰准确的写出代码。缺点在于滥用 await 可能会导致性能问题因为 await 会阻塞代码也许之后的异步代码并不依赖于前者但仍然需要等待前者完成导致代码失去了并发性此时更应该使用 Promise.all。一个函数如果加上 async 那么该函数就会返回一个 Promise async await yield // 基本用法async function timeout (ms) {await new Promise((resolve) {setTimeout(resolve, ms) }) } async function asyncConsole (value, ms) {await timeout(ms)console.log(value) } asyncConsole(hello async and await, 1000)下面来看一个使用 await 的代码。 var a 0 var b async () {a a await 10console.log(2, a) // - 2 10a (await 10) aconsole.log(3, a) // - 3 20 } b() a console.log(1, a) // - 1 1首先函数b 先执行在执行到 await 10 之前变量 a 还是 0因为在 await 内部实现了 generators generators 会保留堆栈中东西所以这时候 a 0 被保存了下来因为 await 是异步操作遇到await就会立即返回一个pending状态的Promise对象暂时返回执行代码的控制权使得函数外的代码得以继续执行所以会先执行 console.log(1, a)这时候同步代码执行完毕开始执行异步代码将保存下来的值拿出来使用这时候 a 10然后后面就是常规执行代码了 优缺点 async/await的优势在于处理 then 的调用链能够更清晰准确的写出代码并且也能优雅地解决回调地狱问题。当然也存在一些缺点因为 await 将异步代码改造成了同步代码如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。 async原理 async/await语法糖就是使用Generator函数自动执行器来运作的 // 定义了一个promise用来模拟异步请求作用是传入参数 function getNum(num){return new Promise((resolve, reject) {setTimeout(() {resolve(num1)}, 1000)}) }//自动执行器如果一个Generator函数没有执行完则递归调用 function asyncFun(func){var gen func();function next(data){var result gen.next(data);if (result.done) return result.value;result.value.then(function(data){next(data);});}next(); }// 所需要执行的Generator函数内部的数据在执行完成一步的promise之后再调用下一步 var func function (){var f1 yield getNum(1);var f2 yield getNum(f1);console.log(f2) ; }; asyncFun(func);在执行的过程中判断一个函数的promise是否完成如果已经完成将结果传入下一个函数继续重复此步骤每一个 next() 方法返回值的 value 属性为一个 Promise 对象所以我们为其添加 then 方法 在 then 方法里面接着运行 next 方法挪移遍历器指针直到 Generator函数运行完成 ——————– 事件循环 默认代码从上到下执行执行环境通过script来执行宏任务在代码执行过程中调用定时器 promise click事件…不会立即执行需要等待当前代码全部执行完毕给异步方法划分队列分别存放到微任务立即存放和宏任务时间到了或事情发生了才存放到队列中script执行完毕后会清空所有的微任务微任务执行完毕后会渲染页面不是每次都调用再去宏任务队列中看有没有到达时间的拿出来其中一个执行执行完毕后按照上述步骤不停的循环 例子 自动执行的情况 会输出 listener1 listener2 task1 task2 如果手动点击click 会一个宏任务取出来一个个执行先执行click的宏任务取出微任务去执行。会输出 listener1 task1 listener2 task2 console.log(1)async function asyncFunc(){console.log(2)// await xx promise.resolve((){console.log(3)}).then()// console.log(3) 放到promise.resolve或立即执行await console.log(3) // 相当于把console.log(4)放到了then promise.resolve((){console.log(3)}).then((){// console.log(4)// })// 微任务谁先注册谁先执行console.log(4) }setTimeout((){console.log(5)})const promise new Promise((resolve,reject){console.log(6)resolve(7) })promise.then(d{console.log(d)})asyncFunc()console.log(8)// 输出 1 6 2 3 8 7 4 51. 浏览器事件循环 涉及面试题异步代码执行顺序解释一下什么是 Event Loop JavaScript的单线程与它的用途有关。作为浏览器脚本语言JavaScript的主要用途是与用户互动以及操作DOM。这决定了它只能是单线程否则会带来很复杂的同步问题。比如假定JavaScript同时有两个线程一个线程在某个DOM节点上添加内容另一个线程删除了这个节点这时浏览器应该以哪个线程为准所以为了避免复杂性从一诞生JavaScript就是单线程这已经成了这门语言的核心特征将来也不会改变 js代码执行过程中会有很多任务这些任务总的分成两类 同步任务异步任务 当我们打开网站时网页的渲染过程就是一大堆同步任务比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务就是异步任务。我们用导图来说明 我们解释一下这张图 同步和异步任务分别进入不同的执行场所同步的进入主线程异步的进入Event Table并注册函数。当指定的事情完成时Event Table会将这个函数移入Event Queue。主线程内的任务执行完毕为空会去Event Queue读取对应的函数进入主线程执行。上述过程会不断重复也就是常说的Event Loop(事件循环)。 那主线程执行栈何时为空呢js引擎存在monitoring process进程会持续不断的检查主线程执行栈是否为空一旦为空就会去Event Queue那里检查是否有等待被调用的函数 以上就是js运行的整体流程 面试中该如何回答呢 下面是我个人推荐的回答 首先js 是单线程运行的在代码执行的时候通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行在执行同步代码的时候如果遇到了异步事件js 引擎并不会一直等待其返回结果而是会将这个事件挂起继续执行执行栈中的其他任务当同步事件执行完毕后再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行任务队列可以分为宏任务对列和微任务对列当当前执行栈中的事件执行完毕后js 引擎首先会判断微任务对列中是否有任务可以执行如果有就将微任务队首的事件压入栈中执行当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。 setTimeout(function() {console.log(1) }, 0); new Promise(function(resolve, reject) {console.log(2);resolve() }).then(function() {console.log(3) }); process.nextTick(function () {console.log(4) }) console.log(5)第一轮主线程开始执行遇到setTimeout将setTimeout的回调函数丢到宏任务队列中在往下执行new Promise立即执行输出2then的回调函数丢到微任务队列中再继续执行遇到process.nextTick同样将回调函数扔到微任务队列再继续执行输出5当所有同步任务执行完成后看有没有可以执行的微任务发现有then函数和nextTick两个微任务先执行哪个呢process.nextTick指定的异步任务总是发生在所有异步任务之前因此先执行process.nextTick输出4然后执行then函数输出3第一轮执行结束。第二轮从宏任务队列开始发现setTimeout回调输出1执行完毕因此结果是25431 JS 在执行的过程中会产生执行环境这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码会被挂起并加入到 Task有多种 task 队列中。一旦执行栈为空Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行所以本质上来说 JS 中的异步还是同步行为 console.log(script start);setTimeout(function() {console.log(setTimeout); }, 0);console.log(script end);不同的任务源会被分配到不同的 Task 队列中任务源可以分为 微任务microtask 和 宏任务macrotask。在 ES6 规范中microtask 称为 jobsmacrotask 称为 task console.log(script start);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 Promise script end promise1 promise2 setTimeout以上代码虽然 setTimeout 写在 Promise 之前但是因为 Promise 属于微任务而 setTimeout 属于宏任务 微任务 process.nextTickpromiseObject.observeMutationObserver
宏任务 scriptsetTimeoutsetIntervalsetImmediateI/O 网络请求完成、文件读写完成事件UI rendering用户交互事件比如鼠标点击、滚动页面、放大缩小等 宏任务中包括了 script 浏览器会先执行一个宏任务接下来有异步代码的话就先执行微任务 所以正确的一次 Event loop 顺序是这样的 执行同步代码这属于宏任务执行栈为空查询是否有微任务需要执行执行所有微任务必要的话渲染 UI然后开始下一轮 Event loop执行宏任务中的异步代码 通过上述的 Event loop 顺序可知如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话为了更快的响应界面响应我们可以把操作 DOM 放入微任务中 JavaScript 引擎首先从宏任务队列macrotask queue中取出第一个任务执行完毕后再将微任务microtask queue中的所有任务取出按照顺序分别全部执行这里包括不仅指开始执行时队列里的微任务如果在这一步过程中产生新的微任务也需要执行然后再从宏任务队列中取下一个执行完毕后再次将 microtask queue 中的全部取出循环往复直到两个 queue 中的任务都取完。 总结起来就是一次 Eventloop 循环会处理一个宏任务和所有这次循环中产生的微任务。 2. Node 中的 Event loop 当 Node.js 开始启动时会初始化一个 Eventloop处理输入的代码脚本这些脚本会进行 API 异步调用process.nextTick() 方法会开始处理事件循环。下面就是 Node.js 官网提供的 Eventloop 事件循环参考流程 Node 中的 Event loop 和浏览器中的不相同。Node 的 Event loop 分为6个阶段它们会按照顺序反复运行 每次执行执行一个宏任务后会清空微任务执行顺序和浏览器一致在node11版本以上process.nextTick node中的微任务当前执行栈的底部优先级比promise要高 整个流程分为六个阶段当这六个阶段执行完一次之后才可以算得上执行了一次 Eventloop 的循环过程。我们来分别看下这六个阶段都做了哪些事情。 Timers 阶段 这个阶段执行 setTimeout 和 setInterval的回调函数简单理解就是由这两个函数启动的回调函数。I/O callbacks 阶段 这个阶段主要执行系统级别的回调函数比如 TCP 连接失败的回调。idleprepare 阶段 仅系统内部使用你只需要知道有这 2 个阶段就可以。poll 阶段 poll 阶段是一个重要且复杂的阶段几乎所有 I/O 相关的回调都在这个阶段执行除了setTimeout、setInterval、setImmediate 以及一些因为 exception 意外关闭产生的回调。检索新的 I/O 事件执行与 I/O 相关的回调其他情况 Node.js 将在适当的时候在此阻塞。这也是最复杂的一个阶段所有的事件循环以及回调处理都在这个阶段执行。这个阶段的主要流程如下图所示。 check 阶段 setImmediate() 回调函数在这里执行setImmediate 并不是立马执行而是当事件循环 poll 中没有新的事件处理时就执行该部分如下代码所示。 const fs require(fs); setTimeout(() { // 新的事件循环的起点console.log(1); }, 0); setImmediate( () {console.log(setImmediate 1); }); /// fs.readFile 将会在 poll 阶段执行 fs.readFile(./test.conf, {encoding: utf-8}, (err, data) {if (err) throw err;console.log(read file success); }); /// 该部分将会在首次事件循环中执行 Promise.resolve().then((){console.log(poll callback); }); // 首次事件循环执行 console.log(2);在这一代码中有一个非常奇特的地方就是 setImmediate 会在 setTimeout 之后输出。有以下几点原因 setTimeout 如果不设置时间或者设置时间为 0则会默认为 1ms主流程执行完成后超过 1ms 时会将 setTimeout 回调函数逻辑插入到待执行回调函数 poll 队列中由于当前 poll 队列中存在可执行回调函数因此需要先执行完待完全执行完成后才会执行checksetImmediate。 因此这也验证了这句话先执行回调函数再执行 setImmediate close callbacks 阶段 执行一些关闭的回调函数如 socket.on(close, …) 除了把 Eventloop 的宏任务细分到不同阶段外。node 还引入了一个新的任务队列 Process.nextTick() 可以认为Process.nextTick() 会在上述各个阶段结束时在进入下一个阶段之前立即执行优先级甚至超过 microtask 队列 事件循环的主要包含微任务和宏任务。具体是怎么进行循环的呢 微任务 在 Node.js 中微任务包含 2 种——process.nextTick 和 Promise。微任务在事件循环中优先级是最高的因此在同一个事件循环中有其他任务存在时优先执行微任务队列。并且process.nextTick 和 Promise也存在优先级process.nextTick 高于 Promise宏任务 在 Node.js 中宏任务包含 4 种——setTimeout、setInterval、setImmediate 和 I/O。宏任务在微任务执行之后执行因此在同一个事件循环周期内如果既存在微任务队列又存在宏任务队列那么优先将微任务队列清空再执行宏任务队列 我们可以看到有一个核心的主线程它的执行阶段主要处理三个核心逻辑。 同步代码。将异步任务插入到微任务队列或者宏任务队列中。执行微任务或者宏任务的回调函数。在主线程处理回调函数的同时也需要判断是否插入微任务和宏任务。根据优先级先判断微任务队列是否存在任务存在则先执行微任务不存在则判断在宏任务队列是否有任务有则执行。 const fs require(fs); // 首次事件循环执行 console.log(start); /// 将会在新的事件循环中的阶段执行 fs.readFile(./test.conf, {encoding: utf-8}, (err, data) {if (err) throw err;console.log(read file success); }); setTimeout(() { // 新的事件循环的起点console.log(setTimeout); }, 0); /// 该部分将会在首次事件循环中执行 Promise.resolve().then((){console.log(Promise callback); }); /// 执行 process.nextTick process.nextTick(() {console.log(nextTick callback); }); // 首次事件循环执行 console.log(end);分析下上面代码的执行过程 第一个事件循环主线程发起因此先执行同步代码所以先输出 start然后输出 end第一个事件循环主线程发起因此先执行同步代码所以先输出 start然后输出 end再从上往下分析遇到微任务插入微任务队列遇到宏任务插入宏任务队列分析完成后微任务队列包含Promise.resolve 和 process.nextTick宏任务队列包含fs.readFile 和 setTimeout先执行微任务队列但是根据优先级先执行 process.nextTick 再执行 Promise.resolve所以先输出 nextTick callback 再输出 Promise callback再执行宏任务队列根据宏任务插入先后顺序执行 setTimeout 再执行 fs.readFile这里需要注意先执行 setTimeout 由于其回调时间较短因此回调也先执行并非是 setTimeout 先执行所以才先执行回调函数但是它执行需要时间肯定大于 1ms所以虽然 fs.readFile 先于setTimeout 执行但是 setTimeout 执行更快所以先输出 setTimeout 最后输出 read file success。 // 输出结果 start end nextTick callback Promise callback setTimeout read file success当微任务和宏任务又产生新的微任务和宏任务时又应该如何处理呢如下代码所示 const fs require(fs); setTimeout(() { // 新的事件循环的起点console.log(1); fs.readFile(./config/test.conf, {encoding: utf-8}, (err, data) {if (err) throw err;console.log(read file sync success);}); }, 0); /// 回调将会在新的事件循环之前 fs.readFile(./config/test.conf, {encoding: utf-8}, (err, data) {if (err) throw err;console.log(read file success); }); /// 该部分将会在首次事件循环中执行 Promise.resolve().then((){console.log(poll callback); }); // 首次事件循环执行 console.log(2);在上面代码中有 2 个宏任务和 1 个微任务宏任务是 setTimeout 和 fs.readFile微任务是 Promise.resolve。 整个过程优先执行主线程的第一个事件循环过程所以先执行同步逻辑先输出 2。接下来执行微任务输出 poll callback。再执行宏任务中的 fs.readFile 和 setTimeout由于 fs.readFile 优先级高先执行 fs.readFile。但是处理时间长于 1ms因此会先执行 setTimeout 的回调函数输出 1。这个阶段在执行过程中又会产生新的宏任务 fs.readFile因此又将该 fs.readFile 插入宏任务队列最后由于只剩下宏任务了 fs.readFile因此执行该宏任务并等待处理完成后的回调输出 read file sync success。 // 结果 2 poll callback 1 read file success read file sync successProcess.nextick() 和 Vue 的 nextick Node.js 和浏览器端宏任务队列的另一个很重要的不同点是浏览器端任务队列每轮事件循环仅出队一个回调函数接着去执行微任务队列而 Node.js 端只要轮到执行某个宏任务队列则会执行完队列中所有的当前任务但是当前轮次新添加到队尾的任务则会等到下一轮次才会执行。 setTimeout(() {console.log(setTimeout); }, 0); setImmediate(() {console.log(setImmediate); }) // 这里可能会输出 setTimeoutsetImmediate // 可能也会相反的输出这取决于性能 // 因为可能进入 event loop 用了不到 1 毫秒这时候会执行 setImmediate // 否则会执行 setTimeout上面介绍的都是 macrotask 的执行情况microtask 会在以上每个阶段完成后立即执行 setTimeout((){console.log(timer1)Promise.resolve().then(function() {console.log(promise1)}) }, 0)setTimeout((){console.log(timer2)Promise.resolve().then(function() {console.log(promise2)}) }, 0)// 以上代码在浏览器和 node 中打印情况是不同的 // 浏览器中一定打印 timer1, promise1, timer2, promise2 // node 中可能打印 timer1, timer2, promise1, promise2 // 也可能打印 timer1, promise1, timer2, promise2Node 中的 process.nextTick 会先于其他 microtask 执行 setTimeout(() {console.log(timer1);Promise.resolve().then(function() {console.log(promise1);}); }, 0);// poll阶段执行 fs.readFile(./test,(){// 在poll阶段里面 如果有setImmediate优先执行setTimeout处于事件循环顶端 poll下面就是setImmediatesetTimeout(()console.log(setTimeout),0)setImmediate(()console.log(setImmediate),0) })process.nextTick(() {console.log(nextTick); }); // nextTick, timer1, promise1,setImmediate,setTimeout对于 microtask 来说它会在以上每个阶段完成前清空 microtask 队列下图中的 Tick 就代表了 microtask 谁来启动这个循环过程循环条件是什么 当 Node.js 启动后会初始化事件循环处理已提供的输入脚本它可能会先调用一些异步的 API、调度定时器或者 process.nextTick()然后再开始处理事件循环。因此可以这样理解Node.js 进程启动后就发起了一个新的事件循环也就是事件循环的起点。 总结来说Node.js 事件循环的发起点有 4 个 Node.js 启动后setTimeout 回调函数setInterval 回调函数也可能是一次 I/O 后的回调函数。 无限循环有没有终点 当所有的微任务和宏任务都清空的时候虽然当前没有任务可执行了但是也并不能代表循环结束了。因为可能存在当前还未回调的异步 I/O所以这个循环是没有终点的只要进程在并且有新的任务存在就会去执行 Node.js 是单线程的还是多线程的 主线程是单线程执行的但是 Node.js 存在多线程执行多线程包括 setTimeout 和异步 I/O 事件。其实 Node.js 还存在其他的线程包括垃圾回收、内存优化等 EventLoop 对渲染的影响 想必你之前在业务开发中也遇到过 requestIdlecallback 和 requestAnimationFrame这两个函数在我们之前的内容中没有讲过但是当你开始考虑它们在 Eventloop 的生命周期的哪一步触发或者这两个方法的回调会在微任务队列还是宏任务队列执行的时候才发现好像没有想象中那么简单。这两个方法其实也并不属于 JS 的原生方法而是浏览器宿主环境提供的方法因为它们牵扯到另一个问题渲染。我们知道浏览器作为一个复杂的应用是多线程工作的除了运行 JS 的线程外还有渲染线程、定时器触发线程、HTTP 请求线程等等。JS 线程可以读取并且修改 DOM而渲染线程也需要读取 DOM这是一个典型的多线程竞争临界资源的问题。所以浏览器就把这两个线程设计成互斥的即同时只能有一个线程在执行渲染原本就不应该出现在 Eventloop 相关的知识体系里但是因为 Eventloop 显然是在讨论 JS 如何运行的问题而渲染则是浏览器另外一个线程的工作。但是 requestAnimationFrame的出现却把这两件事情给关联起来通过调用 requestAnimationFrame 我们可以在下次渲染之前执行回调函数。那下次渲染具体是哪个时间点呢渲染和 Eventloop 有什么关系呢 简单来说就是在每一次 Eventloop 的末尾判断当前页面是否处于渲染时机就是重新渲染 有屏幕的硬件限制比如 60Hz 刷新率简而言之就是 1 秒刷新了 60 次16.6ms 刷新一次。这个时候浏览器的渲染间隔时间就没必要小于 16.6ms因为就算渲染了屏幕上也看不到。当然浏览器也不能保证一定会每 16.6ms 会渲染一次因为还会受到处理器的性能、JavaScript 执行效率等其他因素影响。回到 requestAnimationFrame这个 API 保证在下次浏览器渲染之前一定会被调用实际上我们完全可以把它看成是一个高级版的 setInterval。它们都是在一段时间后执行回调但是前者的间隔时间是由浏览器自己不断调整的而后者只能由用户指定。这样的特性也决定了 requestAnimationFrame 更适合用来做针对每一帧来修改的动画效果当然 requestAnimationFrame 不是 Eventloop 里的宏任务或者说它并不在 Eventloop 的生命周期里只是浏览器又开放的一个在渲染之前发生的新的 hook。另外需要注意的是微任务的认知概念也需要更新在执行 animation callback 时也有可能产生微任务比如 promise 的 callback会放到 animation queue 处理完后再执行。所以微任务并不是像之前说的那样在每一轮 Eventloop 后处理而是在 JS 的函数调用栈清空后处理 但是 requestIdlecallback 却是一个更好理解的概念。当宏任务队列中没有任务可以处理时浏览器可能存在“空闲状态”。这段空闲时间可以被 requestIdlecallback 利用起来执行一些优先级不高、不必立即执行的任务如下图所示 ——————– 垃圾回收 对于在JavaScript中的字符串对象数组是没有固定大小的只有当对他们进行动态分配存储时解释器就会分配内存来存储这些数据当JavaScript的解释器消耗完系统中所有可用的内存时就会造成系统崩溃。内存泄漏在某些情况下不再使用到的变量所占用内存没有及时释放导致程序运行中内存越占越大极端情况下可以导致系统崩溃服务器宕机。JavaScript有自己的一套垃圾回收机制JavaScript的解释器可以检测到什么时候程序不再使用这个对象了数据就会把它所占用的内存释放掉。针对JavaScript的来及回收机制有以下两种方法常用标记清除引用计数标记清除 v8 的垃圾回收机制基于分代回收机制这个机制又基于世代假说这个假说有两个特点一是新生的对象容易早死另一个是不死的对象会活得更久。基于这个假说v8 引擎将内存分为了新生代和老生代。 新创建的对象或者只经历过一次的垃圾回收的对象被称为新生代。经历过多次垃圾回收的对象被称为老生代。新生代被分为 From 和 To 两个空间To 一般是闲置的。当 From 空间满了的时候会执行 Scavenge 算法进行垃圾回收。当我们执行垃圾回收算法的时候应用逻辑将会停止等垃圾回收结束后再继续执行。 这个算法分为三步 首先检查 From 空间的存活对象如果对象存活则判断对象是否满足晋升到老生代的条件如果满足条件则晋升到老生代。如果不满足条件则移动 To 空间。如果对象不存活则释放对象的空间。最后将 From 空间和 To 空间角色进行交换。 新生代对象晋升到老生代有两个条件 第一个是判断是对象否已经经过一次 Scavenge 回收。若经历过则将对象从 From 空间复制到老生代中若没有经历则复制到 To 空间。第二个是 To 空间的内存使用占比是否超过限制。当对象从 From 空间复制到 To 空间时若 To 空间使用超过 25%则对象直接晋升到老生代中。设置 25% 的原因主要是因为算法结束后两个空间结束后会交换位置如果 To 空间的内存太小会影响后续的内存分配。 老生代采用了标记清除法和标记压缩法。标记清除法首先会对内存中存活的对象进行标记标记结束后清除掉那些没有标记的对象。由于标记清除后会造成很多的内存碎片不便于后面的内存分配。所以了解决内存碎片的问题引入了标记压缩法。 由于在进行垃圾回收的时候会暂停应用的逻辑对于新生代方法由于内存小每次停顿的时间不会太长但对于老生代来说每次垃圾回收的时间长停顿会造成很大的影响。 为了解决这个问题 V8 引入了增量标记的方法将一次停顿进行的过程分为了多步每次执行完一小步就让运行逻辑执行一会就这样交替运行 ——————– 内存泄露 意外的全局变量: 无法被回收定时器: 未被正确关闭导致所引用的外部变量无法被释放事件监听: 没有正确销毁 (低版本浏览器可能出现)闭包 第一种情况是我们由于使用未声明的变量而意外的创建了一个全局变量而使这个变量一直留在内存中无法被回收。第二种情况是我们设置了setInterval定时器而忘记取消它如果循环函数有对外部变量的引用的话那么这个变量会被一直留在内存中而无法被回收。第三种情况是我们获取一个DOM元素的引用而后面这个元素被删除由于我们一直保留了对这个元素的引用所以它也无法被回收。第四种情况是不合理的使用闭包从而导致某些变量一直被留在内存当中。 dom 引用: dom 元素被删除时内存中的引用未被正确清空控制台console.log打印的东西 可用 chrome 中的 timeline 进行内存标记可视化查看内存的变化情况找出异常点。 内存泄露排查方法(opens new window) ——————– 深浅拷贝 1. 浅拷贝的原理和实现 自己创建一个新的对象来接受你要重新复制或引用的对象值。如果对象属性是基本的数据类型复制的就是基本类型的值给新对象但如果属性是引用数据类型复制的就是内存中的地址如果其中一个对象改变了这个内存中的地址肯定会影响到另一个对象 方法一object.assign object.assign是 ES6 中 object 的一个方法该方法可以用于 JS 对象的合并等多个用途其中一个用途就是可以进行浅拷贝。该方法的第一个参数是拷贝的目标对象后面的参数是拷贝的来源对象也可以是多个来源。 object.assign 的语法为Object.assign(target, …sources)object.assign 的示例代码如下 let target {}; let source { a: { b: 1 } }; Object.assign(target, source); console.log(target); // { a: { b: 1 } };但是使用 object.assign 方法有几点需要注意 它不会拷贝对象的继承属性它不会拷贝对象的不可枚举的属性可以拷贝 Symbol 类型的属性。 let obj1 { a:{ b:1 }, sym:Symbol(1)}; Object.defineProperty(obj1, innumerable ,{value:不可枚举属性,enumerable:false }); let obj2 {}; Object.assign(obj2,obj1) obj1.a.b 2; console.log(obj1,obj1); console.log(obj2,obj2);从上面的样例代码中可以看到利用 object.assign 也可以拷贝 Symbol 类型的对象但是如果到了对象的第二层属性 obj1.a.b 这里的时候前者值的改变也会影响后者的第二层属性的值说明其中依旧存在着访问共同堆内存的问题也就是说这种方法还不能进一步复制而只是完成了浅拷贝的功能 方法二扩展运算符方式 我们也可以利用 JS 的扩展运算符在构造对象的同时完成浅拷贝的功能。扩展运算符的语法为let cloneObj { …obj }; /* 对象的拷贝 / let obj {a:1,b:{c:1}} let obj2 {…obj} obj.a 2 console.log(obj) //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}} obj.b.c 2 console.log(obj) //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}} / 数组的拷贝 */ let arr [1, 2, 3]; let newArr […arr]; //跟arr.slice()是一样的效果扩展运算符 和 object.assign 有同样的缺陷也就是实现的浅拷贝的功能差不多但是如果属性都是基本类型的值使用扩展运算符进行浅拷贝会更加方便 方法三concat 拷贝数组 数组的 concat 方法其实也是浅拷贝所以连接一个含有引用类型的数组时需要注意修改原数组中的元素的属性因为它会影响拷贝之后连接的数组。不过 concat 只能用于数组的浅拷贝使用场景比较局限。代码如下所示。 let arr [1, 2, 3]; let newArr arr.concat(); newArr[1] 100; console.log(arr); // [ 1, 2, 3 ] console.log(newArr); // [ 1, 100, 3 ]方法四slice 拷贝数组 slice 方法也比较有局限性因为它仅仅针对数组类型。slice方法会返回一个新的数组对象这一对象由该方法的前两个参数来决定原数组截取的开始和结束时间是不会影响和改变原始数组的。 slice 的语法为arr.slice(begin, end);let arr [1, 2, {val: 4}]; let newArr arr.slice(); newArr[2].val 1000; console.log(arr); //[ 1, 2, { val: 1000 } ]从上面的代码中可以看出这就是浅拷贝的限制所在了——它只能拷贝一层对象。如果存在对象的嵌套那么浅拷贝将无能为力。因此深拷贝就是为了解决这个问题而生的它能解决多层对象嵌套问题彻底实现拷贝 手工实现一个浅拷贝 根据以上对浅拷贝的理解如果让你自己实现一个浅拷贝大致的思路分为两点 对基础类型做一个最基本的一个拷贝对引用类型开辟一个新的存储并且拷贝一层对象属性。 const shallowClone (target) {if (typeof target object target ! null) {const cloneTarget Array.isArray(target) ? []: {};for (let prop in target) {if (target.hasOwnProperty(prop)) {cloneTarget[prop] target[prop];}}return cloneTarget;} else {return target;} }利用类型判断针对引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性基本就可以手工实现一个浅拷贝的代码了 2. 深拷贝的原理和实现 浅拷贝只是创建了一个新的对象复制了原有对象的基本类型的值而引用数据类型只拷贝了一层属性再深层的还是无法进行拷贝。深拷贝则不同对于复杂引用数据类型其在堆内存中完全开辟了一块内存地址并将原有的对象完全复制过来存放。 这两个对象是相互独立、不受影响的彻底实现了内存上的分离。总的来说深拷贝的原理可以总结如下 将一个对象从内存中完整地拷贝出来一份给目标对象并从堆内存中开辟一个全新的空间存放新对象且新对象的修改并不会改变原对象二者实现真正的分离。 方法一乞丐版JSON.stringify JSON.stringify() 是目前开发过程中最简单的深拷贝方法其实就是把一个对象序列化成为 JSON 的字符串并将对象里面的内容转换成字符串最后再用 JSON.parse() 的方法将 JSON 字符串生成一个新的对象 let a {age: 1,jobs: {first: FE} } let b JSON.parse(JSON.stringify(a)) a.jobs.first native console.log(b.jobs.first) // FE但是该方法也是有局限性的
会忽略 undefined会忽略 symbol不能序列化函数无法拷贝不可枚举的属性无法拷贝对象的原型链拷贝 RegExp 引用类型会变成空对象拷贝 Date 引用类型会变成字符串对象中含有 NaN、Infinity 以及 -InfinityJSON 序列化的结果会变成 null不能解决循环引用的对象即对象成环 (obj[key] obj)。 function Obj() { this.func function () { alert(1) }; this.obj {a:1};this.arr [1,2,3];this.und undefined; this.reg /123/; this.date new Date(0); this.NaN NaN;this.infinity Infinity;this.sym Symbol(1); } let obj1 new Obj(); Object.defineProperty(obj1,innumerable,{ enumerable:false,value:innumerable }); console.log(obj1,obj1); let str JSON.stringify(obj1); let obj2 JSON.parse(str); console.log(obj2,obj2);使用 JSON.stringify 方法实现深拷贝对象虽然到目前为止还有很多无法实现的功能但是这种方法足以满足日常的开发需求并且是最简单和快捷的。而对于其他的也要实现深拷贝的比较麻烦的属性对应的数据类型JSON.stringify 暂时还是无法满足的那么就需要下面的几种方法了 方法二基础版手写递归实现 下面是一个实现 deepClone 函数封装的例子通过 for in 遍历传入参数的属性值如果值是引用类型则再次递归调用该函数如果是基础数据类型就直接复制 let obj1 {a:{b:1} } function deepClone(obj) { let cloneObj {}for(let key in obj) { //遍历if(typeof obj[key] object) { cloneObj[key] deepClone(obj[key]) //是对象就再次调用该函数递归} else {cloneObj[key] obj[key] //基本类型的话直接复制值}}return cloneObj } let obj2 deepClone(obj1); obj1.a.b 2; console.log(obj2); // {a:{b:1}}虽然利用递归能实现一个深拷贝但是同上面的 JSON.stringify 一样还是有一些问题没有完全解决例如 这个深拷贝函数并不能复制不可枚举的属性以及 Symbol 类型这种方法只是针对普通的引用类型的值做递归复制而对于 Array、Date、RegExp、Error、Function 这样的引用类型并不能正确地拷贝对象的属性里面成环即循环引用没有解决。 这种基础版本的写法也比较简单可以应对大部分的应用情况。但是你在面试的过程中如果只能写出这样的一个有缺陷的深拷贝方法有可能不会通过。 所以为了“拯救”这些缺陷下面我带你一起看看改进的版本以便于你可以在面试种呈现出更好的深拷贝方法赢得面试官的青睐。 方法三改进版改进后递归实现 针对上面几个待解决问题我先通过四点相关的理论告诉你分别应该怎么做。 针对能够遍历对象的不可枚举属性以及 Symbol 类型我们可以使用 Reflect.ownKeys 方法当参数为 Date、RegExp 类型则直接生成一个新的实例返回利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性以及对应的特性顺便结合 Object.create 方法创建一个新对象并继承传入原对象的原型链利用 WeakMap 类型作为 Hash 表因为 WeakMap 是弱引用类型可以有效防止内存泄漏你可以关注一下 Map 和 weakMap 的关键区别这里要用 weakMap作为检测循环引用很有帮助如果存在循环则引用直接返回 WeakMap 存储的值 如果你在考虑到循环引用的问题之后还能用 WeakMap 来很好地解决并且向面试官解释这样做的目的那么你所展示的代码以及你对问题思考的全面性在面试官眼中应该算是合格的了 实现深拷贝 const isComplexDataType obj (typeof obj object || typeof obj function) (obj ! null)const deepClone function (obj, hash new WeakMap()) {if (obj.constructor Date) {return new Date(obj) // 日期对象直接返回一个新的日期对象}if (obj.constructor RegExp){return new RegExp(obj) //正则对象直接返回一个新的正则对象}//如果循环引用了就用 weakMap 来解决if (hash.has(obj)) {return hash.get(obj)}let allDesc Object.getOwnPropertyDescriptors(obj)//遍历传入参数所有键的特性let cloneObj Object.create(Object.getPrototypeOf(obj), allDesc)// 把cloneObj原型复制到obj上hash.set(obj, cloneObj)for (let key of Reflect.ownKeys(obj)) { cloneObjkey ? deepClone(obj[key], hash) : obj[key]}return cloneObj }// 下面是验证代码 let obj {num: 0,str: ,boolean: true,unf: undefined,nul: null,obj: { name: 我是一个对象, id: 1 },arr: [0, 1, 2],func: function () { console.log(我是一个函数) },date: new Date(0),reg: new RegExp(/我是一个正则/ig),[Symbol(1)]: 1, }; Object.defineProperty(obj, innumerable, {enumerable: false, value: 不可枚举属性 } ); obj Object.create(obj, Object.getOwnPropertyDescriptors(obj)) obj.loop obj // 设置loop成循环引用的属性 let cloneObj deepClone(obj) cloneObj.arr.push(4) console.log(obj, obj) console.log(cloneObj, cloneObj)我们看一下结果cloneObj 在 obj 的基础上进行了一次深拷贝cloneObj 里的 arr 数组进行了修改并未影响到 obj.arr 的变化如下图所示 ——————– 节流与防抖 函数防抖 是指在事件被触发 n 秒后再执行回调如果在这 n 秒内事件又被触发则重新计时。这可以使用在一些点击请求的事件上避免因为用户的多次点击向后端发送多次请求。函数节流 是指规定一个单位时间在这个单位时间内只能有一次触发事件的回调函数执行如果在同一个单位时间内某事件被触发多次只有一次能生效。节流可以使用在 scroll 函数的事件监听上通过事件节流来降低事件调用的频率。 // 函数防抖的实现 function debounce(fn, wait) {var timer null;return function() {var context this,args arguments;// 如果此时存在定时器的话则取消之前的定时器重新记时if (timer) {clearTimeout(timer);timer null;}// 设置定时器使事件间隔指定事件后执行timer setTimeout(() {fn.apply(context, args);}, wait);}; }// 函数节流的实现; function throttle(fn, delay) {var preTime Date.now();return function() {var context this,args arguments,nowTime Date.now();// 如果两次时间间隔超过了指定时间则执行函数。if (nowTime - preTime delay) {preTime Date.now();return fn.apply(context, args);}}; }——————– Proxy代理 proxy在目标对象的外层搭建了一层拦截外界对目标对象的某些操作必须通过这层拦截 var proxy new Proxy(target, handler);new Proxy()表示生成一个Proxy实例target参数表示所要拦截的目标对象handler参数也是一个对象用来定制拦截行为 var target {name: poetries};var logHandler {get: function(target, key) {console.log(\({key} 被读取);return target[key];},set: function(target, key, value) {console.log(\){key} 被设置为 \({value});target[key] value;}}var targetWithLog new Proxy(target, logHandler);targetWithLog.name; // 控制台输出name 被读取targetWithLog.name others; // 控制台输出name 被设置为 othersconsole.log(target.name); // 控制台输出: otherstargetWithLog 读取属性的值时实际上执行的是 logHandler.get 在控制台输出信息并且读取被代理对象 target 的属性。在 targetWithLog 设置属性值时实际上执行的是 logHandler.set 在控制台输出信息并且设置被代理对象 target 的属性的值 // 由于拦截函数总是返回35所以访问任何属性都得到35 var proxy new Proxy({}, {get: function(target, property) {return 35;} });proxy.time // 35 proxy.name // 35 proxy.title // 35Proxy 实例也可以作为其他对象的原型对象 var proxy new Proxy({}, {get: function(target, property) {return 35;} });let obj Object.create(proxy); obj.time // 35proxy对象是obj对象的原型obj对象本身并没有time属性所以根据原型链会在proxy对象上读取该属性导致被拦截 Proxy的作用 对于代理模式 Proxy 的作用主要体现在三个方面 拦截和监视外部对对象的访问降低函数或类的复杂度在复杂操作前对操作进行校验或对所需资源进行管理 Proxy所能代理的范围–handler 实际上 handler 本身就是ES6所新设计的一个对象.它的作用就是用来 自定义代理对象的各种可代理操作 。它本身一共有13中方法,每种方法都可以代理一种操作.其13种方法如下 // 在读取代理对象的原型时触发该操作比如在执行 Object.getPrototypeOf(proxy) 时。 handler.getPrototypeOf()// 在设置代理对象的原型时触发该操作比如在执行 Object.setPrototypeOf(proxy, null) 时。 handler.setPrototypeOf()// 在判断一个代理对象是否是可扩展时触发该操作比如在执行 Object.isExtensible(proxy) 时。 handler.isExtensible()// 在让一个代理对象不可扩展时触发该操作比如在执行 Object.preventExtensions(proxy) 时。 handler.preventExtensions()// 在获取代理对象某个属性的属性描述时触发该操作比如在执行 Object.getOwnPropertyDescriptor(proxy, foo) 时。 handler.getOwnPropertyDescriptor()// 在定义代理对象某个属性时的属性描述时触发该操作比如在执行 Object.defineProperty(proxy, foo, {}) 时。 andler.defineProperty()// 在判断代理对象是否拥有某个属性时触发该操作比如在执行 foo in proxy 时。 handler.has()// 在读取代理对象的某个属性时触发该操作比如在执行 proxy.foo 时。 handler.get()// 在给代理对象的某个属性赋值时触发该操作比如在执行 proxy.foo 1 时。 handler.set()// 在删除代理对象的某个属性时触发该操作比如在执行 delete proxy.foo 时。 handler.deleteProperty()// 在获取代理对象的所有属性键时触发该操作比如在执行 Object.getOwnPropertyNames(proxy) 时。 handler.ownKeys()// 在调用一个目标对象为函数的代理对象时触发该操作比如在执行 proxy() 时。 handler.apply()// 在给一个目标对象为构造函数的代理对象构造实例时触发该操作比如在执行new proxy() 时。 handler.construct()为何Proxy不能被Polyfill 如class可以用function模拟promise可以用callback模拟但是proxy不能用Object.defineProperty模拟 目前谷歌的polyfill只能实现部分的功能如get、set https://github.com/GoogleChrome/proxy-polyfill // commonJS require const proxyPolyfill require(proxy-polyfill/src/proxy)();// Your environment may also support transparent rewriting of commonJS to ES6: import ProxyPolyfillBuilder from proxy-polyfill/src/proxy; const proxyPolyfill ProxyPolyfillBuilder();// Then use... const myProxy new proxyPolyfill(...);-------------------- Ajax 它是一种异步通信的方法通过直接由 js 脚本向服务器发起 http 通信然后根据服务器返回的数据更新网页的相应部分而不用刷新整个页面的一种方法。 面试手写原生 //1创建Ajax对象 var xhr window.XMLHttpRequest?new XMLHttpRequest():new ActiveXObject(Microsoft.XMLHTTP);// 兼容IE6及以下版本 //2配置 Ajax请求地址 xhr.open(get,index.xml,true); //3发送请求 xhr.send(null); // 严谨写法 //4:监听请求接受响应 xhr.onreadysatechangefunction(){if(xhr.readySate4xhr.status200 || xhr.status304 )console.log(xhr.responsetXML) }jQuery写法 \).ajax({type:post,url:,async:ture,//async 异步 sync 同步data:data,//针对post请求dataType:jsonp,success:function (msg) {},error:function (error) {} })promise 封装实现 // promise 封装实现function getJSON(url) {// 创建一个 promise 对象let promise new Promise(function(resolve, reject) {let xhr new XMLHttpRequest();// 新建一个 http 请求xhr.open(GET, url, true);// 设置状态的监听函数xhr.onreadystatechange function() {if (this.readyState ! 4) return;// 当请求成功或失败时改变 promise 的状态if (this.status 200) {resolve(this.response);} else {reject(new Error(this.statusText));}};// 设置错误监听函数xhr.onerror function() {reject(new Error(this.statusText));};// 设置响应的数据类型xhr.responseType json;// 设置请求头信息xhr.setRequestHeader(Accept, application/json);// 发送 http 请求xhr.send(null);});return promise; }——————– 深入数组 一、梳理数组 API - Array.of Array.of 用于将参数依次转化为数组中的一项然后返回这个新数组而不管这个参数是数字还是其他。它基本上与 Array 构造器功能一致唯一的区别就在单个数字参数的处理上 Array.of(8.0); // [8] Array(8.0); // [empty × 8] Array.of(8.0, 5); // [8, 5] Array(8.0, 5); // [8, 5] Array.of(8); // [8] Array(8); // [8]2. Array.from 从语法上看Array.from 拥有 3 个参数 类似数组的对象必选加工函数新生成的数组会经过该函数的加工再返回this 作用域表示加工函数执行时 this 的值。 这三个参数里面第一个参数是必选的后两个参数都是可选的。我们通过一段代码来看看它的用法。 var obj {0: a, 1: b, 2:c, length: 3}; Array.from(obj, function(value, index){console.log(value, index, this, arguments.length);return value.repeat(3); //必须指定返回值否则返回 undefined }, obj);// return 的 value 重复了三遍最后返回的数组为 [aaa,bbb,ccc]// 如果这里不指定 this 的话加工函数完全可以是一个箭头函数。上述代码可以简写为如下形式。 Array.from(obj, (value) value.repeat(3)); // 控制台返回 (3) [aaa, bbb, ccc]除了上述 obj 对象以外拥有迭代器的对象还包括 String、Set、Map 等Array.from 统统可以处理请看下面的代码。 // String Array.from(abc); // [a, b, c] // Set Array.from(new Set([abc, def])); // [abc, def] // Map Array.from(new Map([[1, ab], [2, de]])); // [[1, ab], [2, de]]3. Array 的判断 在 ES5 提供该方法之前我们至少有如下 5 种方式去判断一个变量是否为数组。 var a []; // 1.基于instanceof a instanceof Array; // 2.基于constructor a.constructor Array; // 3.基于Object.prototype.isPrototypeOf Array.prototype.isPrototypeOf(a); // 4.基于getPrototypeOf Object.getPrototypeOf(a) Array.prototype; // 5.基于Object.prototype.toString Object.prototype.toString.apply(a) [object Array];ES6 之后新增了一个 Array.isArray 方法能直接判断数据类型是否为数组但是如果 isArray 不存在那么 Array.isArray 的 polyfill 通常可以这样写 if (!Array.isArray){Array.isArray function(arg){return Object.prototype.toString.call(arg) [object Array];}; }4. 改变自身的方法 基于 ES6会改变自身值的方法一共有 9 个分别为 pop、push、reverse、shift、sort、splice、unshift以及两个 ES6 新增的方法 copyWithin 和 fill // pop方法 var array [cat, dog, cow, chicken, mouse]; var item array.pop(); console.log(array); // [cat, dog, cow, chicken] console.log(item); // mouse // push方法 var array [football, basketball, badminton]; var i array.push(golfball); console.log(array); // [football, basketball, badminton, golfball] console.log(i); // 4 // reverse方法 var array [1,2,3,4,5]; var array2 array.reverse(); console.log(array); // [5,4,3,2,1] console.log(array2array); // true // shift方法 var array [1,2,3,4,5]; var item array.shift(); console.log(array); // [2,3,4,5] console.log(item); // 1 // unshift方法 var array [red, green, blue]; var length array.unshift(yellow); console.log(array); // [yellow, red, green, blue] console.log(length); // 4 // sort方法 var array [apple,Boy,Cat,dog]; var array2 array.sort(); console.log(array); // [Boy, Cat, apple, dog] console.log(array2 array); // true // splice方法 var array [apple,boy]; var splices array.splice(1,1); console.log(array); // [apple] console.log(splices); // [boy] // copyWithin方法 var array [1,2,3,4,5]; var array2 array.copyWithin(0,3); console.log(arrayarray2,array2); // true [4, 5, 3, 4, 5] // fill方法 var array [1,2,3,4,5]; var array2 array.fill(10,0,3); console.log(arrayarray2,array2); // true [10, 10, 10, 4, 5], 可见数组区间[0,3]的元素全部替换为105. 不改变自身的方法 基于 ES7不会改变自身的方法也有 9 个分别为 concat、join、slice、toString、toLocaleString、indexOf、lastIndexOf、未形成标准的 toSource以及 ES7 新增的方法 includes。 // concat方法 var array [1, 2, 3]; var array2 array.concat(4,[5,6],[7,8,9]); console.log(array2); // [1, 2, 3, 4, 5, 6, 7, 8, 9] console.log(array); // [1, 2, 3], 可见原数组并未被修改 // join方法 var array [We, are, Chinese]; console.log(array.join()); // We,are,Chinese console.log(array.join()); // WeareChinese // slice方法 var array [one, two, three,four, five]; console.log(array.slice()); // [one, two, three,four, five] console.log(array.slice(2,3)); // [three] // toString方法 var array [Jan, Feb, Mar, Apr]; var str array.toString(); console.log(str); // Jan,Feb,Mar,Apr // tolocalString方法 var array [{name:zz}, 123, abc, new Date()]; var str array.toLocaleString(); console.log(str); // [object Object],123,abc,2016/1/5 下午1:06:23 // indexOf方法 var array [abc, def, ghi,123]; console.log(array.indexOf(def)); // 1 // includes方法 var array [-0, 1, 2]; console.log(array.includes(0)); // true console.log(array.includes(1)); // true var array [NaN]; console.log(array.includes(NaN)); // true其中 includes 方法需要注意的是如果元素中有 0那么在判断过程中不论是 0 还是 -0 都会判断为 True这里的 includes 忽略了 0 和 -0 6. 数组遍历的方法 基于 ES6不会改变自身的遍历方法一共有 12 个分别为 forEach、every、some、filter、map、reduce、reduceRight以及 ES6 新增的方法 entries、find、findIndex、keys、values // forEach方法 var array [1, 3, 5]; var obj {name:cc}; var sReturn array.forEach(function(value, index, array){array[index] value;console.log(this.name); // cc被打印了三次, this指向obj },obj); console.log(array); // [1, 3, 5] console.log(sReturn); // undefined, 可见返回值为undefined // every方法 var o {0:10, 1:8, 2:25, length:3}; var bool Array.prototype.every.call(o,function(value, index, obj){return value 8; },o); console.log(bool); // true // some方法 var array [18, 9, 10, 35, 80]; var isExist array.some(function(value, index, array){return value 20; }); console.log(isExist); // true // map 方法 var array [18, 9, 10, 35, 80]; array.map(item item 1); console.log(array); // [19, 10, 11, 36, 81] // filter 方法 var array [18, 9, 10, 35, 80]; var array2 array.filter(function(value, index, array){return value 20; }); console.log(array2); // [35, 80] // reduce方法 var array [1, 2, 3, 4]; var s array.reduce(function(previousValue, value, index, array){return previousValue * value; },1); console.log(s); // 24 // ES6写法更加简洁 array.reduce((p, v) p * v); // 24 // reduceRight方法 (和reduce的区别就是从后往前累计) var array [1, 2, 3, 4]; array.reduceRight((p, v) p * v); // 24 // entries方法 var array [a, b, c]; var iterator array.entries(); console.log(iterator.next().value); // [0, a] console.log(iterator.next().value); // [1, b] console.log(iterator.next().value); // [2, c] console.log(iterator.next().value); // undefined, 迭代器处于数组末尾时, 再迭代就会返回undefined // find findIndex方法 var array [1, 3, 5, 7, 8, 9, 10]; function f(value, index, array){return value%20; // 返回偶数 } function f2(value, index, array){return value 20; // 返回大于20的数 } console.log(array.find(f)); // 8 console.log(array.find(f2)); // undefined console.log(array.findIndex(f)); // 4 console.log(array.findIndex(f2)); // -1 // keys方法 […Array(10).keys()]; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] […new Array(10).keys()]; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] // values方法 var array [abc, xyz]; var iterator array.values(); console.log(iterator.next().value);//abc console.log(iterator.next().value);//xyz7. 总结 这些方法之间存在很多共性如下 所有插入元素的方法比如 push、unshift 一律返回数组新的长度所有删除元素的方法比如 pop、shift、splice 一律返回删除的元素或者返回删除的多个元素组成的数组部分遍历方法比如 forEach、every、some、filter、map、find、findIndex它们都包含 function(value,index,array){} 和 thisArg 这样两个形参。 数组和字符串方法 二、理解JS的类数组 在 JavaScript 中有哪些情况下的对象是类数组呢主要有以下几种 函数里面的参数对象 arguments用 getElementsByTagName/ClassName/Name 获得的 HTMLCollection用 querySelector 获得的 NodeList
- arguments对象 arguments对象是函数中传递的参数值的集合。它是一个类似数组的对象因为它有一个length属性我们可以使用数组索引表示法arguments[1]来访问单个值但它没有数组中的内置方法如forEach、reduce、filter和map。 function foo(name, age, sex) {console.log(arguments);console.log(typeof arguments);console.log(Object.prototype.toString.call(arguments)); } foo(jack, 18, male);这段代码比较容易就是直接将这个函数的 arguments 在函数内部打印出来那么我们看下这个 arguments 打印出来的结果请看控制台的这张截图。 从结果中可以看到typeof 这个 arguments 返回的是 object通过 Object.prototype.toString.call 返回的结果是 [object arguments]可以看出来返回的不是 [object array]说明 arguments 和数组还是有区别的。 我们可以使用Array.prototype.slice将arguments对象转换成一个数组。 function one() {return Array.prototype.slice.call(arguments); }注意:箭头函数中没有arguments对象。 function one() {return arguments; } const two function () {return arguments; } const three function three() {return arguments; }const four () arguments;four(); // Throws an error - arguments is not defined当我们调用函数four时它会抛出一个ReferenceError: arguments is not defined error。使用rest语法可以解决这个问题。 const four (…args) args;这会自动将所有参数值放入数组中。 arguments 不仅仅有一个 length 属性还有一个 callee 属性我们接下来看看这个 callee 是干什么的代码如下所示 function foo(name, age, sex) {console.log(arguments.callee); } foo(jack, 18, male);从控制台可以看到输出的就是函数自身如果在函数内部直接执行调用 callee 的话那它就会不停地执行当前函数直到执行到内存溢出 2. HTMLCollection HTMLCollection 简单来说是 HTML DOM 对象的一个接口这个接口包含了获取到的 DOM 元素集合返回的类型是类数组对象如果用 typeof 来判断的话它返回的是 ‘object’。它是及时更新的当文档中的 DOM 变化时它也会随之变化。 描述起来比较抽象还是通过一段代码来看下 HTMLCollection 最后返回的是什么我们先随便找一个页面中有 form 表单的页面在控制台中执行下述代码 var elem1, elem2; // document.forms 是一个 HTMLCollection elem1 document.forms[0]; elem2 document.forms.item(0); console.log(elem1); console.log(elem2); console.log(typeof elem1); console.log(Object.prototype.toString.call(elem1));在这个有 form 表单的页面执行上面的代码得到的结果如下。 可以看到这里打印出来了页面第一个 form 表单元素同时也打印出来了判断类型的结果说明打印的判断的类型和 arguments 返回的也比较类似typeof 返回的都是 ‘object’和上面的类似。 另外需要注意的一点就是 HTML DOM 中的 HTMLCollection 是即时更新的当其所包含的文档结构发生改变时它会自动更新。下面我们再看最后一个 NodeList 类数组。
- NodeList NodeList 对象是节点的集合通常是由 querySlector 返回的。NodeList 不是一个数组也是一种类数组。虽然 NodeList 不是一个数组但是可以使用 for…of 来迭代。在一些情况下NodeList 是一个实时集合也就是说如果文档中的节点树发生变化NodeList 也会随之变化。我们还是利用代码来理解一下 Nodelist 这种类数组。 var list document.querySelectorAll(input[typecheckbox]); for (var checkbox of list) {checkbox.checked true; } console.log(list); console.log(typeof list); console.log(Object.prototype.toString.call(list));从上面的代码执行的结果中可以发现我们是通过有 CheckBox 的页面执行的代码在结果可中输出了一个 NodeList 类数组里面有一个 CheckBox 元素并且我们判断了它的类型和上面的 arguments 与 HTMLCollection 其实是类似的执行结果如下图所示。 4. 类数组应用场景 遍历参数操作 我们在函数内部可以直接获取 arguments 这个类数组的值那么也可以对于参数进行一些操作比如下面这段代码我们可以将函数的参数默认进行求和操作。 function add() {var sum 0,len arguments.length;for(var i 0; i len; i){sum arguments[i];}return sum; } add() // 0 add(1) // 1 add(12) // 3 add(1,2,3,4); // 10定义链接字符串函数 我们可以通过 arguments 这个例子定义一个函数来连接字符串。这个函数唯一正式声明了的参数是一个字符串该参数指定一个字符作为衔接点来连接字符串。该函数定义如下。 // 这段代码说明了你可以传递任意数量的参数到该函数并使用每个参数作为列表中的项创建列表进行拼接。从这个例子中也可以看出我们可以在日常编码中采用这样的代码抽象方式把需要解决的这一类问题都抽象成通用的方法来提升代码的可复用性 function myConcat(separa) {var args Array.prototype.slice.call(arguments, 1);return args.join(separa); } myConcat(, , red, orange, blue); // red, orange, blue myConcat(; , elephant, lion, snake); // elephant; lion; snake myConcat(. , one, two, three, four, five); // one. two. three. four. five传递参数使用 // 使用 apply 将 foo 的参数传递给 bar function foo() {bar.apply(this, arguments); } function bar(a, b, c) {console.log(a, b, c); } foo(1, 2, 3) //1 2 35. 如何将类数组转换成数组 类数组借用数组方法转数组 function sum(a, b) {let args Array.prototype.slice.call(arguments);// let args [].slice.call(arguments); // 这样写也是一样效果console.log(args.reduce((sum, cur) sum cur)); } sum(1, 2); // 3 function sum(a, b) {let args Array.prototype.concat.apply([], arguments);console.log(args.reduce((sum, cur) sum cur)); } sum(1, 2); // 3ES6 的方法转数组 function sum(a, b) {let args Array.from(arguments);console.log(args.reduce((sum, cur) sum cur)); } sum(1, 2); // 3 function sum(a, b) {let args […arguments];console.log(args.reduce((sum, cur) sum cur)); } sum(1, 2); // 3 function sum(…args) {console.log(args.reduce((sum, cur) sum cur)); } sum(1, 2); // 3Array.from 和 ES6 的展开运算符都可以把 arguments这个类数组转换成数组 args 类数组和数组的异同点 在前端工作中开发者往往会忽视对类数组的学习其实在高级 JavaScript 编程中经常需要将类数组向数组转化尤其是一些比较复杂的开源项目经常会看到函数中处理参数的写法例如[].slice.call(arguments) 这行代码。 三、实现数组扁平化的 6 种方式
- 方法一普通的递归实 普通的递归思路很容易理解就是通过循环递归的方式一项一项地去遍历如果每一项还是一个数组那么就继续往下遍历利用递归程序的方法来实现数组的每一项的连接。我们来看下这个方法是如何实现的如下所示 // 方法1 var a [1, [2, [3, 4, 5]]]; function flatten(arr) {let result [];for(let i 0; i arr.length; i) {if(Array.isArray(arr[i])) {result result.concat(flatten(arr[i]));} else {result.push(arr[i]);}}return result; } flatten(a); // [1, 2, 3, 45]从上面这段代码可以看出最后返回的结果是扁平化的结果这段代码核心就是循环遍历过程中的递归操作就是在遍历过程中发现数组元素还是数组的时候进行递归操作把数组的结果通过数组的 concat 方法拼接到最后要返回的 result 数组上那么最后输出的结果就是扁平化后的数组 2. 方法二利用 reduce 函数迭代 从上面普通的递归函数中可以看出其实就是对数组的每一项进行处理那么我们其实也可以用 reduce 来实现数组的拼接从而简化第一种方法的代码改造后的代码如下所示。 // 方法2 var arr [1, [2, [3, 4]]]; function flatten(arr) {return arr.reduce(function(prev, next){return prev.concat(Array.isArray(next) ? flatten(next) : next)}, []) } console.log(flatten(arr));// [1, 2, 3, 45]3. 方法三扩展运算符实现 这个方法的实现采用了扩展运算符和 some 的方法两者共同使用达到数组扁平化的目的还是来看一下代码 // 方法3 var arr [1, [2, [3, 4]]]; function flatten(arr) {while (arr.some(item Array.isArray(item))) {arr [].concat(…arr);}return arr; } console.log(flatten(arr)); // [1, 2, 3, 45]从执行的结果中可以发现我们先用数组的 some 方法把数组中仍然是组数的项过滤出来然后执行 concat 操作利用 ES6 的展开运算符将其拼接到原数组中最后返回原数组达到了预期的效果。 前三种实现数组扁平化的方式其实是最基本的思路都是通过最普通递归思路衍生的方法尤其是前两种实现方法比较类似。值得注意的是 reduce 方法它可以在很多应用场景中实现由于 reduce 这个方法提供的几个参数比较灵活能解决很多问题所以是值得熟练使用并且精通的
- 方法四split 和 toString 共同处理 我们也可以通过 split 和 toString 两个方法来共同实现数组扁平化由于数组会默认带一个 toString 的方法所以可以把数组直接转换成逗号分隔的字符串然后再用 split 方法把字符串重新转换为数组如下面的代码所示。 // 方法4 var arr [1, [2, [3, 4]]]; function flatten(arr) {return arr.toString().split(,); } console.log(flatten(arr)); // [1, 2, 3, 4]通过这两个方法可以将多维数组直接转换成逗号连接的字符串然后再重新分隔成数组你可以在控制台执行一下查看结果。
- 方法五调用 ES6 中的 flat 我们还可以直接调用 ES6 中的 flat 方法可以直接实现数组扁平化。先来看下 flat 方法的语法 arr.flat([depth])其中 depth 是 flat 的参数depth 是可以传递数组的展开深度默认不填、数值是 1即展开一层数组。那么如果多层的该怎么处理呢参数也可以传进 Infinity代表不论多少层都要展开。那么我们来看下用 flat 方法怎么实现请看下面的代码。 // 方法5 var arr [1, [2, [3, 4]]]; function flatten(arr) {return arr.flat(Infinity); } console.log(flatten(arr)); // [1, 2, 3, 45]可以看出一个嵌套了两层的数组通过将 flat 方法的参数设置为 Infinity达到了我们预期的效果。其实同样也可以设置成 2也能实现这样的效果。因此你在编程过程中发现对数组的嵌套层数不确定的时候最好直接使用 Infinity可以达到扁平化。下面我们再来看最后一种场景
- 方法六正则和 JSON 方法共同处理 我们在第四种方法中已经尝试了用 toString 方法其中仍然采用了将 JSON.stringify 的方法先转换为字符串然后通过正则表达式过滤掉字符串中的数组的方括号最后再利用 JSON.parse 把它转换成数组。请看下面的代码 // 方法 6 let arr [1, [2, [3, [4, 5]]], 6]; function flatten(arr) {let str JSON.stringify(arr);str str.replace(/([|])/g, );str [ str ];return JSON.parse(str); } console.log(flatten(arr)); // [1, 2, 3, 45]可以看到其中先把传入的数组转换成字符串然后通过正则表达式的方式把括号过滤掉这部分正则的表达式你不太理解的话可以看看下面的图片 通过这个在线网站 https://regexper.com/ 可以把正则分析成容易理解的可视化的逻辑脑图。其中我们可以看到匹配规则是全局匹配g左括号或者右括号将它们替换成空格最后返回处理后的结果。之后拿着正则处理好的结果重新在外层包裹括号最后通过 JSON.parse 转换成数组返回。 四、如何用 JS 实现各种数组排序 数据结构算法中排序有很多种常见的、不常见的至少包含十种以上。根据它们的特性可以大致分为两种类型比较类排序和非比较类排序。 比较类排序 通过比较来决定元素间的相对次序其时间复杂度不能突破 O(nlogn)因此也称为非线性时间比较类排序。非比较类排序 不通过比较来决定元素间的相对次序它可以突破基于比较排序的时间下界以线性时间运行因此也称为线性时间非比较类排序。 我们通过一张图片来看看这两种分类方式分别包括哪些排序方法。 非比较类的排序在实际情况中用的比较少
- 冒泡排序 冒泡排序是最基础的排序一般在最开始学习数据结构的时候就会接触它。冒泡排序是一次比较两个元素如果顺序是错误的就把它们交换过来。走访数列的工作会重复地进行直到不需要再交换也就是说该数列已经排序完成。请看下面的代码。 var a [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221]; function bubbleSort(array) {const len array.lengthif (len 2) return arrayfor (let i 0; i len; i) {for (let j 0; j i; j) {if (array[j] array[i]) {const temp array[j]array[j] array[i]array[i] temp}}}return array } bubbleSort(a); // [1, 1, 3, 3, 6, 6, 23, 34, 76, 221, 222, 456]从上面这段代码可以看出最后返回的是排好序的结果。因为冒泡排序实在太基础和简单这里就不过多赘述了。下面我们来看看快速排序法
- 快速排序 快速排序的基本思想是通过一趟排序将待排记录分隔成独立的两部分其中一部分记录的关键字均比另一部分的关键字小则可以分别对这两部分记录继续进行排序以达到整个序列有序。 var a [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221]; function quickSort(array) {var quick function(arr) {if (arr.length 1) return arrconst len arr.lengthconst index Math.floor(len 1)const pivot arr.splice(index, 1)[0]const left []const right []for (let i 0; i len; i) {if (arr[i] pivot) {right.push(arr[i])} else if (arr[i] pivot) {left.push(arr[i])}}return quick(left).concat([pivot], quick(right))}const result quick(array)return result } quickSort(a);// [1, 1, 3, 3, 6, 6, 23, 34, 76, 221, 222, 456]上面的代码在控制台执行之后也可以得到预期的结果。最主要的思路是从数列中挑出一个元素称为 “基准”pivot然后重新排序数列所有元素比基准值小的摆放在基准前面、比基准值大的摆在基准的后面在这个区分搞定之后该基准就处于数列的中间位置然后把小于基准值元素的子数列left和大于基准值元素的子数列right递归地调用 quick 方法排序完成这就是快排的思路。 3. 插入排序 插入排序算法描述的是一种简单直观的排序算法。它的工作原理是通过构建有序序列对于未排序数据在已排序序列中从后向前扫描找到相应位置并插入从而达到排序的效果。来看一下代码 var a [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221]; function insertSort(array) {const len array.lengthlet currentlet prevfor (let i 1; i len; i) {current array[i]prev i - 1while (prev 0 array[prev] current) {array[prev 1] array[prev]prev–}array[prev 1] current}return array } insertSort(a); // [1, 1, 3, 3, 6, 6, 23, 34, 76, 221, 222, 456]从执行的结果中可以发现通过插入排序这种方式实现了排序效果。插入排序的思路是基于数组本身进行调整的首先循环遍历从 i 等于 1 开始拿到当前的 current 的值去和前面的值比较如果前面的大于当前的值就把前面的值和当前的那个值进行交换通过这样不断循环达到了排序的目的
- 选择排序 选择排序是一种简单直观的排序算法。它的工作原理是首先将最小的元素存放在序列的起始位置再从剩余未排序元素中继续寻找最小元素然后放到已排序的序列后面……以此类推直到所有元素均排序完毕。请看下面的代码。 var a [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221]; function selectSort(array) {const len array.lengthlet templet minIndexfor (let i 0; i len - 1; i) {minIndex ifor (let j i 1; j len; j) {if (array[j] array[minIndex]) {minIndex j}}temp array[i]array[i] array[minIndex]array[minIndex] temp}return array } selectSort(a); // [1, 1, 3, 3, 6, 6, 23, 34, 76, 221, 222, 456]这样通过选择排序的方法同样也可以实现数组的排序从上面的代码中可以看出该排序是表现最稳定的排序算法之一因为无论什么数据进去都是 O(n 平方) 的时间复杂度所以用到它的时候数据规模越小越好
- 堆排序 堆排序是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构并同时满足堆积的性质即子结点的键值或索引总是小于或者大于它的父节点。堆的底层实际上就是一棵完全二叉树可以用数组实现。 根节点最大的堆叫作大根堆根节点最小的堆叫作小根堆你可以根据从大到小排序或者从小到大来排序分别建立对应的堆就可以。请看下面的代码 var a [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221]; function heap_sort(arr) {var len arr.lengthvar k 0function swap(i, j) {var temp arr[i]arr[i] arr[j]arr[j] temp}function max_heapify(start, end) {var dad startvar son dad * 2 1if (son end) returnif (son 1 end arr[son] arr[son 1]) {son}if (arr[dad] arr[son]) {swap(dad, son)max_heapify(son, end)}}for (var i Math.floor(len / 2) - 1; i 0; i–) {max_heapify(i, len)}for (var j len - 1; j k; j–) {swap(0, j)max_heapify(0, j)}return arr } heap_sort(a); // [1, 1, 3, 3, 6, 6, 23, 34, 76, 221, 222, 456]从代码来看堆排序相比上面几种排序整体上会复杂一些不太容易理解。不过你应该知道两点 一是堆排序最核心的点就在于排序前先建堆二是由于堆其实就是完全二叉树如果父节点的序号为 n那么叶子节点的序号就分别是 2n 和 2n1。 你理解了这两点再看代码就比较好理解了。堆排序最后有两个循环第一个是处理父节点的顺序第二个循环则是根据父节点和叶子节点的大小对比进行堆的调整。通过这两轮循环的调整最后堆排序完成。
- 归并排序
归并排序是建立在归并操作上的一种有效的排序算法该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并得到完全有序的序列先使每个子序列有序再使子序列段间有序。若将两个有序表合并成一个有序表称为二路归并。我们先看一下代码。
var a [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221];
function mergeSort(array) {const merge (right, left) {const result []let il 0let ir 0while (il left.length ir right.length) {if (left[il] right[ir]) {result.push(left[il])} else {result.push(right[ir])}}while (il left.length) {result.push(left[il])}while (ir right.length) {result.push(right[ir])}return result}const mergeSort array {if (array.length 1) { return array }const mid Math.floor(array.length / 2)const left array.slice(0, mid)const right array.slice(mid, array.length)return merge(mergeSort(left), mergeSort(right))}return mergeSort(array)
}
mergeSort(a); // [1, 1, 3, 3, 6, 6, 23, 34, 76, 221, 222, 456]从上面这段代码中可以看到通过归并排序可以得到想要的结果。上面提到了分治的思路你可以从 mergeSort 方法中看到通过 mid 可以把该数组分成左右两个数组分别对这两个进行递归调用排序方法最后将两个数组按照顺序归并起来。
归并排序是一种稳定的排序方法和选择排序一样归并排序的性能不受输入数据的影响但表现比选择排序好得多因为始终都是 O(nlogn) 的时间复杂度。而代价是需要额外的内存空间。 其中你可以看到排序相关的时间复杂度和空间复杂度以及稳定性的情况如果遇到需要自己实现排序的时候可以根据它们的空间和时间复杂度综合考量选择最适合的排序方法
——————–
meta 标签自动刷新/跳转
假设要实现一个类似 PPT 自动播放的效果你很可能会想到使用 JavaScript 定时器控制页面跳转来实现。但其实有更加简洁的实现方法比如通过 meta 标签来实现
meta http-equivRefresh content5; URLpage2.html上面的代码会在 5s 之后自动跳转到同域下的 page2.html 页面。我们要实现 PPT 自动播放的功能只需要在每个页面的 meta 标签内设置好下一个页面的地址即可。
另一种场景比如每隔一分钟就需要刷新页面的大屏幕监控也可以通过 meta 标签来实现只需去掉后面的 URL 即可
meta http-equivRefresh content60meta viewport相关
!DOCTYPE html !–H5标准声明使用 HTML5 doctype不区分大小写–
head lang”en” !–标准的 lang 属性写法–
meta charset’utf-8′ !–声明文档使用的字符编码–
meta http-equiv”X-UA-Compatible” content”IEedge,chrome1″/ !–优先使用 IE 最新版本和 Chrome–
meta name”description” content”不超过150个字符”/ !–页面描述–
meta name”keywords” content””/ !– 页面关键词–
meta name”author” content”name, emailgmail.com”/ !–网页作者–
meta name”robots” content”index,follow”/ !–搜索引擎抓取–
meta name”viewport” content”initial-scale1, maximum-scale3, minimum-scale1, user-scalableno” !–为移动设备添加 viewport–
meta name”apple-mobile-web-app-title” content”标题” !–iOS 设备 begin–
meta name”apple-mobile-web-app-capable” content”yes”/ !–添加到主屏后的标题iOS 6 新增
是否启用 WebApp 全屏模式删除苹果默认的工具栏和菜单栏–
meta name”apple-itunes-app” content”app-idmyAppStoreID, affiliate-datamyAffiliateData, app-argumentmyURL”
!–添加智能 App 广告条 Smart App BanneriOS 6 Safari–
meta name”apple-mobile-web-app-status-bar-style” content”black”/
meta name”format-detection” content”telphoneno, emailno”/ !–设置苹果工具栏颜色–
meta name”renderer” content”webkit” !– 启用360浏览器的极速模式(webkit)–
meta http-equiv”X-UA-Compatible” content”IEedge” !–避免IE使用兼容模式–
meta http-equiv”Cache-Control” content”no-siteapp” / !–不让百度转码–
meta name”HandheldFriendly” content”true” !–针对手持设备优化主要是针对一些老的不识别viewport的浏览器比如黑莓–
meta name”MobileOptimized” content”320″ !–微软的老式浏览器–
meta name”screen-orientation” content”portrait” !–uc强制竖屏–
meta name”x5-orientation” content”portrait” !–QQ强制竖屏–
meta name”full-screen” content”yes” !–UC强制全屏–
meta name”x5-fullscreen” content”true” !–QQ强制全屏–
meta name”browsermode” content”application” !–UC应用模式–
meta name”x5-page-mode” content”app” !– QQ应用模式–
meta name”msapplication-tap-highlight” content”no” !–windows phone 点击无高亮
设置页面不缓存–
meta http-equiv”pragma” content”no-cache”
meta http-equiv”cache-control” content”no-cache”
meta http-equiv”expires” content”0″——————–
viewport
meta nameviewport contentwidthdevice-width,initial-scale1.0,minimum-scale1.0,maximum-scale1.0,user-scalableno /// width 设置viewport宽度为一个正整数或字符串‘device-width’// device-width 设备宽度// height 设置viewport高度一般设置了宽度会自动解析出高度可以不用设置// initial-scale 默认缩放比例初始缩放比例为一个数字可以带小数// minimum-scale 允许用户最小缩放比例为一个数字可以带小数// maximum-scale 允许用户最大缩放比例为一个数字可以带小数// user-scalable 是否允许手动缩放延伸提问 怎样处理 移动端 1px 被 渲染成 2px问题
局部处理 meta标签中的 viewport属性 initial-scale 设置为 1rem按照设计稿标准走外加利用transfrome 的scale(0.5) 缩小一倍即可 全局处理 mate标签中的 viewport属性 initial-scale 设置为 0.5rem 按照设计稿标准走即可 ——————– 性能优化 性能优化是前端开发中避不开的问题性能问题无外乎两方面原因渲染速度慢、请求时间长。性能优化虽然涉及很多复杂的原因和解决方案但其实只要通过合理地使用标签就可以在一定程度上提升渲染速度以及减少请求时间 1. script 标签调整加载顺序提升渲染速度 由于浏览器的底层运行机制渲染引擎在解析 HTML 时若遇到 script 标签引用文件则会暂停解析过程同时通知网络线程加载文件文件加载后会切换至 JavaScript 引擎来执行对应代码代码执行完成之后切换至渲染引擎继续渲染页面。在这一过程中可以看到页面渲染过程中包含了请求文件以及执行文件的时间但页面的首次渲染可能并不依赖这些文件这些请求和执行文件的动作反而延长了用户看到页面的时间从而降低了用户体验。 为了减少这些时间损耗可以借助 script 标签的 3 个属性来实现。 async 属性。立即请求文件但不阻塞渲染引擎而是文件加载完毕后阻塞渲染引擎并立即执行文件内容defer 属性。立即请求文件但不阻塞渲染引擎等到解析完 HTML 之后再执行文件内容HTML5 标准 type 属性对应值为“module”。让浏览器按照 ECMA Script 6 标准将文件当作模块进行解析默认阻塞效果同 defer也可以配合 async 在请求完成后立即执行。 绿色的线表示执行解析 HTML 蓝色的线表示请求文件红色的线表示执行文件 当渲染引擎解析 HTML 遇到 script 标签引入文件时会立即进行一次渲染。所以这也就是为什么构建工具会把编译好的引用 JavaScript 代码的 script 标签放入到 body 标签底部因为当渲染引擎执行到 body 底部时会先将已解析的内容渲染出来然后再去请求相应的 JavaScript 文件 - link 标签通过预处理提升渲染速度
在我们对大型单页应用进行性能优化时也许会用到按需懒加载的方式来加载对应的模块但如果能合理利用 link 标签的 rel 属性值来进行预加载就能进一步提升渲染速度。
dns-prefetch。当 link 标签的 rel 属性值为“dns-prefetch”时浏览器会对某个域名预先进行 DNS 解析并缓存。这样当浏览器在请求同域名资源的时候能省去从域名查询 IP 的过程从而减少时间损耗。下图是淘宝网设置的 DNS 预解析 preconnect。让浏览器在一个 HTTP 请求正式发给服务器前预先执行一些操作这包括DNS 解析、TLS 协商、TCP 握手通过消除往返延迟来为用户节省时间prefetch/preload。两个值都是让浏览器预先下载并缓存某个资源但不同的是prefetch 可能会在浏览器忙时被忽略而 preload 则是一定会被预先下载。prerender。浏览器不仅会加载资源还会解析执行页面进行预渲染
这几个属性值恰好反映了浏览器获取资源文件的过程在这里我绘制了一个流程简图方便你记忆。 3. 搜索优化
meta 标签提取关键信息 通过 meta 标签可以设置页面的描述信息从而让搜索引擎更好地展示搜索结果。示例 meta namedescription content全球最大的中文搜索引擎、致力于让网民更便捷地获取信息找到所求。百度超过千亿的中文网页数据库可以瞬间找到相关的搜索结果。
——————– 如何高效操作DOM - 为什么说 DOM 操作耗时 1.1 线程切换 浏览器为了避免两个引擎同时修改页面而造成渲染结果不一致的情况增加了另外一个机制这两个引擎具有互斥性也就是说在某个时刻只有一个引擎在运行另一个引擎会被阻塞。操作系统在进行线程切换的时候需要保存上一个线程执行时的状态信息并读取下一个线程的状态信息俗称上下文切换。而这个操作相对而言是比较耗时的每次 DOM 操作就会引发线程的上下文切换——从 JavaScript 引擎切换到渲染引擎执行对应操作然后再切换回 JavaScript 引擎继续执行这就带来了性能损耗。单次切换消耗的时间是非常少的但是如果频繁地大量切换那么就会产生性能问题 比如下面的测试代码循环读取一百万次 DOM 中的 body 元素的耗时是读取 JSON 对象耗时的 10 倍。 // 测试次数一百万次 const times 1000000 // 缓存body元素 console.time(object) let body document.body // 循环赋值对象作为对照参考 for(let i0;itimes;i) {let tmp body } console.timeEnd(object)// object: 1.77197265625msconsole.time(dom) // 循环读取body元素引发线程切换 for(let i0;itimes;i) {let tmp document.body } console.timeEnd(dom)// dom: 18.302001953125ms1.2 重新渲染 另一个更加耗时的因素是元素及样式变化引起的再次渲染在渲染过程中最耗时的两个步骤为重排Reflow与重绘Repaint。 浏览器在渲染页面时会将 HTML 和 CSS 分别解析成 DOM 树和 CSSOM 树然后合并进行排布再绘制成我们可见的页面。如果在操作 DOM 时涉及到元素、样式的修改就会引起渲染引擎重新计算样式生成 CSSOM 树同时还有可能触发对元素的重新排布和重新绘制 可能会影响到其他元素排布的操作就会引起重排继而引发重绘 修改元素边距、大小添加、删除元素改变窗口大小 引起重绘 设置背景图片修改字体颜色改变 visibility属性值 了解更多关于重绘和重排的样式属性可以参看这个网址https://csstriggers.com/ (opens new window)。 2. 如何高效操作 DOM 明白了 DOM 操作耗时之后要提升性能就变得很简单了反其道而行之减少这些操作即可 2.1 在循环外操作元素 比如下面两段测试代码对比了读取 1000 次 JSON 对象以及访问 1000 次 body 元素的耗时差异相差一个数量级 const times 10000; console.time(switch) for (let i 0; i times; i) {document.body 1 ? console.log(1) : void 0; } console.timeEnd(switch) // 1.873046875ms var body JSON.stringify(document.body) console.time(batch) for (let i 0; i times; i) {body 1 ? console.log(1) : void 0; } console.timeEnd(batch) // 0.846923828125ms2.2 批量操作元素 比如说要创建 1 万个 div 元素在循环中直接创建再添加到父元素上耗时会非常多。如果采用字符串拼接的形式先将 1 万个 div 元素的 html 字符串拼接成一个完整字符串然后赋值给 body 元素的 innerHTML 属性就可以明显减少耗时 const times 10000; console.time(createElement) for (let i 0; i times; i) {const div document.createElement(div)document.body.appendChild(div) } console.timeEnd(createElement)// 54.964111328125ms console.time(innerHTML) let html for (let i 0; i times; i) {htmldiv/div } document.body.innerHTML html // 31.919921875ms console.timeEnd(innerHTML)——————– 盒模型 content元素内容 padding内边距 border边框 margin外边距 延伸box-sizing content-box默认值总宽度 margin border padding widthborder-box盒子宽度包含 padding 和 border总宽度 margin widthinherit从父元素继承 box-sizing 属性 ——————–
- 上一篇: 网站建设的好公司强大的wordpress插件
- 下一篇: 网站建设的后期服务要包括什么网站拖拽
相关文章
-
网站建设的好公司强大的wordpress插件
网站建设的好公司强大的wordpress插件
- 技术栈
- 2026年04月20日
-
网站建设的好处论文免费做试用的网站
网站建设的好处论文免费做试用的网站
- 技术栈
- 2026年04月20日
-
网站建设的过程包括几个阶段设计网站过程
网站建设的过程包括几个阶段设计网站过程
- 技术栈
- 2026年04月20日
-
网站建设的后期服务要包括什么网站拖拽
网站建设的后期服务要包括什么网站拖拽
- 技术栈
- 2026年04月20日
-
网站建设的基本步骤和过程沈阳做网站哪好
网站建设的基本步骤和过程沈阳做网站哪好
- 技术栈
- 2026年04月20日
-
网站建设的基本规范有什么营销型网站建设哪家便宜
网站建设的基本规范有什么营销型网站建设哪家便宜
- 技术栈
- 2026年04月20日
