一、数据类型
1、JavaScript数据类型
8种数据类型:Number、String、Boolean、Null、undefined、Symbol、Object、BigInt
Symbol和BigInt是ES6新增的数据类型:
- Symbol:创建后独一无二且不可变的数据类型,主要是为了解决可能出现的全局变量冲突的问题
- BigInt:是一种数字类型的数据。可以表示任意精度格式的整数,使用BigInt可以安全的存储和操作大整数,即使这个数已经超出了Number能够表示的安全整数范围
原始数据类型和引用数据类型
- 原始数据类型:Number、Boolean、String、Null、undefined、Symbol、BigInt
- 引用数据类型:对象、数组、函数
原始数据类型和引用数据类型的不同
- 原始数据类型存储在栈中
- 占据空间小,大小固定
- 栈中的数据,先进后出
- 引用数据类型存储在堆中
- 占据空间大,大小不固定
- 在栈中存储了指针,指针指向heap中该实体的起始地址
- 堆是一个优先队列,是按照优先级进行排序的,优先级可以按照大小来规定
在操作系统中,内存可以被分为栈区和堆区:
- 栈区的内存:由编译器自动分配释放,存放函数的局部变量值等,其操作方式类似于数据结构中的栈
- 堆区的内存:一般由开发者分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收
2、数据类型检测的方式
(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
null、对象、数组,都会被判定为object,函数是function
(2)instanceof
instanceof可以正确的判断对象的类型,其内部运行的机制是判断在其原型链中能不能找到该构造函数的原型,内部原理是obj.__proto__ === Array.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
(3)constructor
- 用来判断数据的类型
- 对象实例可以通过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
(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]
obj.toString()的结果为什么和Object.prototype.toString.call(obj)的结果不一样
- 因为Array、Function等类型作为Object的实例,都重写了toString方法,在调用toString函数时,会优先调用原型链上重写的toString方法
- Function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串
3、判断数组的方式有哪些
可以通过目录2中提供的三种方法:constructor、instanceof、Object.prototype.toString.call()
console.log([] instanceof Array); // true
console.log(([]).constructor === Array); // true
Object.prototype.toString.call(obj).slice(8,-1) === 'Array'; // true
- Array.isArrray(obj)
- Array.prototype.isPrototypeOf(obj)
instanceof和isPrototypeOf区别:
- B instanceof A:A的原型对象是否在B的原型链上(构造函数与对象的关系)
- A isPrototyoeOf B:A对象是否在B对象的原型链上(对象与对象的关系)
4、null和undefined区别
- undefined代表的含义是未定义,一般变量声明了但还没有定义的时候会返回undefined
- null代表的含义是空对象,主要用于给存储对象的变量做初始化
null == undefined null !== undefined
5、typeof null的结果为什么是object
在JavaScript的第一个版本中,所有值都存储在32位的单元中,每个单元包含一个小的类型标签(1~3bits)以及当前要存储的数据。类型标签存储在每个单元的低位中,共有5种类型:
000:object // 当前存储的数据指向一个对象
1: int // 当前存储的数据指向一个31位的有符号整数
010: double // 当前存储的数据指向一个双精度的浮点数
100: string // 当前存储的数据指向一个字符串
110: boolean // 当前存储的数据指向一个布尔值
有两种特殊的数据类型:
- undefined:值是(-2)30,(一个超出整数范围的数字)
- null:值是机器码NULL指针(null指针的值全是0)
也就是说null的类型标签也是000,和object一样,所以会被判定为Object
6、instanceof操作符的实现原理
function myInstanceof(target, origin) {
if(typeof target !== 'object' || target === null) return false;
var proto = Object.getPrototypeOf(target);
while (proto) {
if (proto === origin.prototype) {
return true
}
proto = Object.getPrototypeOf(proto);
}
return false
}
7、为什么 0.1 + 0.2 !== 0.3
let n1 = 0.1, n2 = 0.2
console.log(n1 + n2) // 0.30000000000000004
计算机是通过二进制的方式存储数据,所以在计算0.1 + 0.2时,实际上是计算两个数的二进制和
二进制没有单独的浮点型,浮点数和整数都是通过Number表示,是遵循IEEE754标准的64位双精度值,在二进制中,64位由以下三个部分组成
- sign bit(符号S),用来表示正负,0正数,1负数
- exponent(指数E),用来表示次方数,中间11位
- mantissa(尾数M),用来表示精确度,超出的部分自动舍0进1,最后52位
使用二进制表示十进制小数时,有些数字没有办法被有限的二进制小数表示,存储时使用循环来近似的表示,因此双精度浮点数是不精确的
常见的精度问题
0.1 + 0.2 != 0.3
(1.335).toFixed(2) == 1.33
大数问题61453901951867050 + 5 == 61453901951867060
// 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
8、如何获取安全的undefined值
undefined可以被当成变量来使用和赋值。我们可以使用void 0来获取undefined
9、typeof NaN的值
NaN:指的是“不是一个数字”,是一个警戒值(有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”
typeof NaN; // "number"
NaN与任何值都不相等,包括他自己
10、isNaN和Number.isNaN函数的区别
isNaN:函数接收参数后,会尝试将这个数转换为数值,任何不能被转换为数值的值都会返回true,因此非数值传入也会返回true,影响对NaN的判断
Number.isNaN:首先判断传入的参数是否是数字,如果是数字再判断是否是NaN,不会进行数据类型转换,因次对NaN的判断更为准确
11、其他值到字符串的转换规则
值 | 转换为字符串 |
---|---|
null | "null" |
undefined | "undefined" |
Boolean | "true" "false" |
Number | 直接转换,不过极大极小的数字会使用指数形式 |
Symbol | 直接转换,只允许显示强制转换 |
普通对象 | "[object Object]",如果对象重写了toString方法,字符串化时会返回该自定义toString方法的返回值 |
12、其他值到数字的转换规则
值 | 数字 |
---|---|
null | 0 |
undefined | NaN |
Boolean | 1或0 |
String | 相当于使用Number函数,非数字为NaN,空字符串为0 |
Symbol | 不能转换为数字,会报错 |
13、其他值到boolean的转换规则
- null
- undefined
- false
- +0 -0
- NaN
- ""
以上转换为false,其余皆为真值
14、|| 和 &&的返回值
|| 和 && 首先对第一个操作数进行判断,如果不是Boolean则强行转换,然后再执行条件判断
- ||:如果第一个操作数的条件判断为true,则返回第一个操作数,如果为false,返回第二个操作数
- &&:如果第一个操作数的条件判断为true,则返回第二个操作数,如果为false,返回第一个操作数
|| 和 && 的结果是操作数的值,不是对操作数条件判断的结果
15、Object.is()和比较操作符 ==, === 的区别
- ==:如果两边类型不一致,会进行强制类型转换再比较
- ===:如果两边类型不一致,不会强制类型转换,直接返回false
- Object.is():一般情况下和===的结果相同,但处理了一些特殊情况
- +0和-0不再相等
- 两个NaN是相等的
16、什么是JavaScript的包装类型
在JavaScript中,基本类型是没有属性和方法的,但是我们发现实际上可以调用,这是因为在调用属性和方法时,后台会隐式的将基本类型转换为对应的包装对象,在调用完成之后,会立即销毁对象
// 基本类型可以显式的转换为包装类型
// 数字型包装类
new Number(66.66)
// 布尔型包装类
new Boolean(true)
// 字符型包装类
new String('字符串')
// 包装类型转换为基本类型
new Boolean(true).valueOf() // true
17、js如何进行隐式类型转换
首先介绍ToPrimitive方法,这是js中每个值隐含的自带的方法,用来将值(无论是基本类型还是引用类型)转换为基本类型值
- 基本类型,直接返回值本身
- 引用类型,采用以下规则
ToPrimitive(obj,type)
// 如果对象为 Date 对象,则type默认为string,先调用对象的toString方法,如果为原始值则返回,否则调用valueOf方法,如果为原始值则返回,否则报错
// 其他对象和Date对象相反,先调用valueOf,再调用toString
JavaScript的隐式类型转换主要发生在以下运算符之间
+
操作符- 如果有String,则两个值都转换成string
- 否则都会被转换为数字
-
、*
、\
操作符- 转换为数字
- NaN也是数字
- 对于
<
和>
比较符- 如果两边都是字符串,则依次比较字母表顺序
- 其他情况下,转换为数字再比较
以上是基本数据类型,对于引用类型,会被ToPrimitive隐式转换为基本类型
var a = {}
a > 2 // false
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]"
18、+
操作符什么时候用于字符串的拼接
根据 ES5 规范,如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话,+ 将进行拼接操作。如果其中一个操作数是对象(包括数组),则首先对其调用 ToPrimitive 抽象操作,该抽象操作再调用 [[DefaultValue]],以数字作为上下文。如果不能转换为字符串,则会将其转换为数字类型来进行计算。
简单来说就是,如果 + 的其中一个操作数是字符串(或者通过以上步骤最终得到字符串),则执行字符串拼接,否则执行数字加法。
那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字。
19、为什么会有BigInt提案
JavaScript中Number.MAX_SAFE_INTEGER表示最⼤安全数字,计算结果是9007199254740991,即在这个数范围内不会出现精度丢失(⼩数除外)。但是⼀旦超过这个范围,js就会出现计算不准确的情况,这在⼤数计算的时候不得不依靠⼀些第三⽅库进⾏解决,因此官⽅提出了BigInt来解决此问题。
20、Object.assign和扩展算法是深拷贝还是浅拷贝
let outObj = {
inObj: {a: 1, b: 2}
}
let newObj = {...outObj}
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}
let outObj = {
inObj: {a: 1, b: 2}
}
let newObj = Object.assign({}, outObj)
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}
两者都是浅拷贝。
- Object.assign()方法接收的第一个参数作为目标对象,后面的所有参数作为源对象。然后把所有的源对象合并到目标对象中。它会修改了一个对象,因此会触发 ES6 setter。
- 扩展操作符(…)使用它时,数组或对象中的每一个值都会被拷贝到一个新的数组或对象中。它不复制继承的属性或类的属性,但是它会复制ES6的 symbols 属性。
二、ES6
1、let、const、var的区别
(1)块级作用域
- 块级作用域由{}组成,let 和 const 有块级作用域。
- 块级作用域解决了以下问题:内层变量覆盖外层变量
(2)变量提升
- var存在变量提升,可以在声明之前使用变量,值为undefined
- let和const不存在变量提升,声明之前使用变量会报错
(3)暂时性死区
- let和const声明的变量之前,变量是不可用的,从块级作用域的开始一直到变量声明之间,使用变量会报错,该区域称为暂时性死区。
- var不存在暂时性死区
(4)给全局变量添加属性
- 浏览器的全局变量是window,node的全局变量是global
- var声明的变量会成为全局变量的属性,let和const不会
(5)重复声明
- 在同一个作用域中,let和const不可以重复声明变量
- var变量可以重复声明,后声明的变量会覆盖之前的
(6)初始值
- let和var创建时可以不用设置初始值
- const必须设置初始值,且设置之后,值不可以被修改
2、const的值可以修改吗
不可以修改,但是对于引用类型的数据,栈内存中保存的数据的引用,不可修改引用,但是实际存储在堆内存中的数据是可以修改的。
3、如果new一个箭头函数会怎样
箭头函数是es6提出来的,没有prototype,没有this,也不可以使用arguments,所以不能new创建箭头函数
new操作符的实现步骤
- 首先创建一个空对象
- 将空对象的__proto__指向构造函数的prototype
- 执行构造函数,并将空对象的 this 绑定为构造函数的 this
- 如果构造函数有返回一个对象,则返回这个对象,否则返回新创建的那个对象
上述二三步,箭头函数没法执行
// 定义构造函数
function Person (name) {
console.log("constructor");
// 将构造函数的this指向新对象
this.name = name;
}
// 定义类的属性
Person.prototype.say = function () {
console.log("My name is", this.name);
};
function newZn (constructorFn, ...args) {
// 1.创建一个空对象
const obj = {};
// 2.将空对象的 __proto__ 指向构造函数的 prototype
obj.__proto__ = constructorFn.prototype
// 3.执行构造函数,并将空对象的 this 绑定为构造函数的 this
const res = constructor.apply(obj, args);
// 4.如果构造函数有返回一个对象,则返回这个对象,否则返回新创建的那个对象
return typeof res==='obj'?res:obj;
}
const p1 = newZn(Person, 'zhangning')
p1.say();
4、箭头函数和普通函数的区别
(1)箭头函数比普通函数更简洁
- 如果没有参数,可以直接写一个空括号
- 如果只有一个参数,可以直接省略参数的括号
- 如果有多个参数,直接使用逗号分割
- 如果函数体返回值只有一句,可以省略大括号和return
- 如果函数体没有返回值,且只有一句,可以给语句加上void,最常见的就是调用一个函数
let fn = () => void doesNotReturn();
(2)箭头函数没有自己的this,继承的this指向不会改变
- 箭头函数不会创建自己的this,只会在自己的作用域上继承上一层的this,因此箭头函数的this在函数定义时已经确认了,之后不再改变
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() // undefined
new obj.b() // Uncaught TypeError: obj.b is not a constructor
对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。
(3)call、apply、bind对箭头函数失效
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'
(4)箭头函数不能作为构造函数使用,也没有prototype 箭头函数没有this,且this指向外层的执行环境,不能改变指向,所以不能当做构造函数使用。
(5)箭头函数没有自己的arguments 在箭头函数中访问arguments实际上是它外层函数的arguments
(6)箭头函数不能用作generator函数,不能使用yeild关键字
5、扩展运算符的作用与使用场景
(1)对象扩展运算符
对象扩展运算符:用于取出对象中的所有可遍历属性,并拷贝到新对象中,属于浅拷贝,与Object.assign作用相同,扩展运算符后面的属性会覆盖前面的属性。
let bar = { a: 1, b: 2 };
let baz = { ...bar }; // { a: 1, b: 2 }
let bar = { a: 1, b: 2 };
let baz = Object.assign({}, bar); // { a: 1, b: 2 }
(2)数组扩展运算符
数组扩展运算符:将数组转换为一组用逗号分隔的参数序列,每次只能展开一层数组
// 将数组转换为函数参数序列
function add(x, y) {
return x + y;
}
const numbers = [1, 2];
add(...numbers) // 3
// 复制数组
const arr1 = [1, 2];
const arr2 = [...arr1];
// 合并数组
const arr1 = ['two', 'three'];
const arr2 = ['one', ...arr1, 'four', 'five'];
// ["one", "two", "three", "four", "five"]
// 扩展运算符与解构结合(...rest只能放在最后一位)
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest // [2, 3, 4, 5]
// 将字符串转换为真正的数组
[...'hello'] // [ "h", "e", "l", "l", "o" ]
// 任意具有Iterator接口的对象,都可以转换为真正的数组
// arguments对象
function foo() {
// 用于替换es5中的Array.prototype.slice.call(arguments)(将函数传入的参数转换为数组对象)
const args = [...arguments];
}
// 使用Math可以获取数组中特定的值
const numbers = [9, 4, 7, 1];
Math.min(...numbers); // 1
Math.max(...numbers); // 9
6、对象与数组的解构
解构:ES6提供的新的提取数据的模式,能够从对象或数组里有针对性的拿到想要的数据。
(1)数组的解构
- 在解构数组时,以元素的位置为匹配条件来提取数据
- 可以给左侧的变量数组提供空占位,以此精确提取数组数据
// a, b, c分别被赋予数组的第0, 1, 2个索引位的值
const [a, b, c] = [1, 2, 3]
// a=1, c=3
const [a,,c] = [1,2,3]
(2)对象的解构
- 在解构对象时,以属性的名称为匹配条件来提取数据
- 与对象属性所处的位置无关
const stu = {
name: 'Bob',
age: 24
}
const { name, age } = stu
7、如何解构提取高度嵌套的对象里的指定属性
const school = {
classes: {
stu: {
name: 'Bob',
age: 24,
}
}
}
const { classes: { stu: { name } }} = school
console.log(name) // 'Bob'
8、对rest参数的理解
rest:可以把一个分离的参数序列整合成一个数组
- 用于获取函数的多余参数
- 处理函数参数个数不确定的情况
function mutiple(a, ...args) {
console.log(args);
}
mutiple(1, 2, 3, 4) // [2, 3, 4]
function mutiple(...args) {
console.log(args)
}
mutiple(1, 2, 3, 4) // [1, 2, 3, 4]
9、ES6的模板字符串以及字符串处理方法
(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'
(2)字符串方法
- 存在性判定
- 以前只能使用indexOf
- es6提供了includes、startsWith、endsWith
- includes:判断字符串与子串的包含关系
- startsWith:判断字符串是否以某个/某串字符开头
- endsWith:判断字符串是否以某个/某串字符结尾
- 自动重复
- 使用 repeat 方法来使同一个字符串输出多次(被连续复制多次)
const sourceCode = 'repeat'
const repeated = sourceCode.repeat(3)
console.log(repeated) // repeatrepeatrepeat
三、JavaScript基础
1、Object和Map的区别
类型 | Object | Map |
---|---|---|
意外的键 | 对象会继承原型上的键 | 默认不包含任何键,只包含显示插入的键 |
键的类型 | 必须是String或Symbol | 任意值,包括函数、对象 |
键的顺序 | 无序 | 有序,迭代时可以以插入顺序返回键 |
Size | 手动计算 | 通过size属性获取 |
迭代 | Object.keys/values方法获取对象的相关数据数组,再迭代 | 可以直接被迭代 |
性能 | - | 在频繁增删键值对的场景下表现比Object好 |
2、map和weakmap的区别
(1)Map
实际上Map是数组的集合,每个数组元素中,依次为键和值
const map = [
["name","张三"],
["age",18],
]
Map有以下方法
- size:返回Map的成员总数
- set(key, value):设置值,无则新增,有则更新(因为返回值是Map对象,因此可以链式调用该方法)
- get(key):获取值
- has(key):判断某个键是否在当前的map对象中
- delete(key):根据键删除Map的数据(返回值为boolean)
- clear():清除所有数据(没有返回值)
Map提供的迭代器和一个遍历方法
- keys():返回键名的遍历器
- values():返回键值的遍历器
- entries():返回键值对的遍历器
- forEach():遍历Map
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对象也是键值对的集合,键必须是对象,并且键是弱引用的,值可以是任何值,不能被遍历。
具有和Map一样的set、get、has、delete方法
clear方法已弃用,可以通过创建一个空的WeakMap并替换原对象来实现清除
(3)WeakMap的设计理念
强引用:当我们在某个对象上面存放一些数据时,会形成对这个数据的引用,一旦不需要这个对象,需要手动删除各个地方对对象的引用,否则垃圾回收机制不会释放对象占用的内存。
let a = {name: "eric", age: 20}
let arr = [a, "other"]
// 当不需要时,需要手动切断引用,GC才能回收。
a = null;
arr[0] = null;
// 同理Map也是如此
弱引用:不参与垃圾回收机制,当一个对象被回收后,相关的弱引用全部消失
let a = {name: "eric", age: 20}
let wp = new WeakMap();
wp.set(a, new Array(10 * 1024 * 1024));
// 此时如果 a = null, wp里的键名对象和所对应的键值对会自动消失,不用手动删除引用
3、JavaScript的内置对象
- 值属性
- Infinity、NaN、undefined、null 字面量
- 函数属性
- 直接调用:eval、parseInt、parseFloat
- 基本对象
- Object、Function、Boolean、Symbol、Error
- 数字和日期对象
- Number、Math、Date
- 字符串
- String、RegExp
- 可以索引的集合对象
- 数组、类数组
- 使用键的集合对象
- Map、Set、WeakSet、WeakMap
- 矢量集合
- SIMD(?)
- 结构化数据
- JSON
- 控制抽象对象
- Promise、Generator
- 反射
- Reflect、Proxy
- 国际化
- Intl、Intl.collator
- WebAssembly
- 其他
- arguments
4、常用的正则表达式
// (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}$/;
5、对JSON的理解
JSON:一种基于文本的轻量级数据交换格式,可以被任何编程语言读取和传递
JSON.stringify:将JS对象转换为JSON字符串
JSON.parse:将JSON字符串转换为JS对象
6、JavaScript脚本延迟加载的方法
延时加载JS脚本,有助于提高页面的加载速度,避免JS代码的执行阻塞页面的解析
- defer
- 异步加载文档
- 文档解析完成之后执行脚本
- 脚本按照顺序执行(按照规范来说是这样,但实际上在一些浏览器中可能不是)
- async
- 异步加载
- 加载完立即执行
- 执行顺序与加载顺序无关,先下载完先执行
- 动态创建Script DOM
- 监听文档加载时间,创建Script标签来引入js脚本
- 使用setTimeout延迟加载脚本
- 脚本放在文档最底部
7、JavaScript类数组对象
类数组对象:拥有length属性和若干索引属性的对象,和数组类似,但不能调用数组方法
常见的类数组对象:arguments、DOM查询元素的放回结果
类数组转为数组的方法:
- Array.prototype.slice.call(arrayLike)
- Array.prototype.splice.call(arrayLike, 0)
- Array.prototype.concat.apply([], arrayLike);
- Array.from(arrayLike);
- [...arrayLike]
8、数组的原生方法
- 数组和字符串的转换
- toString、toLocalString、join
- 数组数据操作
- push、pop、shift、unshift
- 排序
- sort、reverse
- 查找
- indexOf、lastIndexOf、every、some、filter、map、forEach
- 截取
- slice、substr、splice
- 连接
- concat
- 归并
- reduce、reduceRight
9、Unicode、UTF-8、UTF-16、UTF-32的区别
(1)Unicode
ASCII:美国标准信息交换码
- 基于拉丁字母的一套电脑编码系统
- 定义了一个用于代表常见字符的字典
- 包含A-Z(大小写),0-9,以及一些常见符号
- 专为英语设计,存储128字符集,对其他语言无能为力
Unicode是ASCII的超集,解决了ASCII有限编码的缺点,是一个支持多国语言的字符集
Unicode的实现方式:UTF-8、UTF-16、UTF-32、USC-2
(2)区别
Unicode
是编码字符集(字符集),而UTF-8
、UTF-16
、UTF-32
是字符集编码(编码规则);UTF-16
使用变长码元序列的编码方式,相较于定长码元序列的UTF-32
算法更复杂,甚至比同样是变长码元序列的UTF-8
也更为复杂,因为其引入了独特的代理对这样的代理机制;UTF-8
需要判断每个字节中的开头标志信息,所以如果某个字节在传送过程中出错了,就会导致后面的字节也会解析出错;而UTF-16
不会判断开头标志,即使错也只会错一个字符,所以容错能力较强;- 如果字符内容全部英文或英文与其他文字混合,但英文占绝大部分,那么用
UTF-8
就比UTF-16
节省了很多空间;而如果字符内容全部是中文这样类似的字符或者混合字符中中文占绝大多数,那么UTF-16
就占优势了,可以节省很多空间;
10、常见的位运算符以及计算规则
- 与
&
:两位同时为1,结果才为1,否则结果为0- 判断奇数偶数:最后一位数 & 1 === 0表示偶数,反之为奇数
- 清零:二进制位全部置位0
- 或
|
:只要有一个为1,其值为1,否则结果为0 - 异或
^
:相同为0,相异为1 - 取反
~
- 左移运算符
<<
:左移之后,右边的丢弃,右边的补0- 若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2
- 右移运算符
>>
:右移之后,正数左补0,负数左补1,右边丢弃- 左补0 或者 左补1得看被移数是正还是负
- 操作数每右移一位,相当于该数除以2
原码、反码、补码
- 原码:二进制数
- 反码
- 正数的反码与原码相同
- 负数的反码:除符号位,按位取反
- 补码
- 正数的补码与原码相同
- 负数的补码:除符号位,按位取反,再+1(也就是反码 + 1)
11、为什么函数的arguments是类数组不是数组,如何遍历类数组
arguments
是一个对象,它的属性是从 0 开始依次递增的数字,值是对应的形参值
遍历类数组:
- 使用call、apply将数组方法应用到类数组上
- Array.prototype.forEach.call(arguments, a => console.log(a))
- 将类数组转换为数组
12、BOM和DOM
-
BOM:浏览器对象模型,主要定义了与浏览器进行交互的方法和接口
- 核心是window
- window对象包含location、navigator、screen等子对象
- window对象也包含了document对象
-
DOM:文档对象模型,把文档当成对象,主要定义处理网页内容的方法和接口
13、escape、encodeURI、encodeURIComponent
-
encodeURI:对整个URI进行转义,将URI中的非法字符转化为合法字符,所以对于一些在URI中有特殊意义的字符(元字符以及语义字符)不会转义
- URL 元字符:分号(
;
),逗号(,
),斜杠(/
),问号(?
),冒号(:
),at(@
),&
,等号(=
),加号(+
),美元符号($
),井号(#
) - 语义字符:
a-z
,A-Z
,0-9
,连词号(-
),下划线(_
),点(.
),感叹号(!
),波浪线(~
),星号(*
),单引号('
),圆括号(()
)
- URL 元字符:分号(
-
encodeURIComponent 是对 URI 的组成部分进行转义,会转码除了语义字符之外的所有字符,即元字符也会被转码
- 若整个链接被
encodeURIComponent()
转码,则该链接无法被浏览器访问,需要解码之后才可以正常访问。
- 若整个链接被
-
escape 和 encodeURI 的作用相同,不过它们对于 unicode 编码为 0xff 之外字符的时候会有区别,escape 是直接在字符的 unicode 编码前加上 %u,而 encodeURI 首先会将字符转换为 UTF-8 的格式,再在每个字节前加上 %。
对于无特殊参数的链接,都可以使用encodeURI()
进行转码,链接带着一些特殊参数的时候使用encodeURIComponent()
URI:用字符串标识某一互联网资源
URL:资源的地点(在互联网上所处的位置),是一种具体的URI
14、对ajax的理解以及实现
ajax:利用js的异步通信,从服务器获取的XML文档中提取数据,再更新当前网页的对应部分,而不用刷新整个网页
创建ajax请求的步骤:
- 创建一个XMLHttpRequest对象
- 在这个对象上使用open方法创建一个http请求
- open方法的参数是请求的方法、地址、是否异步、用户认证信息
- 在发起请求前,可以为对象添加一些信息和监听函数
- 使用setRequestHeader添加请求头信息
- 状态监听函数,XMLHttpRequest对象共有5种状态,状态变更时会触发onreadystatechange事件
- 当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,可以判断请求状态是否为200或304等状态码,并利用response中的数据进行页面更新
- 当对象的属性和监听函数设置完成之后,可以调用sent方法向服务器发送请求,可以传入参数作为发送的数据体
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", "/server", true);
// 设置状态监听函数
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 请求
xhr.send(null);
使用ppromise封装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;
}
15、为什么进行变量提升,它导致了什么问题
(1)提高性能
-
JS代码执行之前,会进行语法检查和预编译,通过预先识别变量和函数,引擎可以更好地分配资源并提高执行效率
-
在解析的过程中,会为函数生成预编译代码
- 统计声明了哪些变量、创建了哪些函数
- 并对函数代码进行压缩、去除注释、空白等
- 执行函数时都可以直接为函数分配栈空间
- 并且由于代码压缩的原因,代码执行变快
-
var变量在解析时被声明,function函数在被提前声明的同时也会被定义
- 被定义相当于赋值,函数在声明之前可以直接调用不会有问题
(2)提升代码的容错性
声明之前使用代码不会报错
导致问题:
- 变量值被后续的同名变量覆盖
- 循环变量泄露
var tmp = new Date();
function fn(){
console.log(tmp);
if(false){
var tmp = 'hello world';
}
fn(); // undefined
var tmp = 'hello world';
for (var i = 0; i < tmp.length; i++) {
console.log(tmp[i]);
}
console.log(i); // 11
16、什么是尾调用,有什么好处
- 尾调用:函数的最后一步调用另一个函数
- 代码执行是基于执行栈的,在一个函数中执行另一个函数时,会保留当前的执行上下文,然后新建另一个函数的执行上下文加入栈中。
- 使用尾调用,由于已经执行到函数最后一步,不必保留当前的执行上下文,从而节省内存
- ES6的尾调用优化只在严格模式下开启,正常模式下无效
17、ES6模块和commonJS模块的异同
JS模块化的演变经历了一个漫长的过程,从最初的commonJS,到后来的AMD、CMD。再到今天的ES6模块化方案。
优胜劣汰,主要用于Node端的模块化方案CommonJS活了下来,ES6更是成为大家普遍接受的方案
为什么不在浏览器中使用CommonJS
commonJS是同步的,使用require加载文件的时候,必须要等模块加载完成之后才会执行后面的代码。
倘若浏览器使用CommonJS,加载服务器或CDN上面的代码可能会时间很长,所以才有了后续的AMD和CMD方案,都是异步加载,适合在浏览器使用
CommonJS
- module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
- require命令用于加载模块文件
- require第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性
- CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
ES
- export 模块导出
- import模块导入
- JS引擎对脚本静态分析的时候,遇到模块加载命令
import
,就会生成一个只读引用。等到脚本真正执行的时候,再根据这个只读引用,到被加载的那个模块里去取值
- JS引擎对脚本静态分析的时候,遇到模块加载命令
两个重大的差异
- CommonJS输出的模块是一个值的浅拷贝,ES6输出的是值的引用
- CommonJS是运行时加载,ES5模块是编译时输出接口
- CommonJS模块的输出接口是module.exports,是一个对象,该对象只有在脚本运行结束之后才会生成
- ES6模块在编译时就可以输出接口,值是模块的引用
- CommonJS的require()是同步加载模块,ES6 模块的import命令是异步加载(内部可以使用顶层await)
(async () => {
await import('./my-app.mjs');
})();
18、常见的DOM操作有哪些
(1)DOM节点的获取
getElementById // 按照 id 查询
getElementsByTagName // 按照标签名查询
getElementsByClassName // 按照类名查询
querySelectorAll // 按照 css 选择器查询
(2)DOM节点的创建
var container = document.getElementById('container')
var targetSpan = document.createElement('span')
targetSpan.innerHTML = 'hello world'
container.appendChild(targetSpan)
(3)DOM节点的删除
<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')
container.removeChild(targetNode)
// 通过子节点数组来完成删除
var targetNode = container.childNodes[1]
container.removeChild(targetNode)
(4)修改DOM
将指定的两个 DOM 元素交换位置
<html>
<head>
<title>DEMO</title>
</head>
<body>
<div id="container">
<h1 id="title">我是标题</h1>
<p id="content">我是内容</p>
</div>
</body>
</html>
var container = document.getElementById('container')
var title = document.getElementById('title')
var content = document.getElementById('content')
container.insertBefore(content, title)
19、use strict
use strict是一种ES5添加的运行模式(严格模式),这种模式使得JavaScript在严格模式下运行。
严格模式的目的:
- 消除JavaScript语法的不合理不严谨之处,减少怪异行为
- 消除代码运行的不安全之处,保证代码的安全运行
- 提高编译效率,增加运行速度
- 为未来新版本的JavaScript做好铺垫
区别:
- 禁止使用with语句(将某个对象添加到作用域的顶部)
- 禁止this关键字指向全局变量
- 对象不能有重名的属性
20、强语言类型和弱语言类型
强语言类型:强制类型定义的语言,要求变量的使用要严格符合规范,所有的变量必须先定义再使用,并且一个变量一旦被指定了数据类型,除非强制转换数据类型,否则类型不可改变
弱类型语言:弱类型定义语言,变量的类型取决于为变量赋的值
强类型语言在速度上可能略逊色于弱类型语言,但是它的严谨性可以避免很多错误
21、解释型语言和编译型语言
解释型语言:使用专门的解释器对源程序逐行解释成特定平台的机器码,并立即执行,代码在执行时被解释器一行行动态翻译并执行,而不是在执行之前完成翻译。
- 每次运行时,都需要将源代码解释成机器码并执行,效率较低
- 只要平台提供解释器,就可以运行源代码,跨平台执行
- JavaScript、python属于解释型语言
编译型语言:使用专门的编译器,针对特定的平台,将高级语言源代码一次性的编译成可被该平台硬件执行的机器码,并包装成该平台所能识别的可执行性程序的格式。在程序执行之前,需要一个专门的编译过程,把源代码编译成机器语言的文件,如exe格式的文件,以后再运行时,直接使用编译的结果即可
- 一次性的编译成平台相关的机器语言文件,效率较高
- 与特定平台相关,一般不可跨平台
- C、C++
两者主要区别在于:解释型语言逐行编译并执行,编译型语言一次编译后再执行;前者跨平台性好,后者效率高
22、for...of和for...in
for...of是es6新增方法,允许遍历含有Iterator接口的数据结构,并返回各项的值
- for...in遍历的是对象的键名,for...of遍历的是对象的键值
- for...in会遍历整个原型链,性能较差,for...of不会遍历对象的原型链
for...in主要用于遍历对象,for...of主要用于遍历数组、类数组、字符串、set、map以及generator对象(普通对象没有实现迭代器)
23、ajax、axios、fetch
- ajax:发送网络请求
- 本身是针对MVC编程,不符合前端MVVM的浪潮
- 基于原生XHR开发,XHR本身的架构不清晰
- 不符合关注分离(Separation of Concerns)的原则
- 配置和调用方式非常混乱,而且基于事件的异步模型不友好
- fetch:是ajax的替代品,使用了es6的promise对象,是基于promise设计的。fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。
- 语法简洁,更加语义化
- 基于标准 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可以
- axios:基于Promise封装的HTTP客户端
- 浏览器端发起XMLHttpRequests请求
- node端发起http请求
- 支持Promise API
- 监听请求和返回
- 对请求和返回进行转化
- 取消请求
- 自动转换json数据
- 客户端支持抵御XSRF攻击
四、原型、原型链
1、对原型、原型链的理解
原型的产生:函数声明,即创建一个构造函数A时(一般情况下都用构造函数创建对象),浏览器会在内存中创建一个对象B,对象B就是函数A的原型对象,A的prototype属性指向B,B的constructor指向A,原型对象默认只有属性:constructor。其他都是从Object继承而来,暂且不用考虑。
使用原型对象的好处:所有对象实例共享它所包含的属性和方法。
对象实例创建:使用构造函数创建对象,实例对象会有不可见的属性 [[prototype]], 而且这个属性指向了构造函数的原型对象,使用__proto__访问原型对象。
原型链:创建对象实例对象会产生原型链,因为每个对象都拥有一个原型对象,通过__proto__ 指针指向其原型对象,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null。
原型链用途:当访问一个实例属性或方法时,在通过原型链实现继承的情况下,首先会在实例中搜索该属性,在没有找到属性或方法时,便会沿着原型链继续往上搜索,直到原型链末端才会停下来。
原型属性与对象属性:实例对象可以直接访问原型的属性,如果p1定义了属性,访问p1属性,会屏蔽原型对象的属性,p1.hasOwnProperty方法,可以判断一个属性是否来自对象本身,在p1中添加属性为true,属性为原型中或不存在false(in)
2、原型链的终点是什么,如何打印出原型链的终点
Object是构造函数,原型链的终点是Object.prototype.proto,这个值等于null,所以原型链的终点是null
五、作用域
1、闭包
闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式是,在一个函数内部创建另外一个函数,并在内部访问外层函数的局部变量。
闭包有两个常用的用途:
- 创建私有变量
function A() {
let a = 1
window.B = function () {
console.log(a)
}
}
A()
B() // 1
// 经典面试题:循环中使用闭包解决 var 定义函数的问题
for (var i = 1; i <= 5; i++) {
// 输出的结果全是6
setTimeout(function timer() { console.log(i) }, i * 1000)
}
// 使用闭包,每次循环的立即执行函数都会保存当前的形参
for (var i = 1; i <= 5; i++) {
(function(j) {setTimeout(function timer() {console.log(j)}, j * 1000)})(i)
}
2、作用域、作用域链
全局作用域
- 最外层函数、最外层变量
- 未定义直接声明的变量
- 所有window对象的属性
全局变量容易引起命名冲突
函数作用域
- 函数内部声明的变量
块级作用域
- let和const声明的变量属于块级作用域,作用域范围为最近的{}
作用域链
-
函数定义时:js引擎会默认创建一个仅供后台使用的内部属性[[Scope]],此属性存储函数作用域链
- 如果是全局函数,此时包含一个变量对象(全局对象window)
- 如果是嵌套函数,除了自身的变量对象外,作用域链还加上了父函数的变量对象
-
函数调用时:
- 以定义函数时的作用域链初始化它自己的作用域链
- 用 arguments 创建了一个活动对象
- 初始化活动对象,即加入形参、函数声明、变量声明
- 活动对象作为函数的变量对象
作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。 js引擎在查找变量时会先查找当前作用域内,如果查找不到,会查找外层作用域内是否含有,直到查找到全局作用域。
3、执行上下文
执行上下文:函数调用时创建,生命周期主要包含三个阶段,创建阶段、执行阶段、回收阶段
执行上下文类型:全局执行上下文、函数执行上下文、eval函数执行上下文
执行上下文栈:JS引擎使用栈管理执行上下文,在执行代码时,首先执行全局代码,会创建一个全局执行上下文并压入栈中,当遇到函数调用时,会创建函数执行上下文,并压入栈顶,函数执行完成之后,将栈顶的函数执行上下文弹出,直到所有代码执行完毕之后,会弹出全局执行上下文。
4、对this对象的理解
this是函数运行时,在函数体内部自动生成的一个对象,不同的情况下,this指向不同:
- 默认绑定
- 非严格模式下,this默认为window对象
- 严格模式下,this为undefined
- 隐式绑定
- 函数作为某个对象的方法调用时,this指向最近的上级对象
var j = o.b.fn; j();
this永远指向最后调用它的对象,即使fn是b的方法,但是赋值给j的时候没有执行,this指向window
- new 绑定
- 通过构造函数new一个实例对象时,this指向这个实例
- 显式修改
- call、apply、bind
5、call、apply、bind函数
改变函数的执行上下文,也就是改变函数运行时的this指向
- 三者第一个参数都是
this
要指向的对象,如果没有这个参数或参数为undefined
或null
,则默认指向全局window
- 三者都可以传参,但是
apply
是数组,而call
是参数列表,且apply
和call
是一次性传入参数,而bind
可以分为多次传入 bind
是返回绑定this之后的函数,apply
、call
则是立即执行
六、异步编程
1、异步编程的实现方式
- 回调函数
- 多个函数嵌套会形成回调地狱,函数与函数之间的耦合度高,不利于代码维护
- Promise
- 将嵌套回调改为链式调用,但是也会造成then链太长
- generator
- 执行异步函数时,将函数的执行权转移出去,异步函数执行完毕之后,再将执行权转移回来,可以用同步的顺序来书写
- async/await
- 这是generator和promise实现的语法糖
- 内部自带执行器,当函数内部执行到await语句时,如果语句返回promise对象,函数会等待promise的对象变为resolve之后再继续向下执行。
- setTimeout
- 延时器,指定时间后执行函数
2、事件队列以及setTimeout、Promise、async/await区别
事件循环:通过异步执行任务的方法,解决单线程的弊端
- 一开始整个脚本作为一个宏任务执行
- 执行过程中同步代码直接执行,执行到异步操作时,将任务放在任务队列中(宏任务、微任务)
- 当前宏任务执行完毕,读取微任务列表,执行所有的微任务
- 执行浏览器UI线程的渲染工作
- 检查执行web worker任务
- 执行完本轮宏任务,回到步骤二,循环执行,直到所有的宏任务和微任务全部执行完毕
宏任务
- script
- setTimeout、setInterval
- I/O
- requestAnimationFrame
- postMessage
微任务
- promise的then、catch、finally await之后的代码
- MutationObserver
- process.nextTick(node环境中)
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('settimeout')
})
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
//答案:
script start
async1 start
async2
promise1
script end
async1 end
promise2
settimeout
3、Promise
promise对象有三种状态
- pending 进行中
- fulfilled 成功
- rejected 失败
Promise是一个构造函数
- new生成一个promise实例,接收一个函数作为参数,函数的两个参数分别是resolve和reject
- resolve函数的作用:将promise对象的状态由pending转为fulfilled
- reject函数的作用:将promise对象的状态由pending转为rejected
实例方法
- then:状态为成功时调用
- catch:状态为失败时调用
- finally:无论promise的状态如何,最终都会执行的操作
构造函数方法
all()、race()、allSettled()、resolve()、reject()、try()
- Promise.all
// p1, p2, p3都成功,p成功
// 只要一个失败,p失败
// 如果参数promise自己定义了catch,一旦他被rejected,不会触发promise.all的catch方法
const p = Promise.all([p1, p2, p3]);
- Promise.race
// p的状态由最先改变状态的参数promise决定
const p = Promise.race([p1, p2, p3]);
- Promise.allSettled
// 等到所有实例均返回结果,不管是fulfilled还是rejected,包装实例才会结束
const promises = [
fetch('/api-1'),
fetch('/api-2'),
fetch('/api-3'),
];
await Promise.allSettled(promises);
removeLoadingIndicator();
- Promise.resolve
// 将现有对象转换为promise对象
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
- Promise.reject
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
console.log(s)
});
// 出错了
4、Promise使用场景
将图片的加载写成一个promise,一旦加载完成,promise的状态就会变化
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
初始化页面数据时,汇总所有数据请求结果,请求完成之后关闭loading
function initLoad(){
// loading.show() //加载loading
Promise.all([getBannerList(),getStoreList(),getCategoryList()]).then(res=>{
console.log(res)
loading.hide() //关闭loading
}).catch(err=>{
console.log(err)
loading.hide()//关闭loading
})
}
//数据初始化
initLoad()
通过race可以设置图片请求超时
//请求某个图片资源
function requestImg(){
var p = new Promise(function(resolve, reject){
var img = new Image();
img.onload = function(){
resolve(img);
}
//img.src = "https://b-gold-cdn.xitu.io/v3/static/img/logo.a7995ad.svg"; 正确的
img.src = "https://b-gold-cdn.xitu.io/v3/static/img/logo.a7995ad.svg1";
});
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);
});
5、async/await
async返回的是一个promise对象,如果函数return了一个值,async 会把这个直接量通过 Promise.resolve()
封装成 Promise 对象
function testAsy(x){
return new Promise(resolve=>{setTimeout(() => {
resolve(x);
}, 3000)
}
)
}
async function testAwt(){
let result = await testAsy('hello world');
console.log(result); // 3秒钟之后出现hello world
console.log('cuger') // 3秒钟之后出现cug
}
testAwt();
console.log('cug') //立即输出cug
如果调用的 promise 的状态是 pending ,那么调用 then 不会加微任务,而只是将回调保存在 promise 的“属性([[PromiseFulfillReactions]]
)”里面。当 promise 的状态由 pending 变化为 fullfilled 的时候,这个属性的里的所有记录会被加进微任务队列
七、面向对象
1、对象创建的方式
- 字面量
- new操作符(构造函数/class)
- Object返回对象的方法
// Object构造函数,这种方式可以创建一个空对象,现实不推荐使用
const obj = new Object()
// Object的create方法
// create方法通过传入一个prototype原型对象,返回一个新对象
const obj = Object.create(null)
// Object的assign方法,会返回一个对象
const object = Object.assign({})
// 字面量
const obj = {}
// 自定义构造函数创建
function Person(name){
this.name = name;
this.age = 21;
}
const object = new Person("hap");
2、对象的继承
继承可以使子类具有父类的属性和方法,子类也可以重新定义属性和方法来覆盖父类的属性和方法
实现方式:原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承
(1)原型链继承
- 实例共享原型
function Parent() {
this.name = 'parent';
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child';
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
console.log(new Child())
var s1 = new Child();
var s2 = new Child();
s1.play.push(4);
console.log(s1.play, s2.play); // [1,2,3,4]
改变s1的play,发现s2的play也被修改了,因为两个实例使用的是同一个原型对象
(2)构造函数继承
-
call
调用Parent
函数 - 不能继承父元素原型对象上的方法和属性
function Parent(){
this.name = 'parent';
}
Parent.prototype.getName = function () {
return this.name;
}
function Child(){
Parent.call(this);
this.type = 'child'
}
let child = new Child();
console.log(child); // 没问题
console.log(child.getName()); // 会报错
(3)组合继承 前面两种方式各有优缺点,组合继承结合两者
- 解决原型链继承,实例共享原型的问题
- 解决构造函数继承,子类不能继承原型属性方法的问题
组合继承缺点:多次调用父级的构造函数
function Parent () {
this.name = 'parent';
this.play = [1, 2, 3];
}
Parent.prototype.getName = function () {
return this.name;
}
function Child() {
// 第二次调用 Parent()
Parent.call(this);
this.type = 'child';
}
// 第一次调用 Parent()
Child.prototype = new Parent();
// 手动挂上构造器,指向自己的构造函数
Child.prototype.constructor = Child;
var s3 = new Child();
var s4 = new Child();
s3.play.push(4);
console.log(s3.play, s4.play); // 不互相影响
console.log(s3.getName()); // 正常输出'parent'
console.log(s4.getName()); // 正常输出'parent'
(4)原型式继承
- 将对象作为原型对象,实例指向同一个原型对象 原型式继承
// Object.create内部实现类似于
function object(o){
function F(){}
F.prototype = o;
return new F();
}
let parent = {
name: "parent",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
let p1= Object.create(parent);
p1.name = "tom"; // 没有修改原型上的name,只是自定义属性
p1.friends.push("jerry");
let p2= Object.create(parent);
p2.friends.push("lucy");
console.log(p1.name); // tom
console.log(p2.name); // parent
console.log(p1.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(p2.friends); // ["p1", "p2", "p3","jerry","lucy"]
Object.create
方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能
(5)寄生式继承
寄生式继承在原型式继承基础上进行优化,利用这个浅拷贝的能力再进行增强,添加一些方法
let parent5 = {
name: "parent5",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
function clone(original) {
let clone = Object.create(original);
clone.getFriends = function() {
return this.friends;
};
return clone;
}
let person5 = clone(parent5);
console.log(person5.getName()); // parent5
console.log(person5.getFriends()); // ["p1", "p2", "p3"]
缺点同原型式继承:共享原型
(6)寄生组合式继承 - 最优解
function Parent() {
this.name = 'parent';
this.play = [1, 2, 3];
}
Parent.prototype.getName = function () {
return this.name;
}
function Child() {
Parent.call(this);
this.friends = 'child';
}
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.getFriends = function () {
return this.friends;
}
(7)ES6的extends关键字
class Person {
constructor(name) {
this.name = name
}
// 原型方法
// 即 Person.prototype.getName = function() { }
// 下面可以简写为 getName() {...}
getName = function () {
console.log('Person:', this.name)
}
}
class Gamer extends Person {
constructor(name, age) {
// 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
super(name)
this.age = age
}
}
const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法
八、垃圾回收与内存泄漏
1、垃圾回收机制
垃圾回收
- javaScript代码在运行时,需要分配内存空间来存储变量
- 当变量不再参与运行时,就需要收回被占用的内存空间,这就是垃圾回收
常用的回收方式:引用计数和标记清除
标记清除
- 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记
- 为环境中的变量删除标记
- 剩下的带标记的变量即为要清除的内存
引用计数
- 跟踪值的引用次数
- 引用次数为0时,变量占用的内存可以被释放
- 具有循环引用的问题
通过将变量值设置为null可以手动释放内存
2、减少垃圾回收
-
对数组进行优化: 在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以此来达到清空数组的目的(堆内存中没有放任何东西)
-
对
object
进行优化: 对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收 -
对函数进行优化: 在循环中的函数表达式,如果可以复用,尽量放在函数的外面
- 函数表达式
var notHoisted = function () {};
- 函数表达式
3、内存泄漏
- 意外的全局变量: 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收
- 被遗忘的计时器或回调函数: 设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收
- 脱离 DOM 的引用: 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以元素无法被回收
- 闭包: 不合理的使用闭包,从而导致某些变量一直被留在内存当中
九、其他
1、什么是微前端
微前端:将应用程序拆分成多个子应用程序,每个子应用程序都可以独立的进行开发、测试和部署。
qiankun 是一款微前端框架。
微前端的实现:iframe、web components等
2、幽灵依赖
幽灵依赖:没有在项目的 package.json 中定义,但是可以直接使用的依赖
原因:由于扁平化的 node_modules 引起的,依赖提升将所有的依赖和子依赖都放在目录顶层,但是同名的包只会提升一次
问题:a 的子依赖 b 被提升到顶层,项目中可以直接使用依赖 b,万一 a 被删除或者 a 中的 b 版本更新,会对项目产生一定的影响。
扁平化问题:幽灵依赖、扁平化算法本身的复杂性较高、耗时较长
3、npm、yarn、pnpm
yarn 最先实现扁平化依赖,且安装速度比 npm 快
pnpm 速度更快,解决了npm和yarn的幽灵依赖的问题
4、函数柯里化
将一个接受多个参数的函数,转换成多个函数。第一个函数接受部分参数,并返回一个函数,可以用来接收剩下的参数。
5、函数式编程
(1)概念
函数式编程是一种编程范式,也就是如何编写程序的方法论,它属于结构化编程的一种,主要是把运算过程写成一系列嵌套的函数调用。
(2)特点
- 和其他数据类型一样使用,可以复制给其他变量,可以作为参数,也可以作为返回值
- 只用表达式,不用语句,也就是说每一步都是单纯的运算,且有返回值
- 纯函数,相同的输入总会有相同的输出
- 没有副作用,函数保持独立,功能就是运算并返回值,不得修改外部的变量
- 所谓副作用指的是函数内部与外部互动,产生运算以外的其他结果(比如:修改全局变量的值)
- 不使用状态,函数式编程只返回值,不修改值,也就是说不会将状态存在变量里面,会将状态保存在参数里面
(3)优势
- 代码简洁,开发迅速:函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快。
- 接近自然语言,易于理解(add(1,2).multiply(3).subtract(4))
- 更方便的代码管理:函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试和错误定位,以及模块化组合。
- 易于并发编程:函数式编程不需要考虑"死锁",因为它不修改变量,所以根本不存在"锁"线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署"并发编程"。
- 代码的热升级,函数式编程没有副作用,只要保证接口不变,内部实现是外部无关的。所以,可以在运行状态下直接升级代码,不需要重启,也不需要停机