- 零、目录
- 一、数据类型
- 1. JavaScript有哪些数据类型,它们的区别?
- 5. typeof null 的结果是什么,为什么?
- 6. intanceof 操作符的实现原理及实现
- 7. 为什么0.1+0.2 ! == 0.3,如何让其相等
- 8. 如何获取安全的 undefined 值?
- 9. typeof NaN 的结果是什么?
- 10. isNaN 和 Number.isNaN 函数的区别?
- 11. == 操作符的类型转换规则?
- 12. 其他值到字符串的转换规则?
- 13. 其他值到数字值的转换规则?
- 14. 其他值到布尔类型的值的转换规则?
- 15. || 和 && 以及if语句?
- 16. Object.is() 和=== 和 ==的区别?
- 19. + 操作符?
- 20. 为什么会有BigInt的提案?
- 21. object.assign和扩展运算法是深拷贝还是浅拷贝,两者区别
- 22 强制类型转换/隐式类型转换/显式类型转换
- 二、ES6
- 三、JavaScript基础
零、目录
一、数据类型
1. JavaScript有哪些数据类型,它们的区别?
js是一种弱类型脚本语言,就是在定义的时候不需要定义类型,在程序运行过程中会自动判断其类型的。
(1)分为哪几种数据类型
JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。其中Object是复杂数据类型
其中 Symbol 和 BigInt 是 ES6 之后 新增的数据类型
- Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。 ```
1 Symbol没有字面量的语法,要调用Symbol()函数 2 Symbol.for(key值)则可以用key值获取实例,并且一样的key值总会返回相同的值,如果没有找到关联的数据会创建并返回一个新符号 3 普通对象使用for…. of方法遍历的话会报错,给对象添加一个[Symbol.iterator]属性并指向迭代器便可正常迭代 4 Symbol.keyFor(Symbol.for(key值)) 会返回key值
<a name="WcERz"></a>#### Symbol新增类型有什么用?**Symbol是es6新增的数据类型,没有字面量只能通过Symbol()函数创建,代表独一无二的唯一值,不能更改;**<br />**作用:1.防止命名冲突;2.模拟私有变量,不会被常规的方法for遍历到,但可以Object.getOwnPropertySymbols可以遍历到Symbol()键值(比如说给一个变量里定义了一个变量,当访问这个变量的时候能拿到,但是遍历的时候不想被外界变量访问到就可以用这个模拟私有变量),**Reflect.ownKeys()也能拿到**Symbol()的值**for...in 和 for...of 能否遍历symbol的值?<br />**不能,Symbol只能通过Object.getOwnPropertySymbols遍历出键值**<br />- BigInt 是一种数字类型的数据,它可以表示**任意精度格式的整数**,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。字面量可写作一串数字后跟小写字母n<a name="l25Z2"></a>### (2)原始数据类型和引用数据类型这些数据可以分为**原始数据类型(简单数据类型)**和**引用数据类型**:- 栈:7种原始数据类型(Undefined、Null、Boolean、Number、String, Symbol 和 BigInt)- 堆:1种引用数据类型(对象(Date、Array、Function))<a name="hKZNA"></a>#### 1.两种类型的区别:存储位置和值得传递不同:| 原始数据 | 引用数据 || --- | --- || 存储在**栈** | 存储在**堆,**如果存储在栈中,将会影响程序运行的性能 || **占据空间小** | **占据空间大** || **大小固定** | **大小不固定** || **属于被频繁使用数据,所以放入栈中存储** | **在栈中存储了指针,该指针指向堆中对应的值。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体**。 || 按值传递(赋值给其他变量原值不发生改变) | 按共享传递(赋值给其他变量其实是把地址共享出去了,其他变量发生改变也会导致堆中的值发生改变,因为他们指向的是同一个地址) |<a name="bEFrx"></a>#### 2.JS 值传递/共享传递 的设计原因- 按值传递的类型,复制一份存入栈内存,这类类型一般**不占用太多内存**,而且按值传递**保证了其访问速度**。- 按共享传递的类型,是复制其引用,而不是整个复制其值(C 语言中的指针),**保证过大的对象等不会因为不停复制内容而造成内存的浪费。**(函数不用于存储数据,所以没有拷贝复制函数这一说,函数内是可执行代码)<br /><a name="jDk5O"></a>#### 3.堆和栈区别堆和栈的概念存在于数据结构和操作系统内存中| 栈 | 堆 || --- | --- || **先进后出** | **优先队列(**按优先级来进行排序的,优先级可以按照大小来规定**)** || **编译器自动分配内存空间,自动释放**,存放函数的参数值,局部变量的值,其操作方式类似于数据结构中的栈。 | 由**开发者分配内存空间,手动释放**,若开发者不释放,**程序结束**时**可能**由垃圾回收机制回收,大小不一定 || 基本类型保存在栈中, | 引用类型保存在堆和栈中 |<a name="yOYMe"></a>## 2. 数据类型检测的方式有哪些<a name="ziMiW"></a>### (1)typeof
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof ‘str’); // string
console.log(typeof []); // object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
console.log(typeof undefined); // undefined
console.log(typeof null); // object
console.log(typeof Symbol()) // symbol
通常用于判断简单数据类型,除了**数组、对象、null**都会被判断**为object**,其他判断都正确。js在底层存储变量的时候会在变量的机器码的低位1-3位存储其类型信息(000:对象,010:浮点数,100:字符串,110:布尔,1:整数),但是null所有机器码均为0,直接被当做了对象来看待。 那么有没有更好的办法区分类型呢,一般使用Object.prototype.toString.call()<a name="gVuKF"></a>### (2)instanceof
// 简单数据类型 检测不出来 console.log(2 instanceof Number); // false console.log(true instanceof Boolean); // false console.log(‘str’ instanceof String); // false console.log(null instanceof Object) // false
console.log([] instanceof Array); // true console.log([] instanceof Object) // true console.log({} instanceof Object); // true console.log(function(){} instanceof Function); // true
可以看到,instanceof只能正确判断**引用**数据类型,而不能判断基本数据类型。obj instanceof Constructor:判断构造函数的prototype在不在obj的原型链上,在就是true不在就是false<a name="RSLAT"></a>#### (一)instanceof的原理是什么?**遍历循环左侧的__proto__查找是否有跟右侧的prototype属性相等的值,如果相等就返回true,找不到就返回false**<a name="FftVa"></a>#### (二)在寄生组合继承的时候要判断一个函数是否在原型链上,这个方法是什么?**instanceof**<a name="Owsmq"></a>#### (三)instanceof和typeof区别**两个都用来判断数据类型的**<br />**typeof一般用于判断简单数据类型,当判断[]、{}、null都会被判断为object,其他判断都正确。**<br />**instanceof可以判断复杂数据类型,简单数据类型检测不出来,而且会把[],{} instanceof Object的检测结果都是true;**<a name="muKPz"></a>### (3) constructor
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有**两个**作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。<br />需要注意,**如果**创建一个对象来**改变它的原型**,constructor**就不能用来判断数据类型**了:
function Fn(){};
Fn.prototype = new Array();
var f = new Fn();
console.log(f.constructor===Fn); // false console.log(f.constructor===Array); // true
<a name="vfjyA"></a>### (4)Object.prototype.toString.call()Object.prototype.toString.call() 使用 Object 对象的原型方法 toString 来判断数据类型:
var a = Object.prototype.toString;
console.log(a.call(2)); // [object Number] console.log(a.call(true)); // [object Boolean] console.log(a.call(‘str’)); // [object String] console.log(a.call([])); // [object Array] console.log(a.call(function(){})); // [object Function] console.log(a.call({})); // [object Object] console.log(a.call(undefined)); //[object Undefined] console.log(a.call(null)); //[object Null]
** Object.prototype.toString原理:**<br />对于 Object.prototype.toString() 方法,会返回一个形如 "[object XXX]" 的字符串。<br />若参数为 null 或 undefined,直接返回结果。若参数不为 null 或 undefined,则将参数转为对象,再作判断。对于原始类型,转为对象的方法即装箱,<br />转为对象后,取得该对象的 [Symbol.toStringTag] 属性值(可能会遍历原型链)作为 tag,如无该属性,或该属性值不为字符串类型,则依下表取得 tag, 然后返回 "[object " + tag + "]" 形式的字符串。**obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样**<br />同样是检测对象**obj调用toString**方法,obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,这是**为什么**?<br />js中大多数类型都已经有一个**预定义的toString()方法覆盖了Object.prototype提供的toString()方法**<br />这是因为**toString是Object的原型方法**,而Array、function等类型作为Object的**实例,都重写了toString方法**。不同的对象类型调用toString方法时,根据原型链的知识,**调用的是对应的重写之后的toString方法**(function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而**不会去调用Object上原型toString方法**(返回对象的具体类型),**所以**采用**obj.toString()**不能得到其对象类型,**只能将obj转换为字符串类型**;因此,在**想要得到对象的具体类型时,应该调用Object原型上的toString方法**。<a name="HZqj5"></a>## 3. 判断数组的方式有哪些1. 通过Object.prototype.toString.call()做判断会返回[objecct 构造函数]
Object.prototype.toString.call(obj).slice(8,-1) === ‘Array’;
2. 通过原型链做判断:手动用原型链判断,遍历实例的__proto__和Array的prototype做比较
obj.proto === Array.prototype;
3. 通过ES6的Array.isArray()做判断
Array.isArrray(obj); // Array是否在obj的原型链上
4. 通过instanceof做判断
obj instanceof Array
5. 通过Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj)
<a name="aaFtF"></a>## 4. null和undefined区别| | Undefined | Null || --- | --- | --- || 数据类型 | 基本数据类型 | 基本数据类型 || 值 | 只有一个值Undefined | 只有一个值Null || 含义 | **声明未定义** | **空对象/可用来初始化对象** || 是否保留字 | 不是一个保留字,可以使用 undefined 来作为一个变量名,会影响对 undefined 值的判断,可以用void 0获得安全的 undefined 值 | 是保留字 || typeof | ‘undefined’ | “object” |(**保留字不能**被用作**变量名或函数名**,比如double,debugger,export,throw,try等等)当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。
null == undefined // true null === undefined // false
```function fn(arg = 1){console.log(arg)}fn(null) // nullfn(undefined) // 1fn() // 1// undefined相当于声明但是没赋值,所以使用默认值 null的话相当于传一个空的对象进去赋值给arg
5. typeof null 的结果是什么,为什么?
typeof null 的结果是Object。
在 JavaScript 第一个版本中,所有值都存储在 32 位的单元中,每个单元包含一个小的 类型标签(1-3 bits) 以及当前要存储值的真实数据。类型标签存储在每个单元的低位中,共有五种数据类型:
000: object - 当前存储的数据指向一个对象。1: int - 当前存储的数据是一个 31 位的有符号整数。010: double - 当前存储的数据指向一个双精度的浮点数。100: string - 当前存储的数据指向一个字符串。110: boolean - 当前存储的数据是布尔值。
如果最低位是 1,则类型标签标志位的长度只有一位;如果最低位是 0,则类型标签标志位的长度占三位,为存储其他四种数据类型提供了额外两个 bit 的长度。
有两种特殊数据类型:
- undefined的值是 (-2)30(一个超出整数范围的数字);
- null 的值是机器码 NULL 指针(null 指针的值全是 0)
那也就是说null的类型标签也是000,和Object的类型标签一样,所以会被判定为Object。
js把机械码标签为0的都定义为object,null是0x000,所以也被误认为object)
6. intanceof 操作符的实现原理及实现
instanceof 运算符用于判断构造函数的 prototype 属性是否出现在实例的原型链中。
getPrototypeOf和proto都能获取obj对象的原型对象;getPrototypeOf是es5中的标准方法;在不支持es5的语法中我们可以用proto
// 简约版function myInstanceof(left, right) {let proto = Object.getPrototypeOf(left)let prototype = right.prototype;while (true) {if (!proto) return false;if (proto === prototype) return true;proto = Object.getPrototypeOf(proto);}}// 解析版function myInstanceof(left, right) {// 获取对象的原型let proto = Object.getPrototypeOf(left)// 获取构造函数的 prototype 对象let prototype = right.prototype;// 判断构造函数的 prototype 对象是否在对象的原型链上while (true) {if (!proto) return false;if (proto === prototype) return true;// 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型proto = Object.getPrototypeOf(proto);}}// 使用myInstanceof({},Object) // true
7. 为什么0.1+0.2 ! == 0.3,如何让其相等
在开发过程中遇到类似这样的问题:
let n1 = 0.1, n2 = 0.2console.log(n1 + n2) // 0.30000000000000004
这里得到的不是想要的结果,要想等于0.3,就要把它进行转化:
(n1 + n2).toFixed(2)// 注意,toFixed为四舍五入,toFixed(num) 方法可把 Number 四舍五入为指定小数位数的数字。
(一)为什么0.1+0.2 ! == 0.3?
计算机是通过**二进制的方式存储数据**的,浮点数丢失是做进制转换的时候发生,0.1+0.2 转成二进制后相加不等于0.3,不过0.2+0.2这里进制转换得到的是0.4
(1)二进制转换
计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算的两个数的二进制的和。
0.1的二进制是无限循环小数(0.0001100110011001100…(1100循环)),0.2的二进制是无限循环小数(0.00110011001100…(1100循环)),这两个数的二进制都是无限循环的数。
那JavaScript是如何处理无限循环的二进制小数呢?
一般我们认为数字包括整数和小数,但是在 JavaScript 中只有一种数字类型:Number,它的实现遵循IEEE754二进制浮点运算,使用64位固定长度来表示,也就是标准的double双精度浮点数。在二进制科学表示法中,双精度浮点数的小数部分最多只能保留52位,再加上前面的1,其实就是保留小数后面53位有效数字,后面的都会丢失,剩余的需要舍去,遵从“0舍1入”的原则。
根据这个原则,0.1和0.2的二进制数相加,再转化为十进制数就是:0.30000000000000004。
下面看一下双精度数是如何保存的:
- 第一部分(蓝色):用来存储符号位(sign),用来区分正负数,0表示正数,占用1位
- 第二部分(绿色):用来存储指数(exponent),占用11位
- 第三部分(红色):用来存储小数(fraction),占用52位
对于0.1,它的二进制为:
0.00011001100110011001100110011001100110011001100110011001 10011...
转为科学计数法(科学计数法的结果就是浮点数):
1.1001100110011001100110011001100110011001100110011001*2^-4
可以看出0.1的符号位为0,指数位为-4,小数位为:
1001100110011001100110011001100110011001100110011001
那么问题又来了,指数位是负数,该如何保存呢?
IEEE标准规定了一个偏移量,对于指数部分,每次都加这个偏移量进行保存,这样即使指数是负数,那么加上这个偏移量也就是正数了。由于JavaScript的数字是双精度数,这里就以双精度数为例,它的指数部分为11位,能表示的范围就是0~2047,IEEE固定双精度数的偏移量为1023。
- 当指数位不全是0也不全是1时(规格化的数值),IEEE规定,阶码计算公式为 e-Bias。 此时e最小值是1,则1-1023= -1022,e最大值是2046,则2046-1023=1023,可以看到,这种情况下取值范围是-1022~1013。
- 当指数位全部是0的时候(非规格化的数值),IEEE规定,阶码的计算公式为1-Bias,即1-1023= -1022。
- 当指数位全部是1的时候(特殊值),IEEE规定这个浮点数可用来表示3个特殊值,分别是正无穷,负无穷,NaN。 具体的,小数位不为0的时候表示NaN;小数位为0时,当符号位s=0时表示正无穷,s=1时候表示负无穷。
对于上面的0.1的指数位为-4,-4+1023 = 1019 转化为二进制就是:1111111011.
所以,0.1表示为:
0 1111111011 1001100110011001100110011001100110011001100110011001
(2)对阶运算
阶小的尾数要根据阶差来右移,尾数位移时可能会发生数丢失的情况,影响精度
(二)如何解决精度丢失问题(保证浮点数相加减结果符合预期)?
(1)判断小数点后面的位数,使用位数最长的那个值作为10的几次幂与每一个值相乘再进行运算,算出结果后再除以这个10 的几次幂;
0.1+0.02 => 0.1*10^2+0.02*10^2 = 12 => 12/10^2=> 0.12
但是这个方法也存在问题,因为有的情况数据可能会超过js的最大数
(2)直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2-52,在ES6中,提供了Number.EPSILON属性,而它的值就是2-52,只要判断0.1+0.2-0.3是否小于Number.EPSILON,如果小于,就可以判断为0.1+0.2 === 0.3;但是这个方法兼容性差
function numberepsilon(arg1,arg2){return Math.abs(arg1 - arg2) < Number.EPSILON;}console.log(numberepsilon(0.1 + 0.2, 0.3)); // true
或者
(0.1*10+0.2*10)/10 === 0.3 // true
(3)利用第三方库,比如Math.js,big.js等等
(4)都转成字符串,然后对两个字符串做加法运算
8. 如何获取安全的 undefined 值?
因为 undefined 是一个标识符,所以可以被当作变量来使用和赋值,但是这样会影响 undefined 的正常判断。表达式 void _ 没有返回值,因此返回结果是 undefined。void 并不改变表达式的结果,只是让表达式不返回值。因此可以用 void 0 来获得 undefined。
var a = void 0 // undefined
为什么有的情况要用void 0来替换undefined呢?
- undefined可以被重写(局部还可以被重写),void是不能被重写的
- void 运算符能对给定的表达式进行求值,然后返回 undefined(void返回的都是 undefined)
- void 0 是表达式中最短
9. typeof NaN 的结果是什么?
NaN 指“不是一个数字”(not a number),NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。
typeof NaN; // "number"
NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。
而 NaN !== NaN 为 true。
NaN !== NaN // trueNaN === NaN // false
10. isNaN 和 Number.isNaN 函数的区别?
二者都能用于判断是否是NaN,
- 函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。
- 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。
总结:isNaN('ssss') // true 不够准确Number.isNaN('ssss') // false 更加准确
- Number.isNaN 先判断是不是数字类型,是数组类型的话才判断是不是NaN,又是数字类型又是NaN的话则true,其他情况false;
而isNaN有问题,只要不能被转成数字的值就返回true,是NaN的话也是true ,其他情况都是false;
11. == 操作符的类型转换规则?
对于 == 来说,如果对比双方的类型不一样,就会进行类型转换。假如对比 x 和 y 是否相同,就会进行如下判断流程:
首先会判断两者类型是否相同,相同的话就比较两者的大小;
- 类型不相同的话,就会进行类型转换;
- 会先判断是否在对比 null 和 undefined,是的话就会返回 true( null == undefined // true)
- 判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number
5.判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断1 == '1'↓1 == 1
6.判断其中一方是否为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断'1' == true↓'1' == 1↓1 == 1
'1' == { name: 'js' }↓'1' == '[object Object]'
其流程图如下:
12. 其他值到字符串的转换规则?
- Null 和 Undefined 类型 ,null 转换为 “null”,undefined 转换为 “undefined”,
- Boolean 类型,true 转换为 “true”,false 转换为 “false”。
- Number 类型的值直接转换,不过那些极小和极大的数字会使用指数形式。
- Symbol 类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误。
- 对普通对象来说,除非自行定义 toString() 方法,否则会调用 toString()(Object.prototype.toString())来返回内部属性 [[Class]] 的值,如”[object Object]”。如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。
13. 其他值到数字值的转换规则?
- Undefined 类型的值转换为 NaN。
- Null 类型的值转换为 0。
- Boolean 类型的值,true 转换为 1,false 转换为 0。
- String 类型的值转换如同使用 Number() 函数进行转换,如果包含非数字值则转换为 NaN,空字符串为 0。
- Symbol 类型的值不能转换为数字,会报错。
- 对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。
为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先(通过内部操作 DefaultValue)检查该值是否有valueOf()方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。
如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。
字符串转成数字
Number("3.14") // 返回 3.14Number(" ") // 返回 0Number("") // 返回 0Number("99 88") // 返回 NaNparseFloat() // 解析一个字符串,并返回一个浮点数。parseInt(string, radix)// 解析一个字符串,并返回一个整数。// radix可选。表示要解析的数字的基数// radix参数小于 2 或者大于 36,则 parseInt() 将返回 NaN
14. 其他值到布尔类型的值的转换规则?
以下这些是假值:
• undefined
• null
• false
• +0、-0 和 NaN
• “”
假值的布尔强制类型转换结果为 false。从逻辑上说,假值列表以外的都应该是真值。
15. || 和 && 以及if语句?
(1)|| 和 &&
|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔类型,然后再执行条件判断。
- 对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。
- && 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。
|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果
(2)if语句/truly变量/falsely变量
- truly变量:!!a === true的变量
- falsely变量: !!a === false的变量
!!是把他转成truly变量/falsely变量
const a = 100(truly变量)!n //false!!n //trueconst n1 = 0(falsely变量)!n1 // true!!n1 //false!!null (falsely变量)!!undefined (falsely变量)!!NaN (falsely变量) ->不是一个数字,但是是number类型的!!'' (falsely变量)!!{} (truly变量)除此之外 其他都是truly变量我们的if()内判断的是truly变量还是falsely变量;并不是判断值的true还是flase,比如[]为false的,但是!![]是truly变量;所以if()内判断的是!!后的值,而不是它本身是否为true还是false

16. Object.is() 和=== 和 ==的区别?
- 使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。只比较值
- 使用三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。比较值和类型
- 使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。 ``` -0 === +0 // true Object.is(-0,+0)// false
NaN === NaN // false Object.is(NaN,NaN)// true
<a name="xAL9o"></a>## 17. 什么是 JavaScript 中的包装类型?在 JavaScript 中,**基本类型是没有属性和方法的**,但是**为了便于操作**基本类型的值,在调用基本类型的属性或方法时 **JavaScript 会在后台隐式地将基本类型的值转换为对象**,如:
const a = “abc”; a.length; // 3 a.toUpperCase(); // “ABC”
在访问'abc'.length时,JavaScript 将'abc'在后台转换成String('abc'),然后再访问其length属性。JavaScript也可以**使用Object函数显式**地将基本类型转换为包装类型:
var a = ‘abc’ Object(a) // String {“abc”} Object(a).slice(0, 2) // “ab” 可以用对应属性对象的方法了
也可以使用**valueOf**方法将**包装类型倒转成基本类型**:
var a = ‘abc’ var b = Object(a) var c = b.valueOf() // ‘abc’
看看如下代码会打印出什么:
var a = new Boolean( false ); //a是一个对象 Boolean对象 if (!a) { console.log( “Oops” ); // never runs }
答案是什么都不会打印,因为虽然包裹的基本类型是false,但是false被包裹成包装类型后就成了对象,所以其非值为false,所以循环体中的内容不会运行。<a name="oYd2R"></a>## 18. JavaScript 中如何进行隐式类型转换?首先要介绍**ToPrimitive**方法,这是 JavaScript 中每个值隐含的自带的方法,**用来将值** (无论是基本类型值还是对象)**转换为基本类型值**。如果值为基本类型,则直接返回值本身;如果值为对象,其看起来大概是这样:
/**
- @obj 需要转换的对象
- @type 期望的结果类型
/
ToPrimitive(obj,type)
```
type的值为number或者string。
(1)当type为*number时规则如下:
- 调用obj的valueOf方法,如果为原始值,则返回,否则下一步;
- 调用obj的toString方法,后续同上;
- 抛出TypeError 异常。
(2)当type为string时规则如下:
- 调用obj的toString方法,如果为原始值,则返回,否则下一步;
- 调用obj的valueOf方法,后续同上;
- 抛出TypeError 异常。
可以看出两者的主要区别在于调用toString和valueOf的先后顺序。默认情况下:
- 如果对象为 Date 对象,则type默认为string;
- 其他情况下,type默认为number。
总结上面的规则,对于 Date 以外的对象,转换为基本类型的大概规则可以概括为一个函数:
var objToNumber = value => Number(value.valueOf().toString())objToNumber([]) === 0objToNumber({}) === NaN
而 JavaScript 中的隐式类型转换主要发生在+、-、*、/以及==、>、<这些运算符之间。而这些运算符只能操作基本类型值,所以在进行这些运算前的第一步就是将两边的值用ToPrimitive转换成基本类型,再进行操作。
以下是基本类型的值在不同操作符的情况下隐式转换的规则 (对于对象,其会被ToPrimitive转换成基本类型,所以最终还是要应用基本类型转换规则):
1.+操作符:+操作符的两边有至少一个string类型变量时,两边的变量都会被隐式转换为字符串;其他情况下两边的变量都会被转换为数字。
1 + '23' // '123'1 + false // 11 + Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number'1' + false // '1false'false + true // 1
2.-、*、\:操作符NaN也是一个数字
1 * '23' // 231 * false // 01 / 'aa' // NaN
3.对于**==**操作符
操作符两边的值都尽量转成number:
3 == true // false, 3 转为number为3,true转为number为1'0' == false //true, '0'转为number为0,false转为number为0'0' == 0 // '0'转为number为0100 == '100' //true0 == '' //truefalse == '' //truenull == undefined //truelet a=1; let b=1;console.log(a==b);console.log(a===b) // true truelet a=[1,2]; let b=[1,2];console.log(a==b);console.log(a===b); // false false 两者储存的地址并没有指向同一个内容,所以不等

if (obj.a == null){...}// 相当于 if(obj.a === null || obj.a === undefined){...}
4.对于**<和>**比较符
如果两边都是字符串,则比较字母表顺序:
'ca' < 'bd' // false'a' < 'b' // true
其他情况下,转换为数字再比较:
'12' < 13 // truefalse > -1 // true
以上说的是基本类型的隐式转换,而对象会被ToPrimitive转换为基本类型再进行转换:
var a = {}a > 2 // false
{ }>2 其对比过程如下:valueOf,toString,Number
a.valueOf() // {}, 上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步a.toString() // "[object Object]",现在是一个字符串了Number(a.toString()) // NaN,根据上面 < 和 > 操作符的规则,要转换成数字NaN > 2 //false,得出比较结果
又比如:
var a = {name:'Jack'}var b = {age: 18}a + b // "[object Object][object Object]"
运算过程如下:
a.valueOf() // {},上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步a.toString() // "[object Object]"b.valueOf() // 同理b.toString() // "[object Object]"a + b // "[object Object][object Object]"
19. + 操作符?
根据 ES5 规范,如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话,+ 将进行拼接操作。
如果其中一个操作数是对象(包括数组),则首先对其调用 ToPrimitive 抽象操作,该抽象操作再调用 [[DefaultValue]],以数字作为上下文。
如果不能转换为字符串,则会将其转换为数字类型来进行计算。
简单来说就是,如果 + 的其中一个操作数是字符串(或者通过以上步骤最终得到字符串),则执行字符串拼接,否则执行数字加法。
那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字。
100 + 10 // 110100 + '10' // '10010'true + '10'// 'true10'[]+{} // "[object Object]"{} + [] // 0{} + 0 // 0[] + 0 // "0"1+'1' // '11'1+{} // 1+ '[Object object]' '1[Object object]'1 + [] //'1'1+true // 21+function aaa(){} // 1+"function aaa(){} " "1function aaa(){} "[] === false // false[] === true // false[] == true // false[] == {} // false[] == false // true false == false
20. 为什么会有BigInt的提案?
JavaScript中Number.MAX_SAFE_INTEGER表示最⼤安全数字,计算结果是9007199254740991,即在这个数范围内不会出现精度丢失(⼩数除外)。但是⼀旦超过这个范围,js就会出现计算不准确的情况,这在⼤数计算的时候不得不依靠⼀些第三⽅库进⾏解决,因此官⽅提出了BigInt来解决此问题。
21. object.assign和扩展运算法是深拷贝还是浅拷贝,两者区别
扩展运算符:
当一维数据时,扩展运算符可以进行完全深拷贝;数据为多维时,浅拷贝
let outObj = {inObj: {a: 1, b: 2}}let newObj = {...outObj}newObj.inObj.a = 2console.log(outObj) // {inObj: {a: 2, b: 2}}
Object.assign():
第一级是深拷贝,以后各级是浅拷贝
let outObj = {inObj: {a: 1, b: 2}}let newObj = Object.assign({}, outObj)newObj.inObj.a = 2console.log(outObj) // {inObj: {a: 2, b: 2}}
22 强制类型转换/隐式类型转换/显式类型转换

- 强制/显式:paseInt,paseFloat,toString,String(),Boolean
- 隐式:if,+.-.*./,<,>,==
二、ES6
1.es6新特性
https://es6.ruanyifeng.com/#docs/object
- let、const
- 箭头函数
- 数组的扩展和对象方法的扩展(扩展运算符….)
- proxy构造函数,实现数据响应式
- 解构赋值
- rest参数
- 模板字符串、新增字符串方法
- 提供了定义类的语法糖(class)
- 新增了一种基本数据类型(Symbol)
- ES6新增了模块化(import / export)
- ES6新增了Set和Map数据结构。
- ES6新增了生成器(Generator)和遍历器(Iterator)
- Reflect
- Promise 对象
- Generator 函数是 ES6 提供的一种异步编程解决方案
- Symbol
- Class
- async/await
2. let、const、var的区别
(一)let、const、var的区别
| 区别 | var | let | const |
|---|---|---|---|
| 是否有块级作用域 | × | ✔️ | ✔️ |
| 是否存在变量提升 | ✔️ | ||
| 是否存在暂时性死区 | × | ✔️ | ✔️ |
| 是否添加全局属性 | ✔️ | × | × |
| 能否重复声明变量 | ✔️ | × | × |
| 是否必须设置初始值 | × | × | ✔️ |
| 能否改变指针指向 | ✔️ | ✔️ | × |
| ??? | 声明存在变量环境 | 变量会存在词法环境 | 变量会存在词法环境 |
(1)块级作用域:块作用域由 { }包括,let和const具有块级作用域,只能在块作用域里访问,不能跨块访问,也不能跨函数访问,var不存在块级作用域。
块级作用域解决了ES5中的两个问题:
- 内层变量可能覆盖外层变量
- 用来计数的循环变量泄露为全局变量
(2)变量提升:var存在变量提升,let和const不存在变量提升,即变量只能在声明之后使用,否则会报错。
(3)给全局添加属性:浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。
(4)重复声明:var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。
(5)暂时性死区:在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。
(6)初始值设置:在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。
(7)指针指向:let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。
词法环境 变量环境
在每个执行上下文中都会有词法环境,变量环境,和一个隐藏的对外部环境的引用
https://juejin.cn/post/6844903752139276301
https://juejin.cn/post/6844904051369312263
(二) const对象的属性可以修改吗
const保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。
- 对于基本类型的数据(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。
- 但对于引用类型的数据(主要是对象和数组)来说,变量指向数据的内存地址,保存的只是一个指针,const只能保证这个指针是固定不变的,至于它指向的数据结构是不是可变的,就完全不能控制了。
(三)如何在es5条件下实现一个let
我们可以先采用模拟块级作用域的形式;主要是控制作用域
// es6的letfor(let i = 0; i < 5; i ++){console.log(i) // 0 1 2 3 4}console.log(i) // Uncaught ReferenceError: i is not defined// es5实现的let(function(){for(var i = 0; i < 5; i ++){console.log(i) // 0 1 2 3 4}})();console.log(i) // Uncaught ReferenceError: i is not defined
(四)如何在es5条件下实现一个const
实现const的关键在于Object.defineProperty()这个API,这个API用于在一个对象上增加或修改属性。通过配置属性描述符,可以精确地控制属性行为。Object.defineProperty() 接收三个参数:
Object.defineProperty(obj, prop, desc)Object.defineProperty(在其上定义属性的对象, 要定义或修改的属性的名称, 将被定义或修改的属性描述符)Object.defineProperty() 内部有个属性writable(当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false)
对于const不可修改的特性,我们通过设置writable:false属性来实现
function _const(key, value) {var desc = {value,writable: false}Object.defineProperty(window, key, desc)}_const('obj', {a: 1}) //定义objobj.b = 2 //可以 正常 给obj的属性赋值,可以新增属性但是不能更改地址obj = {} //无法 赋值新对象
(五)let和var的原理
(这里想听到的是let和var的底层实现)
现在在浏览器上面没有一个真正的对let的实现,es6中的所有语法都是可以通过babel转义成es5的,如果我们从babel转义let实现let的块级作用域,只能通过 es5的闭包+函数作用域 来模拟块级作用域,let在很多情况都会转换成var,但在某些函数里面以及形成块级作用域的代码块里面我们其实是把它变成一个闭包,自调用函数去执行它,js中只有两个作用域,一个全局作用域一个局部作用域,局部作用域是用函数作用域去模拟的,let是用闭包模拟的函数作用域,最后在函数里面模拟块级作用域,这一块我当时有专门去研究,去看了一下babel转义的结果,就是这么一个结果,至于它在其他浏览器上的实现以及不同浏览器对他的兼容性处理是不一样的。
3 箭头函数
箭头函数是ES6中的提出来的:
function name(a1,a2){...}// 可以写成(a1,a2) => {...}
(一)如果new一个箭头函数的会怎么样
不能new,new一个箭头函数的话会报错:该函数不是一个constructor;因为箭头函数内部没有prototype,但有proto
它没有prototype,也没有自己的this指向,更不可以使用arguments参数(伪数组),所以不能New一个箭头函数。
new操作符的实现步骤如下:
- 创建一个对象
- 将构造函数的作用域赋给新对象(也就是将新对象的proto属性指向构造函数的prototype属性)
- 指向构造函数中的代码,构造函数中的this指向该对象(也就是为这个对象添加属性和方法)
- 返回新的对象
所以,上面的第二、三步,箭头函数都是没有办法执行的。
(二)箭头函数与普通函数的区别
7点:写法更简洁;没有自己的this;指向外层上下文环境;继承来的this就算用call,bind,apply也不会被改变,不能new;没有arguments;没有prototype有proto;不能作为迭代器函数,
(1)箭头函数比普通函数更加简洁
- 如果没有参数,就直接写一个空括号即可
- 如果只有一个参数,可以省去参数的括号: var fun = x=>x*x
- 如果有多个参数,用逗号分割
- 如果函数体的返回值只有一句,可以省略大括号
- 如果函数体不需要返回值,且只有一句话,可以给这个语句前面加一个void关键字。最常见的就是调用一个函数:
(2)箭头函数没有自己的thislet fn = () => void doesNotReturn();
箭头函数不会创建自己的this, 所以它没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了,之后不会改变。
(3)箭头函数继承来的this指向永远不会改变
对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。需要注意,定义对象的大括号{}是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中。var id = 'GLOBAL';var obj = {id: 'OBJ',a: function(){console.log(this.id);},b: () => {console.log(this.id);}};obj.a(); // 'OBJ'obj.b(); // 'GLOBAL'new obj.a() // undefinednew obj.b() // Uncaught TypeError: obj.b is not a constructor
(4)call()、apply()、bind()等方法不能改变箭头函数中this的指向
(5)箭头函数不能作为构造函数使用var id = 'Global';let fun1 = () => {console.log(this.id)};fun1(); // 'Global'fun1.call({id: 'Obj'}); // 'Global'fun1.apply({id: 'Obj'}); // 'Global'fun1.bind({id: 'Obj'})(); // 'Global'
构造函数在new的步骤在上面已经说过了,实际上第二步就是将函数中的this指向该对象。 但是由于箭头函数时没有自己的this的,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。不具有super。
(6)箭头函数没有自己的arguments
箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。
(7)箭头函数没有prototype
(8)箭头函数不能用作Generator函数,不能使用yeild关键字(generator : 生成器,不断地调用generator对象的next()方法。。生成器 99.999% 的用途都是拿它来实现异步编程,并不是真的需要生成器本来的用途,自从有了 async/await,生成器越来越没人用了)
(三)箭头函数的this指向哪⾥?
箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于⾃⼰的this,它所谓的this是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值,并且由于没有属于⾃⼰的this,所以是不会被new调⽤的,这个所谓的this也不会被改变。
可以⽤Babel理解⼀下箭头函数:
// ES6const obj = {getArrow() {return () => {console.log(this === obj);};}}obj.getArrow()
转化后:
// ES5,由 Babel 转译var obj = {getArrow: function getArrow() {var _this = this;return function () {console.log(_this === obj);};}};obj.getArrow()
(四)如果用箭头函数调用call/apply/bind方法会怎么样
会执行不会报错;
本质原因:虽然箭头函数自身属性没有那些方法,但箭头函数也是函数,它的proto指Function.prototype,call/apply/bind这些方法是挂在在大写的Funciton.prototype上的,所以可以调用不会报错,箭头函数继承来的this一旦确定就不会改变,所以这些方法也不能改变它的this指向;
箭头函数可以用bind,不会报错,但是会没有效果
var name = 'xrr0'function jaintou(){let fun1 = () => {console.log(this.name)}fun1.bind({name:'xrr1'})()}jaintou() // xrr0
4. 扩展运算符
(1)对象扩展运算符
对象的扩展运算符(…)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中。
let bar = { a: 1, b: 2 };let baz = { ...bar }; // { a: 1, b: 2 }
上述方法实际上等价于Object.assign({}, bar):
let bar = { a: 1, b: 2 };let baz = Object.assign({}, bar); // { a: 1, b: 2 }
Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。(如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性)。
同样,如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。
let bar = {a: 1, b: 2};let baz = {...bar, ...{a:2, b: 4}}; // {a: 2, b: 4}
利用上述特性就可以很方便的修改对象的部分属性。在redux中的reducer函数规定必须是一个纯函数,reducer中的state对象要求不能直接修改,可以通过扩展运算符把修改路径的对象都复制一遍,然后产生一个新的对象返回。
需要注意:扩展运算符对对象实例的拷贝属于浅拷贝。
(2)数组扩展运算符
数组的扩展运算符可以将一个数组转为用逗号分隔的参数序列,且每次只能展开一层数组。
console.log(...[1, 2, 3])// 1 2 3console.log(...[1, [2, 3, 4], 5])// 1 [2, 3, 4] 5
下面是数组的扩展运算符的应用:
- 将数组转换为参数序列/Math传参(函数获取数组中特定的值) ``` function add(x, y) { return x + y; } const numbers = [1, 2]; add(…numbers) // 3
const numbers = [9, 4, 7, 1]; Math.min(…numbers); // 1 Math.max(…numbers); // 9
2. **复制数组(一层深拷贝,多层浅拷贝)**
const arr1 = [1, 2]; const arr2 = […arr1];
3. **合并数组**解构赋值:从一个集体中分解出自己想要的并赋值给特定的属性<br /><br />如果想在数组内合并数组,可以这样:
const arr1 = [‘two’, ‘three’]; const arr2 = [‘one’, …arr1, ‘four’, ‘five’]; // [“one”, “two”, “three”, “four”, “five”]
4. 扩展运算符与解构赋值结合起来,用于**生成数组**
const [first, …rest] = [1, 2, 3, 4, 5]; first // 1 rest // [2, 3, 4, 5]
需要注意:如果**将扩展运算符**用于**数组赋值**,只能放在参数的最后一位,否则会报错。
const […rest, last] = [1, 2, 3, 4, 5]; // 报错 const [first, …rest, last] = [1, 2, 3, 4, 5]; // 报错
5. 将**字符串转为**真正的**数组**
[…’hello’] // [ “h”, “e”, “l”, “l”, “o” ]
6. 任何 Iterator 接口的对象,都可以用扩展运算符转为真正的数组(**伪数组转成真正的数组**)比较常见的应用是可以将某些数据结构转为数组:
// arguments对象 function foo() { const args = […arguments]; // Array.from(arguments) }
用于**替换**es5中的**Array.prototype.slice.call(arguments)**写法。<a name="nAktS"></a>## 5.内置对象Reflect反射就是一种动态去执行方法的技术,反射机制指的是程序在运行时能够获取自身的信息<br />Reflect是一个内置的对象,它提供拦截 JavaScript 操作的方法,并非一个构造函数,Reflect的所有属性和方法都是静态的。·<br />与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API
// 往target添加property属性,值为value Reflect.set(target,property,value)
// 从target中获取property这个键,如果指定了getter则receiver代表调用时的this指 return Reflect.get(target,property,receiver)
<br /><a name="n08co"></a>## 6. Proxy 可以实现什么功能?在 Vue3.0 中通过 Proxy 来**替换**原本的 **Object.defineProperty** 来实现数据响应式。<br />Proxy 是 ES6 中新增的功能,它可以用来**自定义对象中的操作**
let p = new Proxy(target, handler)
target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。<br />下面来通过 Proxy 来实现一个数据响应式:
let onWatch = (obj, setBind, getLogger) => {let handler = {set(target, property, value, receiver) {// 调用自定义set的回调(派发更新)setBind(value, property)// 往target添加property属性,值为valuereturn Reflect.set(target,property,value)},get(target, property, receiver) {// 调用自定义get的回调(收集依赖)getLogger(target, property)// 从target中获取property这个键,如果指定了getter则receiver代表调用时的this指return Reflect.get(target,property,receiver)}}// 返回一个Proxy对象return new Proxy(obj, handler)}let obj = { a: 1 }let p = onWatch(obj,(v, property) => {console.log(`监听到属性${property}改变为${v}`)},(target, property) => {console.log(`'获取${property}属性' = ${target[property]}`)})
p.a = 2 // 监听到属性a改变 p.a // ‘a’ = 2
在**上述代码**中,通过自定义 set 和 get 函数的方式,在原本的逻辑中插入了我们的函数逻辑,**实现了在对对象任何属性进行读写时发出通知**。<br />当然这是简单版的响应式实现,**如果需要实现一个 Vue 中的响应式,需要在 get 中收集依赖,在 set 派发更新**,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于** Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到(**不能监听通过数组下标修改数组,和给数组新增方法等造成的数据变化**),但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷就是浏览器的兼容性不好。**<a name="FFbnX"></a>## 7. 解构解构是 ES6 提供的一种新的提取数据的模式,这种模式能够从对象或数组里有**针对性**地拿到想要的数值。<a name="S1Shl"></a>### 1)数组的解构在解构数组时,以元素的位置为**匹配条件**来提取想要的数据的:
const [a, b, c] = [1, 2, 3]
// 最终,a、b、c分别被赋予了数组第0、1、2个索引位的值. a // 1 b // 2 c // 3
数组里的0、1、2索引位的元素值,精准地被映射到了左侧的第0、1、2个变量里去,这就是数组解构的工作模式。还可以通过给左侧变量数组设置空占位的方式,实现对数组中某几个元素的精准提取,通过把中间位留空,可以顺利地把数组第一位和最后一位的值赋给 a、c 两个变量:
const [a,,c] = [1,2,3]
a // 1 c // 3
<a name="NQTUq"></a>### 2)对象的解构对象解构比数组结构稍微复杂一些,也更显强大。在解构对象时,是**以属性的名称**为**匹配条件**,来提取想要的数据的。现在定义一个对象:
const stu = { name: ‘Bob’, age: 24 }
// 解构它的两个自有属性,可以这样: const { name, age } = stu
// 这样就得到了 name 和 age 两个和 stu 平级的变量: name // ‘Bob’ age // 24
注意,对象解构严格以**属性名**作为**定位依据**,所以就算调换了 name 和 age 的位置,结果也是一样的:<a name="igCp0"></a>### 3)如何提取高度嵌套对象里的指定属性?有时会遇到一些嵌套程度非常深的对象:
const school = { classes: { stu: { name: ‘Bob’, age: 24, } } }
name 位于 school 对象的“儿子的儿子”对象里面。要想把 name 提取出来,一种比较笨的方法是逐层解构:
const { classes } = school const { stu } = classes const { name } = stu name // ‘Bob’
但是还有一种更标准的做法,可以用一行代码来解决这个问题:
const { classes: { stu: { name } }} = school
console.log(name) // ‘Bob’
可以在解构出来的变量名右侧,通过 **冒号+{目标属性名}(:{ 属性名 })** 这种形式,进一步解构它,一直解构到拿到目标数据为止。<a name="UrPwx"></a>## 8. 对 rest 参数的理解**扩展运算符**被**用在**函数**形参上**,为了处理获取函数的多余参数或者是函数参数个数不确定的情况<br />它还可以把一个数组**分离成多个参数**:
function mutiple(…args) { let result = 1; for (var val of args) { result *= val; } return result; } mutiple(1, 2, 3, 4) // 24
这里,传入 mutiple 的是四个分离的参数,但是如果在 mutiple 函数里尝试输出 args 的值,会发现它是一个数组:
function mutiple(…args) { console.log(args) } mutiple(1, 2, 3, 4) // [1, 2, 3, 4]
这就是 **… rest运算符**的又一层威力了,它**可以把函数的多个入参整合成一个数组**。<a name="Ja2nO"></a>## 9. 模板字符串ES6 提出了“模板语法”的概念。在 ES6 以前,拼接字符串是很麻烦的事情:
var name = ‘css’
var career = ‘coder’
var hobby = [‘coding’, ‘writing’]
var finalString = ‘my name is ‘ + name + ‘, I work as a ‘ + career + ‘, I love ‘ + hobby[0] + ‘ and ‘ + hobby[1]
//仅仅几个变量,写了这么多加号,还要时刻小心里面的空格和标点符号有没有跟错地方。但是有了模板字符串,拼接难度直线下降:
var finalString = my name is ${name}, I work as a ${career} I love ${hobby[0]} and ${hobby[1]}
字符串不仅更容易拼了,也更易读了,代码整体的质量都变高了。这就是模板字符串的第一个优势——允许用${}的方式嵌入变量。但这还不是问题的关键,模板字符串的关键优势有两个:- 在模板字符串中,空格、缩进、换行都会被保留- 模板字符串完全支持“运算”式的表达式,可以在${}里完成一些计算基于第一点,可以在模板字符串里无障碍地直接写 **html** 代码:
let list = <ul>
<li>列表项1</li>
<li>列表项2</li>
</ul>;
console.log(message); // 正确输出,不存在报错
基于第二点,可以把一些简单的**计算和调用**丢进 ${} 来做:
function add(a, b) {
const finalString = ${a} + ${b} = ${a+b}
console.log(finalString)
}
add(1, 2) // 输出 ‘1 + 2 = 3’
除了模板语法外, ES6中还新增了一系列的**字符串方法**用于提升开发效率:- 存在性判定:在**过去**,当判断一个字符/字符串是否在某字符串中时,**只能用 indexOf > -1** 来做。现在 ES6 提供了三个方法:**includes、startsWith、endsWith**,它们都会**返回一个布尔值**来告诉你是否存在。- **includes**:判断字符串与子串的**包含关系**:
const son = ‘haha’ const father = ‘xixi haha hehe’ father.includes(son) // true
- **startsWith**:判断字符串**是否以**某个/某串字符**开头**:
const father = ‘xixi haha hehe’ father.startsWith(‘haha’) // false father.startsWith(‘xixi’) // true
- **endsWith**:判断字符串**是否以**某个/某串字符**结尾**:
const father = ‘xixi haha hehe’ father.endsWith(‘hehe’) // true
- 自动重复:可以使用 **repeat** 方法来使同一个字符串输出多次(被连续复制多次):
const sourceCode = ‘repeat for 3 times;’ const repeated = sourceCode.repeat(3) console.log(repeated) // repeat for 3 times;repeat for 3 times;repeat for 3 times;
<a name="E8SB6"></a>#<a name="GuPMM"></a>## 10 class 类JavaScript语言中,**生成实例对象**的传统方法是通过构造函数。由于这种语法与c++,java相差有点大,故ES6引入了class这个**语法糖**,新的class写法让对象原型的写法更加清晰,更像面向对象的编程。<br />作为对象的模板。通过class关键字,可以定义类。
提示 1.class是ES6语法规范,由ECMA委员会发布 2.ECMA 只规定语法规则,代码的书写规范,不规定如何实现 3.以上说的都是V8引擎的实现方式,也是主流的
- constructor/子类目的constructor要执行super()- 属性- 方法
// js构造函数的写法: function MathHandle(x, y) { this.x = x; this.y = y; } MathHandle.prototype.add = function () { return this.x + this.y; }; var m = new MathHandle(1,2); console.log(m.add())
// ES6的class写法: class MathHandle { constructor(x, y) { this.x = x; this.y = y; } add() { return this.x + this.y; } } const m = new MathHandle(1, 2); console.log(m.add())
给大家看两个例子:- js构造函数实现的继承:
// 动物 function Animal() { this.eat = function () { console.log(‘animal eat’) } } // 狗 function Dog() { this.bark = function () { console.log(‘dog bark’) } } Dog.prototype = new Animal()
// 哈士奇 var hashiqi = new Dog() console.log(hashiqi) // {bark: ƒ (),proto:Animal}
- ES6 class实现的继承1. extends1. super1. 扩展或重写的方法
直接打印一个未声明的变量会报错 xxx is not defined 但直接打印一个对象内不存在的值会是undefined
关于class有5点要注意的:1. class是一种新的语法形式,是class Name {...}这种形式写的1. 两者对比,class的构造函数的**函数体要写在constructor中**,constructor 即构造器,初始化实例时默认执行1. **class中函数的写法是add(){...}**,并没有function关键字1. 使用extends可以**实现继承**,更加符合面向对象的语言写法1. 子类的constructor一定要执行super(),以调用父类的constructor
// 父类
class People {
constructor(name){
this.name = name
}
eat(){
console.log(${this.name}是一个漂亮妹子)
}
}
// 子类 继承的话要记得在constructor内super
class Student extends People {
constructor(name,number){
super(name)
this.number = number
}
sayHi(){
console.log(姓名${this.name}漂亮妹子还对你招了手${this.number})
}
}
// 再写一个继承People的子类
class Teacher extends People {
constructor(name,major) {
super(name)
this.major = major
}
teacher(){
console.log(${this.name}教授${this.major})
}
}
// 实例 const xialuo = new Student(‘夏洛’, 100) console.log(xialuo.name) //夏洛 console.log(xialuo.Siholll)//undefined xialuo.sayHi() xialuo.eat() xialuo instanceof Student // true xialuo instanceof People // true
// 了解this指向后补充一下 xialuo.proto.sayHi() // 姓名undfined漂亮妹子还对你招了手undfined // 这里undefined其实是因为xialuo.proto调用了sayHi()方法,this指向的是xialuo.proto
// xialuo.sayHi() // 其实相当于xialuo.proto.sayHi.call(xialuo) ,但内部不是这样执行的
ES6的class中的static方法和普通方法的区别,class中的static方法不会被实例继承,所以构造函数构造出来的实例不能调用static定义的方法,会报错
class Chameleon { static colorChange(newColor) { this.newColor = newColor; }
constructor({ newColor = "green" } = {}) {this.newColor = newColor;}
} const freddie = new Chameleon({ newColor: “purple” }); freddie.colorChange(“orange”);// 报错
<a name="EHFRU"></a>## 11 数组和对象方法的扩展[https://es6.ruanyifeng.com/#docs/object](https://es6.ruanyifeng.com/#docs/object)<br /><br /><a name="e5K8Y"></a>## 12 生成器(Generator)和遍历器(Iterator)Generator 函数(只是在function后面,函数名前面加了个*)是 ES6 提供的一种异步编程解决方案,<br />Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。<br />他其实就是es6定义的一个迭代器:<br />调用生成器函数并不会执行函数体,而是返回一个生成器对象、这个生成器对象是一个迭代器。调用它的next()方法会导致生成器函数的函数体从头开始执行,直到遇见一个yield语句,类似于return语句,yield语句的值会成为调用迭代器的next()方法的返回值。
function* oneDigitPrimes(){ yeild 2; yeild 3; yeild 5; yeild 7; } // 调用这个方法并不会运行下面的代码,照顾会返回一个生成器对象,调用next()会开始执行直至yield提供返回值
let primes = oneDigitPrimes() // 调用生成器函数得到一个生成器
<a name="bdg4x"></a>## 13 Set和MapSet 和 Map 都是 ES6 中新增的**数据结构**,是对当前 JS **数组和对象**这两种重要数据结构的**扩展**。- Set类似于数组,不过数组内部可以有重复的元素,但Set内不可以有重复的元素,会自动过滤掉,是个对象数据类型,通常需要...解析成数组- Map类似于对象,不过普通的对象的key必须是字符串或者数字,而新增的Map中的key可以是任何数据类型。<a name="S7u1B"></a>#### (一)Set(默认去重)1. Set实例不允许元素有重复的,即使你通过它添加元素的方法往内部添加元素也会被去重掉1. Set实例的5个属性和方法
size:获取元素的数量 add(value):添加元素到数组中,返回Set实例本身 delete(value):删除数组中的某元素,返回Boolean值表示删除是否成功 has(value):返回Boolean值,表示该值是否是Set实例的元素 clear():清除所有元素,没有返回值
3. Set实例的遍历:
keys():返回键名的遍历器 values():返回值的遍历器,因为Set结构没有键名,只有键值,所以keys()和value()返回的结果一样 entries():返回键值对的遍历器 forEach():使用回调函数遍历每个成员
<a name="SUpTZ"></a>#### (二)Map(键可以为任何类型)1. Map map.set(obj, 'OK') 就是用对象obj作为key,'OK'为值 (不光可以是对象,任何数据类型都可以为键)2. Map实例6个属性和方法:
size:获取成员数量 set:设置key和value get:获取成员的属性值 has:判断成员是否存在 delete:删除成员 clear:清空所有
3. Map 实例的遍历方法有:
keys() :返回键名的遍历器。 values() :返回键值的遍历器。 entries() :返回所有成员的遍历器。 forEach() :遍历 Map 的所有成员。
<a name="lihmb"></a>## 14 模块化(import / export)<a name="aeNpS"></a>### (1)什么是模块化?模块化通常是我们用来把一个复杂的大程序,根据功能或业务将它拆分成互相依赖的小文件,再用简单的方式拼装起来。<a name="zCnc5"></a>### (2)模块化发展史1. 最开始IIFE(立即执行函数,闭包,最简单的模块化),1. 然后nodejs想了想我可以整一个commonjs,然后commonjs就出来了,利用require导入 module.exports = value / exports.xx = value 导出,cmj的require事实上是可以在代码块中执行的,同步执行,值拷贝,同步,可缓存1. 然后浏览器说靠,那我也要一个,然后就是amd,这时候采用的是异步执行的和commonjs已经有了不同了,define定义 require引入 ,exports导出,1. 然后浏览器就继续基于amd发展出cmd,大概是amd的定义太麻烦了,cmd就近依赖,amd依赖必须前置。依赖后置 require定义 exports导出1. 再然后有人就想说我想搞一个规范nodejs环境和浏览器都能用上,然后就有了umd,实现原理其实就是对作用环境做一个判断,nodejs我就用commonjs,浏览器我就用amd(没用cmd的原因大概是这时候cmd还不够成熟),如果都不支持就挂到全局window,umd内部还是amd + commonjs1. 基于umd然后就有了esm,也是都支持两个环境的,但是仍然保留了amd遗传的特性就是异步执行,且是值引用ES6 中模块化语法更加简洁;import导入 export / export default导出,按需加载,import export不能放在代码块中,异步,值引用```php// 创建 util2.js 文件,内容如export function fn1() {alert('fn1')}export function fn2() {alert('fn2')}// 创建 index.js 文件,内容如--如果想要输出多个对象,就不能用 default 了,且 import 时候要加 {...} ,我们一般在写api的时候,调用引入接口的时候,也是这样写的,还有引入一些组件啊方法啊,都是这样引入,可以简化代码import { fn1, fn2 } from './util2.js'fn1()fn2()
(3)ES6模块与CommonJS模块的异同点
| CommonJS | ES module |
|---|---|
| 对模块的浅拷贝(基本类型是值复制,引用类型是浅拷贝) | 对模块的引用, |
| 可以重新赋值(改变指针指向) | 只存只读,不能改变其值,也就是指针指向不能改变,赋值会编译报错;import的接口是read-only(只读状态),不能修改其变量值(不能修改指针指向),可调用引入中包含的方法,可改变变量内部指针指向。可调用引入中包含的方法 |
| require导入 module.exports = value / exports.xx = value 导出 |
import导入 export / export default导出 |
| 运行时加载 | 静态加载,编译时执行 |
| 1.检查是否有该模块的缓存2.如果有使用缓存3.没有则执行该模块代码并缓存 | 动态引用,不缓存,会成为一个指向被加载模块的引用 1检查该模块是否引入过2.是则暂时该模块为{} 3.否,进入该模块并执行代码,不做缓存 |
| 同步加载,后面的代码必须等待这个命令执行完,才会执行 | 异步加载,有一个独立的模块依赖的解析阶段 |
| 在 Node.js 遇到.mjs文件,就认为它是 ES6 模块,默认启用严格模式 | |
| 顶层的this指向当前模块 | 顶层this指向undefined |
共同点: 1. 都可以对引入的对象进行赋值,即对对象内部属性的值进行改变。
https://www.ruanyifeng.com/blog/2020/08/how-nodejs-use-es6-module.html
https://es6.ruanyifeng.com/#docs/module-loader
15 promise
看下面的异步编程
三、JavaScript基础
1. new操作符
new操作符的执行过程:
1 新建了一个新的空对象
2 设置原型,将构造对象的prototype设置给新对象的proto属性
3 将this指向新对象,通过call/apply等方法执行构造函数改变this指向,将属性和方法赋值给新对象;
4 返回这个新对象;判断返回的是引用类型还是原始类型,如果是原始类型就返回原始类型的值,如果是引用类型的话就返回引用类型
具体实现:
// 关于argumentsfunction objectFactory(){let newObject = nullconsole.log(arguments)}objectFactory('11', '22') // Arguments(2) ["11", "22", callee: ƒ, Symbol(Symbol.iterator): ƒ]Object.create(新创建对象的原型对象,(可选))
function objectFactory(){// 把数组的第一个元素从其中删除,并返回第一个元素的值=>第一个为构造函数后面的为参数let constructor = Array.prototype.shift.call(arguments)// 限定第一个参数必须为构造函数if (typeof constructor !== "function") {console.error("第一个参数应为函数")return}// 创建一个空对象let newObject = null// 将构造函数的prototype赋给新对象的__proto__newObject = Object.create(constructor.prototype)// 相当于newObject.constructor(arguments)let result = null // 将this指向新对象,执行构造函数,将属性方法赋值给添加到这个新对象里result = constructor.apply(newObject, arguments)// 判断返回的类型-对象或者函数let flag = result && (typeof result === "object" || typeof result === "function")// 如果是函数或者对象则返回结果,否则返回构造函数return flag ? result : newObject}// 使用方法objectFactory(构造函数, 初始化参数);let a = objectFactory(Array, '111', '2222')console.log(a, 'aa') // ["111", "2222"] "aa"
2. Map和Object的区别(了解即可)
| Map | Object | |
|---|---|---|
| 意外的键 | Map默认情况不包含任何键,只包含显式插入的键。 | Object 有一个原型, 原型链上的键名有可能和自己在对象上的设置的键名产生冲突。 |
| 键的类型 | Map的键可以是任意值,包括函数、对象或任意基本类型。 | Object 的键必须是 String 或是Symbol。 |
| 键的顺序 | Map 中的 key 是有序的。因此,当迭代的时候, Map 对象以插入的顺序返回键值。 | Object 的键是无序的 |
| Size | Map 的键值对个数可以轻易地通过size 属性获取 | Object 的键值对个数只能手动计算 |
| 迭代 | Map 是 iterable 的,所以可以直接被迭代。 | 迭代Object需要以某种方式获取它的键然后才能迭代。 |
| 性能 | 在频繁增删键值对的场景下表现更好。 | 在频繁添加和删除键值对的场景下未作出优化。 |
3. map和weakMap的区别(了解即可)
(1)Map
map本质上就是键值对的集合,但是普通的Object中的键值对中的键只能是字符串和Symbol。而ES6提供的Map数据结构类似于对象,但是它的键不限制范围,可以是任意类型,是一种更加完善的Hash结构。如果Map的键是一个原始数据类型,只要两个键严格相同,就视为是同一个键。
实际上Map是一个数组,它的每一个数据也都是一个数组,其形式如下:
const map = [["name","张三"],["age",18],]
Map数据结构有6个操作方法(size,set,get,has,delete,clear)
Map结构原生提供是3个遍历器生成函数和1个遍历方法(keys,values,entries,forEach)
const map = new Map([["foo",1],["bar",2],])for(let key of map.keys()){console.log(key); // foo bar}for(let value of map.values()){console.log(value); // 1 2}for(let items of map.entries()){console.log(items); // ["foo",1] ["bar",2]}map.forEach( (value,key,map) => {console.log(key,value); // foo 1 bar 2})
(2)WeakMap
WeakMap 对象也是一组键值对的集合,其中的键是弱引用的。其键必须是对象,原始数据类型不能作为key值,而值可以是任意的。
该对象也有以下4种方法:
- set(key,value):设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用)
- get(key):该方法读取key对应的键值,如果找不到key,返回undefined。
- has(key):该方法返回一个布尔值,表示某个键是否在当前Map对象中。
- delete(key):该方法删除某个键,返回true,如果删除失败,返回false。
- 其clear()方法已经被弃用,所以可以通过创建一个空的WeakMap并替换原对象来实现清除。
WeakMap的设计目的在于,有时想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,就必须手动删除这个引用,否则垃圾回收机制就不会释放对象占用的内存。
而WeakMap的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
总结:
- Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
- WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。但是 WeakMap 只接受对象作为键名( null 除外),不接受其他类型的值作为键名。而且 WeakMap 的键名所指向的对象,不计入垃圾回收机制。
4. JavaScript有哪些内置对象
全局的对象( global objects )或称标准内置对象,不要和 “全局对象(global object)” 混淆。这里说的全局的对象是说在全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。
标准内置对象的分类:
(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。
例如 Infinity、NaN、undefined、null 字面量
(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。
例如 eval()、parseFloat()、parseInt() 等
(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。
例如 Object、Function、Boolean、Symbol、Error 等
(4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。
例如 Number、Math、Date
(5)字符串,用来表示和操作字符串的对象。
例如 String、RegExp
(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。例如 Array
(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。
例如 Map、Set、WeakMap、WeakSet
(8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。
例如 SIMD 等
(9)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。
例如 JSON 等
(10)控制抽象对象
例如 Promise、Generator 等
(11)反射
例如 Reflect、Proxy
(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。
例如 Intl、Intl.Collator 等
(13)WebAssembly
(14)其他
例如 arguments
总结:
js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js 定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。
5. 常用的正则表达式有哪些?
// (1)匹配 16 进制颜色值var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;// (2)匹配日期,如 yyyy-mm-dd 格式var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;// (3)匹配 qq 号var regex = /^[1-9][0-9]{4,10}$/g;// (4)手机号码正则var regex = /^1[34578]\d{9}$/g;// (5)用户名正则var regex = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;
6. 对JSON的理解(了解即可)
JSON 是一种基于文本的轻量级的数据交换格式。它可以被任何的编程语言读取和作为数据格式来传递。
在项目开发中,使用 JSON 作为前后端数据交换的方式。在前端通过将一个符合 JSON 格式的数据结构序列化为
JSON 字符串,然后将它传递到后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递。
因为 JSON 的语法是基于 js 的,因此很容易将 JSON 和 js 中的对象弄混,但是应该注意的是 JSON 和 js 中的对象不是一回事,JSON 中对象格式更加严格,比如说在 JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等,因此大多数的 js 对象是不符合 JSON 对象的格式的。
在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理,
- JSON.stringify 函数,通过传入一个符合 JSON 格式的数据结构,将其转换为一个 JSON 字符串。如果传入的数据结构不符合 JSON 格式,那么在序列化的时候会对这些值进行对应的特殊处理,使其符合规范。在前端向后端发送数据时,可以调用这个函数将数据对象转化为 JSON 格式的字符串。
- JSON.parse() 函数,这个函数用来将 JSON 格式的字符串转换为一个 js 数据结构,如果传入的字符串不是标准的 JSON 格式的字符串的话,将会抛出错误。当从后端接收到 JSON 格式的字符串时,可以通过这个方法来将其解析为一个 js 数据结构,以此来进行数据的访问。
- JSON.parse(JSON.stringify(obj) )函数,这个可以用来深拷贝obj
7. JavaScript脚本延迟加载的方式有哪些?
js脚本异步加载/如果我们想让我们的js脚本异步加载的话有什么方法呢?
script标签中的async defer可以进行异步加载,这样可以提高页面的加载速度;
两者区别:async:加载完后立即执行,执行顺序不能保证的,因为async是异步加载js脚本;defer:defer是把文档内容都解析完之后才执行js脚本,会按加载顺序去执行。
延迟加载就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度。
一般有以下5种方式:
- **defer 属性:给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件**,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
- **async 属性:给 js 脚本添加 async 属性,这个属性会使脚本异步加载**,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
- **动态创建 DOM 方式:动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本**。
- **使用 setTimeout 延迟方法:设置一个定时器**来延迟加载js脚本文件
- **让 JS 最后加载:**将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。
8. 类数组/伪数组的定义和转换?
(一)什么是伪数组?
一个拥有 length 属性(可以通过length获取长度)和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。
(二)为什么函数的arguments参数是类数组?如何遍历?
arguments是一个对象,它的属性是从 0 开始依次递增的数字,还有callee和length等属性,与数组相似;但是它却没有数组常见的方法属性,如forEach, reduce等,所以叫它们类数组。而且arguments这个对象是浏览器的内置对象,就是代表的伪数组。
想遍历伪数组的话可以先把它转成数组再进行遍历,或者直接使用for循环或者for…in…
(三)常见的类数组有哪些?
(1)内置对象 arguments(比如函数参数)
(2)String.prototype
(3)Function.prototype.constructor
(4)Object.prototype.constructor
(5)DOM 方法的返回结果
(6)DOM相关的数组基本都是类数组(window(length属性是frame/iframe数量) storage(比如localStorage、sessionStorage,length属性是里面存的数据的条数,说起来这能算数组吗……) FileList(File API的玩意)除此之外还有好多好多)
(四)常见的类数组转换为数组的方法有这样几种:
(1)通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arguments);// 或者[].slicee.call(arguments)
(2)通过 call 调用数组的 splice 方法来实现转
Array.prototype.splice.call(arguments, 0);// 或者[].splice.call(arguments, 0)
(3)通过 apply 调用数组的 concat 方法来实现转换
Array.prototype.concat.apply([], arguments);// 或者[].concat.apply([], arguments)
(4)通过 Array.from 方法来实现转换(Array.from()方法就是将一个类数组对象或者可遍历对象转换成一个真正的数组。)
Array.from(arguments);
(5)通过 扩展运算符 来实现转换
var arr = [... arguments]
(6)通过 for循环 来实现转换
var arr1 = []for (var i = 0; i<arguments.length; i++) {arr.push(arguments[i])}
9. 数组的方法
在回答这类题目的时候可以从这几个方面去回答:
- 功能是什么?
- 返回值是什么?
- 是否会对原数组造成影响? (纯函数:1.不改变原数组,2.返回一个全新的数组)
删除数组最后一个元素
pop() arr.splice(0,arr.length-1)
(一)有哪些原生方法?
- 数组和字符串的转换方法:toString()、toLocalString()、join(),(其中 join() 方法可以指定转换为字符串时的分隔符,数组中的所有元素转换一个字符串)。
- 数组尾部操作的方法 pop() 和 push()。 ``` const arr = [10, 20, 30, 40]
// pop 从后删掉一个值, 返回删除值,改变原数组 const popRes = arr.pop() console.log(popRes, arr)// 40 [10, 20, 30]
// push 从后追加一个值, 返回新数组长度,改变原数组 push 方法可以传入多个参数。 const pushRes = arr.push(50) console.log(pushRes, arr)// 5 [10, 20, 30, 40, 50]
- 数组首部操作的方法** shift() **和 **unshift()**
const arr = [10, 20, 30, 40]
// shift 从前删掉一个值, 返回删除值,改变原数组 const shiftRes = arr.shift() console.log(shiftRes, arr)// 10 [20, 30, 40]
// unshift 从前追加一个值, 返回新数组长度,改变原数组 const unshiftRes = arr.unshift(5) console.log(unshiftRes, arr)// 5 [5, 10, 20, 30, 40]
- 重排序的方法 **reverse() **和 **sort()**,sort() 方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置。- 数组连接的方法 **concat()** ,返回的是拼接好的数组,不影响原数组,属于纯函数。- 数组截取/切片 **slice()**,用于截取数组中的一部分返回,不影响原数组,属于纯函数。
const arr = [10, 20, 30, 40, 50]
//slice 纯函数 const arr1 = arr.slice() // 类似于做了个一层深拷贝,多层的深层浅拷贝 const arr2 = arr.slice(1, 4) // [20, 30, 40] (startIndex包含,endIndex不包含) const arr3 = arr.slice(2) // [30, 40, 50] const arr4 = arr.slice(-2) // [40, 50] 最后两个
- 数组插入/剪接 **splice()**
const arr = [10, 20, 30, 40, 50]
//splice 非纯函数 const slpiceRes = arr.plice(1, 2, ‘a’, ‘b’, ‘c’) //把index为1的地方剪切长度为2的数组换成’a’, ‘b’, ‘c’,返回剪掉的内容 console.log(slpiceRes, arr)//[20, 30][10, ‘a’, ‘b’, ‘c’, 40, 50]
const slpiceRes1 = arr.splice(1, 2) // 不粘贴直接剪切对应index元素 console.log(slpiceRes1, arr) // [20, 30] [10, 40, 50]
const slpiceRes2 = arr.splice(1, 0, ‘a’, ‘b’, ‘c’) // 不剪切元素直接粘贴 console.log(slpiceRes2, arr) // [] [10, ‘a’, ‘b’, ‘c’, 20, 30, 40, 50]
- 影响原数组查找特定项的索引的方法**indexOf() **和** lastIndexOf()**- 迭代方法 **every()、some()、filter()、map() **和** forEach()** 方法- 数组归并方法 **reduce() **和 **reduceRight() **方法
arr.reduce(callback,[initialValue])
//reduce 为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素 var arr = [1, 2, 3, 4]; var sum = arr.reduce((x,y)=>x+y) var mul = arr.reduce((x,y)=>x*y) console.log( sum ); //求和,10 console.log( mul ); //求乘积,24
<a name="lStb4"></a>#### 1.数组方法splice和slice区别| slice | splice || --- | --- || 两个参数分别是起始和结束索引 | 第一个参数是起始位置,第二个参数是删除几个,后面是插入的元素,可以放多个 || 起始闭区间,结束开区间 | || 第一个参数是-1的话代表截取最后一个元素,-2代表截取倒数第二个参数;第二个参数可以不传, | || 不会改变原数组 | 会改变原数组 |<a name="eKH8a"></a>#### 2.sort实现原理V8 引擎 sort 函数只给出了两种排序分别是:根据传入的数组大小判断使用 数组长度小于等于 10 的用插入排序,比10大的数组则使用快速排序(log N 排序)<br />如果还要按引擎区分的话 v8就是上面说的这个,ff用的归并排序,webkit用的C++库的排序<a name="XtElJ"></a>### (二)数组的遍历/迭代方法有哪些?| **方法** | **是否改变原数组** | **特点** || --- | --- | --- || forEach() | 否 | 数组方法,不改变原数组,没有返回值 || map() | 否 | 数组方法,不改变原数组,有返回值,可链式调用 || filter() | 否 | 数组方法,**过滤数组**,返回包含符合条件的元素的数组,可链式调用 || for...of | 否 | for...of遍历具有Iterator迭代器的对象的属性,返回的是数组的元素、对象的属性值,**不能遍历普通的obj对象**,将异步循环变成同步循环,不能遍历原型属性<br />想遍历普通对象的话,需要给这个普通对象加一个迭代器就可以遍历了,异步遍历 || for...in | 用于遍历对象的 | for...in遍历对象的话遍历的是key值,遍历数组的话遍历的是索引,同步遍历,可以遍历出可读原型属性; || every() 和 some() | 否 | 数组方法,some()只要有一个是true,便返回true;而every()只要有一个是false,便返回false. || find() 和 findIndex() | 否 | 数组方法,find()返回的是**第一个符合条件的值**;findIndex()返回的是**第一个返回条件的值的索引值** || reduce() 和 reduceRight() | 否 | 数组方法,reduce()对数组**正序操作,**为数组中的每一个元素**依次执行回调函数**;reduceRight()对数组逆序操作 |遍历方法的详细解释:[《细数JavaScript中那些遍历和循环》](https://cuggz.blog.csdn.net/article/details/107649549)<a name="mpMfn"></a>### (三)是否为纯函数纯函数:不对外界产生副作用的函数<br />非纯函数:push pop shift unshift sort reverse splice
const arr = [10, 20, 30, 40]
// concat 数组拼接数组,返回全新的数组,不改变原数组 const arr1 = arr.concat([50, 60, 70]) console.log(arr, arr1) // [10, 20, 30, 40] [10, 20, 30, 40, 50, 60, 70]
// map 数组拼接数组,返回全新的数组,不改变原数组 const arr2 = arr.map(num => num*10) console.log(arr, arr1)
// filter 返回符合条件的数组 const arr3 = arr.filter(num => num > 25)
// slice const arr4 = arr.slice() // 类似于做了个深拷贝
<a name="v1MPe"></a>## 10.字符串的方法- 字符串转成数组:split():
‘1-2-3’.split(‘-‘) // [1,2,3]
<a name="EC987"></a>### 字符串substring()和substr()的区别都是用于截取字符串;str.substr(start, length),一个传入的是起点和截取长度;str.substring(startIndex,endIndex)传入的是起点和下标index,起始是闭区间末尾是开区间,'Mozilla'.substring(2,5)是"zil"<a name="Vdm3s"></a>## 11.对象的方法<a name="Fj2d2"></a>### (一)Object.keys<br />第一个:是个0123的课迭代对象,数组和对象一样的,key从0开始<a name="iympf"></a>### (二)Object.setPrototypeOf和Object.create都是用来关联对象,前者是ES6,后者是ES5,后者性能上较低;一个创造对象,一个修改原型<br />其实本质区别就在于 对象的原型被覆盖还是增加指向<br />[https://juejin.cn/post/6844903527941144589](https://juejin.cn/post/6844903527941144589)<br /><br /><a name="aZ9BN"></a>### (三)Object.assign()<a name="l4pSy"></a>### (四)Object.defineProperty<a name="cyx4F"></a>## 12. js遍历对象的方法1. for…in ES5的标准,原理是Object.keys();它们会先提取所有 key 的 parseFloat 值为非负整数的属性,然后根据数字顺序对属性排序首先遍历出来,然后按照对象定义的顺序遍历余下的所有属性。1. for...of ES6的标准, 把对象转成可遍历的对象才能用这个方法遍历1. Object.keys() 可以遍历对象的键 Object.values()可以遍历对象的值1. Object.getOwnPropertyNames() 返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。<br />[https://blog.csdn.net/weixin_39714113/article/details/110611246](https://blog.csdn.net/weixin_39714113/article/details/110611246)<a name="RFKci"></a>## 12. Unicode、UTF-8、UTF-16、UTF-32的区别?(了解即可)(先只看总结吧~~~)<a name="ihIMB"></a>### (1)Unicode在说Unicode之前需要先了解一下**ASCI**I码:ASCII 码(American Standard Code for Information Interchange)称为**美国标准信息交换码**。- 它是基于拉丁字母的一套电脑编码系统。- 它定义了一个用于代表常见字符的字典。- 它包含了"A-Z"(包含大小写),数据"0-9" 以及一些常见的符号。- 它是专门为英语而设计的,有128个编码,对其他语言无能为力ASCII码可以表示的编码有限,要想表示其他语言的编码,还是要使用Unicode来表示,可以说Unicode是ASCII 的超集。<br />**Unicode**全称 Unicode Translation Format,又叫做**统一码、万国码、单一码**。Unicode 是**<br />Unicode的**实现方式(也就是编码方式)有很多种,常见的是UTF-8、UTF-16、UTF-32和USC-2。**<a name="jVTLk"></a>### (2)UTF-8UTF-8是使用最广泛的Unicode编码方式,它是一种可变长的编码方式,可以是1—4个字节不等,它可以完全兼容ASCII码的128个字符。<br />注意: UTF-8 是一种编码方式,Unicode是一个字符集合。<br />UTF-8的编码规则:- 对于单字节的符号,字节的第一位为0,后面的7位为这个字符的Unicode编码,因此对于英文字母,它的Unicode编码和ACSII编码一样。- 对于n字节的符号,第一个字节的前n位都是1,第n+1位设为0,后面字节的前两位一律设为10,剩下的没有提及的二进制位,全部为这个符号的Unicode码 。来看一下具体的Unicode编号范围与对应的UTF-8二进制格式 :| 编码范围(编号对应的十进制数) | 二进制格式 || --- | --- || 0x00—0x7F (0-127) | 0xxxxxxx || 0x80—0x7FF (128-2047) | 110xxxxx 10xxxxxx || 0x800—0xFFFF (2048-65535) | 1110xxxx 10xxxxxx 10xxxxxx || 0x10000—0x10FFFF (65536以上) | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |那该如何通过具体的Unicode编码,进行具体的UTF-8编码呢?步骤如下:- 找到该Unicode编码的所在的编号范围,进而找到与之对应的二进制格式- 将Unicode编码转换为二进制数(去掉最高位的0)- 将二进制数从右往左一次填入二进制格式的X中,如果有X未填,就设为0来看一个实际的例子:<br />“马” 字的Unicode编码是:0x9A6C,整数编号是39532<br />(1)首选确定了该字符在第三个范围内,它的格式是 1110xxxx 10xxxxxx 10xxxxxx<br />(2)39532对应的二进制数为1001 1010 0110 1100<br />(3)将二进制数填入X中,结果是:11101001 10101001 10101100<a name="zfVOk"></a>### (3)UTF-161. 平面的概念<br />在了解UTF-16之前,先看一下平面的概念:<br />Unicode编码中有很多很多的字符,它并不是一次性定义的,而是分区进行定义的,每个区存放65536(216)个字符,这称为一个平面,目前总共有17 个平面。<br />最前面的一个平面称为基本平面,它的码点从0 — 216-1,写成16进制就是U+0000 — U+FFFF,那剩下的16个平面就是辅助平面,码点范围是 U+10000—U+10FFFF。<br />2. UTF-16 概念:<br />UTF-16也是Unicode编码集的一种编码形式,把Unicode字符集的抽象码位映射为16位长的整数(即码元)的序列,用于数据存储或传递。Unicode字符的码位需要1个或者2个16位长的码元来表示,因此UTF-16也是用变长字节表示的。<br />3. UTF-16 编码规则:- 编号在 U+0000—U+FFFF 的字符(常用字符集),直接用两个字节表示。- 编号在 U+10000—U+10FFFF 之间的字符,需要用四个字节表示。4. 编码识别<br />那么问题来了,当遇到两个字节时,怎么知道是把它当做一个字符还是和后面的两个字节一起当做一个字符呢?<br />UTF-16 编码肯定也考虑到了这个问题,在基本平面内,从 U+D800 — U+DFFF 是一个空段,也就是说这个区间的码点不对应任何的字符,因此这些空段就可以用来映射辅助平面的字符。<br />辅助平面共有 2****20 个字符位,因此表示这些字符至少需要 20 个二进制位。UTF-16 将这 20 个二进制位分成两半,前 10 位映射在 U+D800 — U+DBFF,称为高位(H),后 10 位映射在 U+DC00 — U+DFFF,称为低位(L)。这就相当于,将一个辅助平面的字符拆成了两个基本平面的字符来表示。<br />因此,当遇到两个字节时,发现它的码点在 U+D800 —U+DBFF之间,就可以知道,它后面的两个字节的码点应该在 U+DC00 — U+DFFF 之间,这四个字节必须放在一起进行解读。<br />5. 举例说明<br />以 "𡠀" 字为例,它的 Unicode 码点为 0x21800,该码点超出了基本平面的范围,因此需要用四个字节来表示,步骤如下:- 首先计算超出部分的结果:0x21800 - 0x10000- 将上面的计算结果转为20位的二进制数,不足20位就在前面补0,结果为:0001000110 0000000000- 将得到的两个10位二进制数分别对应到两个区间中- U+D800 对应的二进制数为 1101100000000000, 将0001000110填充在它的后10 个二进制位,得到 1101100001000110,转成 16 进制数为 0xD846。同理,低位为 0xDC00,所以这个字的UTF-16 编码为 0xD846 0xDC00<a name="auc2E"></a>### (4) UTF-32UTF-32 就是字符所对应编号的整数二进制形式,每个字符占四个字节,这个是直接进行转换的。该编码方式占用的储存空间较多,所以使用较少。<br />比如“马” 字的Unicode编号是:U+9A6C,整数编号是39532,直接转化为二进制:1001 1010 0110 1100,这就是它的UTF-32编码。<a name="CLTUy"></a>### (5)总结Unicode、UTF-8、UTF-16、UTF-32有什么区别?- Unicode 是**编码字符集(字符集)**,而**UTF-8、UTF-16、UTF-32是字符集编码**(**编码****规则**);- UTF-16 **使用变长码元序列的编码方式**,相较于定长码元序列的UTF-32算法更复杂,甚至比同样是变长码元序列的UTF-8也更为复杂,因为其引入了独特的代理对这样的代理机制;而UTF-16不会判断开头标志,即使错也只会错一个字符,所以**容错能力较强**- UTF-8需要判断每个字节中的开头标志信息,所以如果某个字节在传送过程中出错了,就会导致后面的字节也会解析出错,所以**容错能力较弱**- 如果字符内容**全部英文或英文与其他文字混合,但英文占绝大部分**,那么**用UTF-8**就比UTF-16节省了很多空间;而如果字符内容**全部是中文这样类似的字符或者混合字符中中文占绝大多数**,那么**UTF-16**就占优势了,可以节省很多空间;<a name="EYyoD"></a>## 13. 常见的位运算符有哪些?其计算规则是什么?(了解即可)我们一般的数字是十进制,我们计算机中一般需要把十进制转成二进制进行存储数据:二进制和十进制的转换如下:<br />现代计算机中数据都是以二进制的形式存储的,即0、1两种状态,**计算机对二进制数据进行的运算加减乘除等都是叫位运算**,即将符号位共同参与运算的运算。<br />常见的位运算有以下几种:| 运算符 | 描述 | 运算规则 || --- | --- | --- || & | 与 | 两个位都为1时,结果才为1 || ` | ` | 或 || ^ | 异或 | 两个位相同为0,相异为1 || ~ | 取反 | 0变1,1变0 || << | 左移 | 各二进制位全部左移若干位,高位丢弃,低位补0 || >> | 右移 | 各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃 |<a name="KuBKZ"></a>#### 1. 按位与运算符(&)定义: 参加运算的两个数据按二进制位进行“与”运算。<br />运算规则:
0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1
总结:**两位同时为1,结果才为1,否则结果为0**。<br />例如:3&5 即:
0000 0011 0000 0101 = 0000 0001
因此 3&5 的值为1。<br />注意:负数按补码形式参加按位与运算。<br />用途:<br />(1)判断奇偶<br />只要根据最未位是0还是1来决定,为0就是偶数,为1就是奇数。因此可以用if ((i & 1) == 0)代替if (i % 2 == 0)来判断a是不是偶数。<br />(2)清零<br />如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都为零的数值相与,结果为零。<a name="peN79"></a>#### 2. 按位或运算符(|)定义: 参加运算的两个对象按二进制位进行“或”运算。<br />运算规则:
0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1
总结:参加运算**的两个对象只要有一个为1,其值为1。**<br />例如:3|5即:
0000 0011 0000 0101 = 0000 0111
因此,3|5的值为7。<br />注意:负数按补码形式参加按位或运算。<a name="yAynD"></a>#### 3. 异或运算符(^)定义: 参加运算的两个数据按二进制位进行“异或”运算。<br />运算规则:
0 ^ 0 = 0
0 ^ 1 = 1
1 ^ 0 = 1
1 ^ 1 = 0
总结:参加运算的两个对象,如果**两个相应位相同为0,相异为1**。<br />例如:3|5即:
0000 0011 0000 0101 = 0000 0110
因此,3^5的值为6。<br />异或运算的性质:- 交换律:(a^b)^c == a^(b^c)- 结合律:(a + b)^c == a^b + b^c- 对于任何数x,都有 x^x=0,x^0=x- 自反性: a^b^b=a^0=a;<a name="LhVul"></a>#### 4. 取反运算符 (~)定义: 参加运算的一个数据按二进制进行“取反”运算。<br />运算规则:
~ 1 = 0 ~ 0 = 1
总结:对一个二进制数按位取反,即**将0变1,1变0**。<br />例如:~6 即:
0000 0110 = 1111 1001
在计算机中,正数用原码表示,负数使用补码存储,首先看最高位,最高位1表示负数,0表示正数。此计算机二进制码为负数,最高位为符号位。<br />当发现按位取反为负数时,就直接取其补码,变为十进制:
0000 0110 = 1111 1001 反码:1000 0110 补码:1000 0111
因此,~6的值为-7。<a name="a0x4L"></a>#### 5. 左移运算符(<<)定义: 将一个运算对象的各二进制位全部左移若干位,左边的二进制位丢弃,右边补0。<br />设 a=1010 1110,a = a<< 2 将a的二进制位左移2位、右补0,即得a=1011 1000。(**原值最左端的10丢弃,得101110,然后再补两个0**)<br />**若左移时舍弃的高位不包含1**,则每左移一位,相当于该数乘以2。(**移动一位相当于乘2的1次方,移动2位相当于乘2的2次方**)**左移后,计算出十进制再对应的2的几次方**<br /><a name="SvcnH"></a>#### 6. 右移运算符(>>)定义: 将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。<br />例如:a=a>>2 将a的二进制位右移2位,左补0 或者 左补1得看被移数是正还是负。<br />操作数每右移一位,相当于该数除以2。<a name="h7Siz"></a>#### 7. 原码、补码、反码上面提到了补码、反码等知识,这里就补充一下。<br />计算机中的**有符号数**有**三种表示方法**,即**原码、反码和补码**。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同。<br />(1)原码<br />原码就是一个数的二进制数。<br />例如:10的原码为0000 1010<br />(2)反码- 正数的反码与原码相同,如:10 反码为 0000 1010- 负数的反码为除符号位,按位取反,**即0变1,1变0**。例如:-10
原码:1000 1010 反码:1111 0101
(3)补码- **正数的补码与原码相同**,如:10 补码为 0000 1010- **负数的补码是**原码除符号位外的所有位取反即0变1,1变0,然后加1,也就是**反码加1**。例如:-10
原码:1000 1010 反码:1111 0101 补码:1111 0110
<a name="LGOaV"></a>## 14. 什么是 DOM 和 BOM?javascript 有三部分构成,ECMAScript,DOM和BOM<a name="HZX9B"></a>### (一)BOM**BOM**是**浏览器对象模型,**把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的方法和接口。是浏览器本身的一些信息的设置和获取,例如获取浏览器的宽度、高度,设置让浏览器跳转到哪个地址。<br />各个浏览器厂商根据 DOM在各自浏览器上的实现<br />javacsript是通过访问**BOM**对象来访问、控制、修改客户端(浏览器)<br />BOM的核心是 window,而 window 对象具有**双重角色**,它既是通过** js 访问浏览器窗口**的一个接口,又是一个 **Global(全局)对象**。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。window 对象含有 location 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对象的子对象。- navigator- screen- location- history- document
var ua = navigator.userAgent //获取浏览器特性(即俗称的 UA )通常ua指的就是当前浏览器信息 var isChrome = ua.indexOf(‘Chrome’) console.log(isChrome)//ua.indexOf(‘Chrome’)这种方式也不一定能很准确很严格的判断是不是我们的目标浏览器
// 获取屏幕的宽度和高度 console.log(screen.width) console.log(screenLeft,screenRight,innerHeight) console.log(screen.height)
// 获取网址、协议、path、参数、hash 等 // location // https://codeing.imooc.com/class/name/133.html?a=100&b=200#Anchor
console.log(location.href) //网址 ‘https://codeing.imooc.com/class/name/133.html?a=100&b=200#Anchor‘ console.log(location.protocol) //协议 ‘https’ console.log(location.pathname) //浏览器路径 ‘/class/name/133.html’ console.log(location.search) //查询参数 ‘?a=100&b=200’ console.log(location.hash) //哈希值 #后面的内容 ‘#Anchor’ console.log(location.host) // host ‘codeing.imooc.com’ location.reload
// 调用浏览器的前进、后退功能等 history.back() history.forward() history.go()
<a name="vCWiL"></a>### (二)DOM(Document Object Model)<a name="T5Lmi"></a>#### (1)DOM结构<br />xml是一个可扩展的语言,就像是一棵树一样;可以自己定义标签,可以任意扩展,类似于json<br />json是后面发展起来的,json和js的数据结构相吻合,所以后来发展了起来<br /><br />html其实也是一种特定的xml,结构和xml是一样的;只不过它规定了标签的名称,标签不能随意扩展<br /><br />所以DOM的本质是一棵树。html是一个文件,是一个树形结构,但放到给浏览器后,浏览器解析它成为一个树的结构,它是一层层的树,DOM结构:从html文件夹html语言解析出来的一棵树,是一层一层的关系;<a name="N5Bqr"></a>#### (1)DOM的定义DOM 指的是**文档对象模型**,它指的是把文档当做一个对象,这个对象主要定义了处理网页内容的方法和接口。DOM 是 W3C 的标准,服务器把 HTML发送给浏览器,浏览器需要把 HTML 转变成 DOM才能识别,HTML 是一棵树,DOM 也是一棵树。对 DOM的理解,可以暂时先抛开浏览器的内部因素,先从 JS 着手,即可以认为 DOM 就是 JS 能识别的 HTML结构,一个普通的 JS 对象或者数组。<a name="qImuP"></a>#### (2)DOM 的API(1)DOM 节点的获取<br />DOM 节点的获取的API及使用:
getElementById // 按照 id 查询 getElementsByTagName // 按照标签名查询 getElementsByClassName // 按照类名查询 querySelectorAll // 按照 css 选择器查询
// 按照 id 查询 var imooc = document.getElementById(‘imooc’) // 查询到 id 为 imooc 的元素 // 按照标签名查询 var pList = document.getElementsByTagName(‘p’) // 查询到标签为 p 的集合 console.log(divList.length) console.log(divList[0]) // 按照类名查询 var moocList = document.getElementsByClassName(‘mooc’) // 查询到类名为 mooc 的集合 // 按照 css 选择器查询 var pList = document.querySelectorAll(‘.mooc’) // 查询到类名为 mooc 的集合
```childNodesparentNode// 获取子节点const div1 = document.getElementById('div1')const child = div1.childNodes //得到的是一个数组console.log(child)// NodeList[6] [0:text, 1:text; 2:p; 3:p; 4:text; 5:p] 里面打印出来的不仅仅有我们写的标签,还有文本div1.childNodes[0].nodeName // '#text'div1.childNodes[0].nodeType // 3div1.childNodes[3].nodeName // 'p'div1.childNodes[3].nodeType // 1 普通的node节点是1// 所以我们可以通过nodeName和nodeType来判断是不是普通的p标签 我们可以这样写const div1ChildNodesP = Array.prototype.slice.call(div.childNodes).filter( child =>{if (child.nodeType === 1) { // child.nodeType === 1 的是正常标签return true}return false})console.log(div1ChildNodesP,'div1ChildNodesP')// 打印出3个p标签// 获取父元素const div1 = document.getElementById('div1')const parent = div1.parentNode
(2)DOM 节点的创建 createElement() appendChild()
创建一个新节点,并把它添加到指定节点的后面。已知的 HTML 结构如下:
<html><head><title>DEMO</title></head><body><div id="container"><h1 id="title">我是标题</h1></div></body></html>// 首先获取父节点var container = document.getElementById('container')// 创建新节点var targetSpan = document.createElement('span')// 设置 span 节点的内容targetSpan.innerHTML = 'hello world'// 把新创建的元素塞进父节点里去container.appendChild(targetSpan)
(3)DOM 节点的删除 removeChild()
<html><head><title>DEMO</title></head><body><div id="container"><h1 id="title">我是标题</h1></div></body></html>// 获取目标元素的父元素var container = document.getElementById('container')// 获取目标元素var targetNode = document.getElementById('title') // 获取目标元素var targetNode1 = container.childNodes[1] // 获取目标元素// 删除目标元素container.removeChild(targetNode)
(4)修改 DOM 元素 insertBefore() 或者 appendChild()
修改 DOM 元素这个动作可以分很多维度,比如说移动 DOM 元素的位置,修改 DOM 元素的属性等。
将指定的两个 DOM 元素交换位置,已知的 HTML 结构如下:
<html><head><title>DEMO</title></head><body><div id="container"><h1 id="title">我是标题</h1><p id="content">我是内容</p></div></body></html>// 现在需要调换 title 和 content 的位置,可以考虑 insertBefore 或者 appendChild:// 获取父元素var container = document.getElementById('container')// 获取两个需要被交换的元素var title = document.getElementById('title')var content = document.getElementById('content')// 交换两个元素,把 content 置于 title 前面container.insertBefore(content, title)
(3)DOM树操作
// 新增节点var div1 = document.getElementById('div1')// 添加新节点var p1 = document.createElement('p')p1.innerHTML = 'this is p1'div1.appendChild(p1) // 添加新创建的元素// 移动已有节点。注意,这里是“移动”,并不是拷贝var p2 = document.getElementById('p2')div1.appendChild(p2)// 获取父元素var div1 = document.getElementById('div1')var parent = div1.parentElement// 获取子元素var div1 = document.getElementById('div1')var child = div1.childNodes// 删除节点var div1 = document.getElementById('div1')var child = div1.childNodesdiv1.removeChild(child[0])
(三)property和attribute
两个都代表属性,一个是js属性,一个是html中的属性
(1)property
DOM 节点就是一个 JS 对象,它符合之前讲述的对象的特征 —— 可扩展属性
都是 JS 范畴的属性,符合 JS 语法标准的。
// 获取dom元素,通过js变量的方法方式去操作内部属性--用js变量操作的一种形式// 这种方式对标签不会产生什么影响const pList = document.querySelectorAll('p')const p = pList[0]console.log(p.style.width) // 获取样式p.style.width = '100px' // 修改样式p.className = 'p1' // 修改classconsole.log(p.style.width)console.log(p.className) // 获取classconsole.log(p.nodeName) // 获取nodeNameconsole.log(p.nodeType) // 获取nodeType
(2)attribute
attribute 是直接改变 HTML 的属性
get 和 set attribute 时,还会触发 DOM 的查询或者重绘、重排,频繁操作会影响页面性能
// 通过getAttribute,setAttribute 这样一个方法去操作// 这种方式修改的是一个标签的属性,能作用到节点属性的const pList = document.querySelectorAll('p')const p = pList[0]p.getAttribute('data-name')p.setAttribute('data-name','imooc') // 设置p.getAttribute('style')p.setAttribute('style','font-size:50px') // 设置

(3) 两者区别
- property:修改对象属性,不会提现到html结构中(可能会引起DOM重新渲染)
- attribute: 修改html属性,会改变html结构(肯定会引起DOM重新渲染,比较耗性能)
- 两者都可能会引起DOM重新渲染
(四)DOM的性能优化
- DOM操作非常“昂贵”,避免频繁的DOM操作,它占CPU比较多,可能会导致浏览器的重绘和重排,重新渲染,这些工作可能会比较耗时一些耗费CPU计算会比较多一些;如果频繁操作的话可能会导致卡顿的问题
- 对DOM 查询做缓存(查了之后存起来,之后再查一样的话就不用再查了)
- 将频繁操作改为一次性操作
对dom查询做缓存
对dom的频繁操作用createDocumentFragment一次性操作
(五)mouseover和mouseenter的区别
二者的本质区别:两个区别就是冒泡和不冒泡的区别,两者内部没有子元素的时候,两者行为一样的;我们在做一些样式的时候,可以根据我们的需求去选择用哪个方法
| mouseenter | mouseover |
|---|---|
| 只有当鼠标移入到当前元素本身,不包含子元素的时候它会触发,也就是说mouseover有个冒泡的过程, mouseenter没有冒泡过程; | 当鼠标移入到自身的元素或者子元素的时候都可以触发父级事件,(想要阻止mouseover的冒泡事件就用mouseenter) |
共同点:当二者都没有子元素时,二者的行为是一致的,但是二者内部都包含子元素时,行为就不同了.
addEventListener默认是冒泡还是捕获呢?默认是冒泡
(六)js中自定义事件的使用与触发
var event = new Event('build');// Listen for the event.elem.addEventListener('build', function (e) { ... }, false);// Dispatch the event.elem.dispatchEvent(event);
15. escape、encodeURI、encodeURIComponent 的区别
JavaScript中有三个可以对字符串编码的函数,分别是: escape,encodeURI,encodeURIComponent
这些方法不会对 ASCII 字母和数字进行编码,也不会对这些 ASCII 标点符号进行编码: - _ . ! ~ ‘ ( ) 。
相应3个解码函数:unescape,decodeURI,decodeURIComponent。
1 escape()函数
escape(string) 函数可对所有字符串进行编码,这样就可以在所有的计算机上读取该字符串。
除了一些 ASCII 字母和数字 其他所有的字符都会被转义序列替换。此方法已被废弃,请勿在生产环境使用此方法。
2 encodeURI()函数encodeURI(string) 函数将字符串作为 URI 进行编码,将 URI 中的非法字符转换为合法字符
3 encodeURIComponent() 函数
encodeURIComponent() 函数对 URI 的组成部分*进行转义,一些特殊字符也会得到转义。
- escape 和 encodeURI 的作用相同,不过它们对于 unicode 编码为 0xff 之外字符的时候会有区别,escape 是直接在字符的 unicode 编码前加上 %u,而 encodeURI 首先会将字符转换为 UTF-8 的格式,再在每个字节前加上 %。
- encodeURIComponent() 函数 与 encodeURI() 函数的区别之处,前者假定它的参数是 URI 的一部分(比如协议、主机名、路径或查询字符串)。因此 encodeURIComponent() 函数将转义用于分隔 URI 各个部分的标点符号。
16. AJAX
AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。
(一)手写ajax
创建AJAX请求的步骤:
- 创建一个 XMLHttpRequest 对象。
- 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
- 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
- 当对象的属性和监听函数设置完成后,最后调用 sent 方法来向服务器发起请求,可以传入参数作为发送的数据体。 ``` // GET const SERVER_URL = “/server”; let xhr = new XMLHttpRequest(); xhr.open(“GET”,‘/data/api’, true);// 创建 Http 请求 true代表异步请求 false同步请求
xhr.onreadystatechange = function() { // 设置状态监听函数 if (this.readyState !== 4) return; // 当请求成功时 if (this.status === 200) { handle(this.response); } else { console.error(this.statusText); } }; // 设置请求失败时的监听函数 xhr.onerror = function() { console.error(this.statusText); }; // 设置请求头信息 xhr.responseType = “json”; xhr.setRequestHeader(“Accept”, “application/json”); // 发送 Http 请求,get请求发送的数据内容为空 xhr.send(null);
// POST const xhr = new XMLHttpRequest() xhr.open(‘POST’,’/login’,false)// true代表异步请求 false同步请求 xhr.onreadystatechange = function(){ if(xhr.readyState === 4 ){ // 必须xhr.readyState === 4,才能拿到status if (xhr.status === 200) { alert(xhr.responseText) } else if (xhr.status === 404) { // 如果请求路径写错了 console.log(‘404 not found’) } } } const postData = {….} xhr.send(JSON.stringfy(postData))
使**用Promise封装AJAX**:
// 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; }
// 使用: const url = ‘data/test.json’ getJSON(url).then(res=>{ console.log(res) }).catch(err => { console.error(err) })
<a name="a0jE4"></a>###<a name="CRrVs"></a>### (二)http状态码
// 状态码说明 xhr.readyState 的状态码说明: 0 -代理被创建,但尚未调用 open() 方法。 1 - open() 方法已经被调用。 2 - send() 方法已经被调用,并且头部和状态已经可获得。 3 -下载中, responseText 属性已经包含部分数据。 4 -下载操作已完成
xhr.status 即 HTTP 状态码,有 2xx 3xx 4xx 5xx 这几种,比较常用的有以下几种: 200 正常 3xx 301 永久重定向。如 http://xxx.com 这个 GET 请求(最后没有 / ),就会被 301 到 http://xxx.com/ (最后是 / ) 302 临时重定向。临时的,不是永久的 304 命中协商缓存。如发送 GET 请求时,head 中有 If-Modified-Since: xxx (要求返回更新时间是 xxx 时间之后的资源),如果此时服务器 端资源未更新,则会返回 304 ,即不符合要求 404 找不到资源 5xx 服务器端出错了
<a name="Lsr2b"></a>###<a name="wJ8Gk"></a>### (三)ajax工具(1)jq的ajax<br />(2)fetch<br />fetch是一个新的API,但是浏览器并没有做到100%的兼容;但是很简洁;内部已经封装好了一个promise,返回的是一个promise<br /><br /><br />fetch() 返回的 Promise 不会被标记为 reject, 即使响应的 HTTP 状态码是 404 或 500也是resolve,仅当网络故障时或请求被阻止时,才会标记为 reject。这点调试起来也蛮麻烦的<br />fetch 不会发送 cookies。除非你做了设置。(3)axios<br />支持**浏览器 **也支持**node.js**<br />支持**XMLHttpRequests**和**Promise**<br /><a name="RwXl5"></a>####<a name="z0QcF"></a>#### axios拦截器怎么配?
// 添加请求拦截器 axios.interceptors.request.use(function (config) { // 在发送请求之前做些什么 return config; }, function (error) { // 对请求错误做些什么 return Promise.reject(error); }); // 添加响应拦截器 axios.interceptors.response.use(function (response) { // 对响应数据做点什么 return response; }, function (error) { // 对响应错误做点什么 return Promise.reject(error); });
<a name="X946i"></a>## 17. JavaScript的变量提升<a name="ihuRj"></a>### (1)变量提升的现象?变量提升的表现是,无论在函数中何处位置声明的变量,好像都**被提升到了函数的首部,可以在变量声明前访问到而不会报错。**<a name="rxnGm"></a>### (2)变量提升的原因?造成变量声明提升的**本质原因**是 **js 引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象**。当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。首先要知道,**JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行**。- 在解析阶段,JS会检查语法,并对函数进行预编译。解析的时候会**先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,var的变量先赋值为undefined,函数先声明好可使用。在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。**- 全局上下文:变量定义,函数声明- 函数上下文:变量定义,函数声明,this,arguments- 在执行阶段,就是按照代码的顺序**依次执行**。那为什么会进行变量提升呢?主要有以下**两个原因**:- 提高**性能**- **容错性**更好(1)提高性能<br />在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。<br />在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处就是**每次执行函数时都可以直接为该函数分配栈空间**(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),**并且因为代码压缩的原因,代码执行也更快了**。<br />(2)容错性更好<br />变量提升可以在一定程度上提高JS的容错性,看下面的代码:
a = 1; var a; console.log(a);
如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。<br />虽然,在可以开发过程中,可以完全避免这样写,但是有时代码很复杂的时候。可能因为疏忽而先使用后定义了,这样也不会影响正常使用。**由于变量提升的存在,而会正常运行。**总结:- 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间- 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行<a name="VktrZ"></a>### (3)变量提升带来的问题变量提升虽然有一些优点,但是他也会造成一定的问题,在ES6中提出了let、const来定义变量,它们就没有变量提升的机制。下面看一下变量提升可能会导致的问题:相同名字的声明,后者会意外的覆盖前者
var tmp = new Date();
function fn(){ console.log(tmp); if(false){ var tmp = ‘hello world’; } }
fn(); // undefined
在这个函数中,原本是要打印出外层的tmp变量,但是因为变量提升的问题,内层定义的tmp被提到函数内部的最顶部,相当于覆盖了外层的tmp,所以打印结果为undefined。
var tmp = ‘hello world’;
for (var i = 0; i < tmp.length; i++) { console.log(tmp[i]); // h e l l o w o r l d }
console.log(i); // 11 console.log()是异步函数的,所以会循环完后再打印
由于遍历时定义的i会变量提升成为一个全局变量,在函数结束之后不会被销毁,所以打印出来11。<a name="Uw1BS"></a>## 18. 什么是尾调用,使用尾调用有什么好处?尾调用指的是**函数的最后一步调用另一个函数**。代码执行是基于执行栈的,所以当在一个函数里调用另一个函数时,会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中。<br />使用尾调用的话,因为已经是函数的最后一步,所以这时可以不必再保留当前的执行上下文,从而**节省了内存,这就是尾调用优化**。但是** ES6 的尾调用优化只在严格模式下开启**,正常模式是无效的。<a name="qPpZE"></a>## 19. ES6模块与CommonJS模块有什么异同?| **commonJs(require/module.exports)** | **ES Module(import/export default)** || --- | --- || 1.基本引用类型:值复制,不共享<br /> 2.引用类型:浅拷贝,共享 | 只可导入,动态读取,**只存只读,不能改变其值(**类似const**)** || 运行时加载 | 静态加载,编译时执行 || 异步加载 可缓存 | 同步 不支持动态加载 || 可修改引入的值 | 不可修改外部引入的值,但可调用引入中包含的方法 || 1.检查是否有该模块的缓存<br />2.如果有使用缓存<br />3.没有则执行该模块代码并缓存 | 1检查该模块是否引入过<br />2.是则暂时该模块为{} <br />3.否,进入该模块并执行代码,不做缓存 |ES6 Module和CommonJS模块(**是 nodejs 默认模块管理方式**)的区别:- **CommonJS是对模块的浅拷⻉**,ES6 Module是对模块的引⽤,即**ES6 Module只存只读,不能改变其值**,也就是指针指向不能变,类似const;- **import**的接**⼝是read-only**(只读状态),**不能修改其变量值**。 即**不能修改其变量的指针指向,但可以改变变量内部指针指向,可以对commonJS对重新赋值**(改变指针指向),但是**对ES6 Module赋值会编译报错**。ES6 Module和CommonJS模块的共同点:- CommonJS和ES6 Module都可以对引⼊的对象进⾏赋值,即对对象内部属性的值进⾏改变。<a name="uHGeK"></a>##<a name="v8dRh"></a>## 21. use strict是什么意思 ? 使用它区别是什么?use strict 是一种 ECMAscript5 添加的(**严格模式**)运行模式,这种模式使得 Javascript 在更严格的条件下运行。设立严格模式的目的如下:- 消除 Javascript 语法的**不合理、不严谨**之处,减少怪异行为;- 消除代码运行的不安全之处,保证代码运行的**安全**;- 提高**编译器效率**,增加运行**速度**;- 为未来新版本的 Javascript 做好**铺垫**。区别:- 禁止使用 **with** 语句。- 禁止 **this** 关键字指向全局对象。- 对象不能有**重名**的属性。<a name="eTk6U"></a>## 23. 强类型语言和弱类型语言的区别| 强类型语言 | 弱类型语言 || --- | --- || **变量的使用要严格符合定义,先定义后使用** | || **一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型(**例如你有一个整数,如果不显式地进行转换,你不能将其视为一个字符串**)** | **变量类型可以被忽略的语言('12'和整数3进行连接得到字符串'123',在相加的时候会进行强制类型转换)** || 强类型语言在**速度**上可能略逊色于弱类型语言 | || **强类型语言带来的严谨性**可以有效地帮助避免许多错误 | |两者对比:强类型语言在**速度**上可能略逊色于弱类型语言,但是**强类型语言带来的严谨性**可以有效地帮助避免许多错误。<a name="WbKG3"></a>## 24. 解释型语言和编译型语言的区别(了解即可)(1)解释型语言<br />使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行。是代码在执行时才被解释器一行行动态翻译和执行,而不是在执行之前就完成翻译。解释型语言不需要事先编译,其直接将源代码解释成机器码并立即执行,所以只要某一平台提供了相应的解释器即可运行该程序。其特点总结如下- 解释型语言**每次运行**都需要**将源代码解释成机器码并执行,效率较低**;- 只要平台提供相应的解释器,就可以运行源代码,所以可以**方便源程序移植**;- **JavaScript、Python**等属于解释型语言。(2)编译型语言<br />使用专门的编译器,针对特定的平台,将高级语言源代码一次性的编译成可被该平台硬件执行的机器码,并包装成该平台所能识别的可执行性程序的格式。在编译型语言写的程序执行之前,需要一个专门的编译过程,把源代码编译成机器语言的文件,如exe格式的文件,**以后要再运行时,直接使用编译结果即可**,如直接运行exe文件。因为只需编译一次,以后运行时不需要编译,所以编译型语言执行效率高。其特点总结如下:- 一次性的编译成平台相关的机器语言文件,运行时**脱离开发环境,运行效率高**;- 与特定平台相关,一般**无法移植到其他平台**;- **C、C++**等属于编译型语言。**两者主要区别在于:**<br />**编译型语言**源程序编译后即可在该平台运行,**解释型语言**是在运行期间才编译。<br />所以**编译型语言**运行速度快,**解释型语言**跨平台性好。<a name="iUY8d"></a>## 25. for...in和for...of的区别for…of 是ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,和ES3中的for…in的区别如下| for… in | for…of || --- | --- || **遍历对象** | 遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象 || 获取的是**对象的键名** | 遍历**获取**的是对象的**键值** || 会遍历对象可读的**整个原型链的键,性能非常差不推荐使用** | 只遍历具有**遍历器的对象**,**不会遍历原型属性** || 数组的遍历,会返回数组中**所有**可枚举的属性(包括原型链上可枚举的属性) | 数组的遍历,只返回数组的下标对应的**属性值** || 一般用于**同步**遍历 | 一般**异步**遍历 || | **不能跳出循环** || | 对普通对象部署iterator就能转换成可迭代对象遍历了:**[Symbol.iterator]** |<a name="S2bA8"></a>## 26. 如何使用for...of遍历对象<a name="j4LU8"></a>### (一)关于迭代器 iterator当你给你的结构**部署了iterator接口**,那么恭喜你,你可以使用**for...of来遍历你的结构**了!- 我们说的数组,Map等结构中的成员都是有顺序的,即都是线性的结构,而对象,各成员并没有一个确定的顺序,所以遍历时先遍历谁后遍历谁并不确定。所以,**给一个对象部署iterator接口,其实就是对该对象做一种线性转换**。- Symbol.iterator会**返回一个对象,这就是一个遍历器对象**,而作为遍历器对象,其必须具备的特征就是必须具备next()方法。由Symbol.iterator函数生成的对象应该通过其next()方法陆续返回值,直到返回的done属性为true结束。可以通过显示地调用**next()方法返回**,也可以隐式地通过生成器函数返回;- 部署iterator有一种很简单的方法,即**直接使用数组的[Symbol.iterator]接口**- 用es6的**Generator**函数来实现Symbol.iterator接口,事半功倍。- <a name="aJ6M8"></a>### (二)关于生成器生成器是es6语法定义的**迭代器;关键字是function***,调用生成器函数并不会实际执行函数体,而是返回一个生成器对象,这个生成器对象是一个迭代器,调用它的next()方法会导致生成器函数的函数体从头开始执行,直至遇到一个yield语句,**yield**语句是es6的新特性,**类似于return语句**;生成器有一个Symbol.iterator方法,所以可以迭代;<a name="u6jUR"></a>### (三)for...of方法for…of是作为ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,普通的对象用**for...of**遍历是会报错的。for of 可以异步遍历如果需要遍历的对象是**类数组对象**,用**Array.from**转成数组即可。
var obj = { 0:’one’, 1:’two’, length: 2 }; arr = Array.from(obj); for(var k of arr){ console.log(k) }
如果**不是类数组对象**,就给**对象添加一个[Symbol.iterator]属性**,并指向一个迭代器即可。- Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。
//方法一: var obj = { a:1, b:2, c:3 };
obj[Symbol.iterator] = function(){ var keys = Object.keys(this); // [“a”, “b”, “c”] var count = 0; return { next(){ if(count<keys.length){ return {value: obj[keys[count++]],done:false}; }else{ return {value:undefined,done:true}; } } } }; // 因为把普通对象线性转换了,所以可以遍历了 for(var k of obj){ console.log(k); }
// 方法二 var obj = { a:1, b:2, c:3 }; obj[Symbol.iterator] = function*(){ var keys = Object.keys(obj); for(var k of keys){ yield [k,obj[k]] // [“a”, 1] [“b”, 2] [“c”, 3] } }; // obj: {a: 1, b: 2, c: 3, Symbol(Symbol.iterator)} for(var [k,v] of obj){ console.log(k,v); // a 1;b 2; c 3 }
- for...in (以及forEach for) 是常规的同步遍历- for...of常用于异步的遍历
// 定时算乘法 function multi (num){ return new Promise(resolve => { setTimeout(() => { resolve(num*num) }, 1000) }) }
function test1 (){ const nums = [1,2,3] // 这里用同步的循环 所以会1s后一下子打印出三个结果 nums.forEach(async x =>{ const res = await multi; conosle.log(res); }) } test1()
async function test2 (){ const nums = [1,2,3]; for (let x of nums) { // 使用 for…of ,可以让计算挨个串行执行 const res = await multi(x) console.log(res) } } test2()
<a name="TlQep"></a>## 27. ajax、axios、fetch的区别(1)AJAX<br />Ajax 即“AsynchronousJavascriptAndXML”(异步 JavaScript 和 XML),是指一种**创建交互式**[**网页**](https://link.zhihu.com/?target=https%3A//baike.baidu.com/item/%E7%BD%91%E9%A1%B5)**应用的网页开发技术。它是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术**。通过在后台与服务器进行少量数据交换,**Ajax 可以使网页实现异步更新**。这意味着可以在**不重新加载整个网页的情况下,对网页的某部分进行更新**。传统的网页(不使用 Ajax)如果需要更新内容,必须重载整个网页页面。其缺点如下:- 本身是针对MVC编程,不符合前端MVVM的浪潮- 基于原生XHR开发,XHR本身的架构不清晰- 不符合关注分离(Separation of Concerns)的原则- 配置和调用方式非常混乱,而且基于事件的异步模型不友好。(2)Fetch<br />fetch的**定义**:<br />fetch号称是**AJAX的替代品**,是在ES6出现的,**使用了ES6中的promise对象**。Fetch是基于promise设计的。Fetch的代码结构比起ajax简单多。fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。fetch的**优点**:- 语法简洁,更加语义化- 基于标准 Promise 实现,支持 async/await- 更加底层,提供的API丰富(request, response)- 脱离了XHR,是ES规范里新的实现方式fetch的**缺点**:- fetch只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。- fetch默认不会带cookie,需要添加配置项: fetch(url, {credentials: 'include'})- fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费- fetch没有办法原生监测请求的进度,而XHR可以fetch的**用法**:
fetch(‘some/api/data.json’, { method:’POST’, //请求类型 GET、POST headers:{}, // 请求的头信息,形式为 Headers 对象或 ByteString body:{}, //请求发送的数据 blob、BufferSource、FormData、URLSearchParams(get 或 head 方法中不能包含 body) mode:’’, //请求的模式,是否跨域等,如 cors、 no-cors 或 same-origin credentials:’’, //cookie 的跨域策略,如 omit、same-origin 或 include cache:’’, //请求的 cache 模式: default、no-store、reload、no-cache、 force-cache 或 only-if-cached }).then(function(response) { … });
Fetch 支持 headers 定义,通过 headers 自定义可以方便地实现多种请求方法( PUT、GET、POST
```
(3)Axios
Axios 是一种基于Promise封装的HTTP客户端,其特点如下:
- 浏览器端发起XMLHttpRequests请求
- node端发起http请求
- 支持Promise API
- 监听请求和返回
- 对请求和返回进行转化
- 取消请求
- 自动转换json数据
- 客户端支持抵御XSRF攻击
29. forEach和map方法有什么区别
这方法都是用来遍历数组的,两者区别如下:
- forEach()方法会针对每一个元素执行提供的函数,对数据的操作不会改变原数组(好像是能改变数组二层以下的数据),该方法没有返回值;
- map()方法不会改变原数组的值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值;

