JavaScript
说说js的数据类型
js的数据类型可以分为两类,基本数据类型和引用数据类型
基本数据类型
基本数据类型主要有6种:Number,String,Boolean,Symbol,Null,Undefined
number
最常见的
整数
类型格式则为十进制,还可以设置八进制(零开头)、十六进制(0x开头)1
2
3let intNum = 55 // 10进制的55
let num1 = 070 // 8进制的56
let hexNum1 = 0xA //16进制的10浮点类型
则在数值中必须包含小数点
,还可通过科学计数法表示。1
2
3
4let floatNum1 = 1.1;
let floatNum2 = 0.1;
let floatNum3 = .1; // 有效,但不推荐
let floatNum = 3.125e7; // 等于 31250000在数值类型中,存在一个特殊数值
NaN
,意为“不是数值”,用于表示数值运算操作失败了(而不是抛出错误)1
2console.log(0/0); // NaN
console.log(-0/+0); // NaNstring
字符串使用双引号(”)、单引号(’)或反引号(`)表示都可以。
在js中,字符串是
不可变
的,意思是一旦创建,它们的值就不能变了。1
2
3
4
5
6
7let lang = "Java";//这行代码会在内存中创建一个包含 "Java" 的字符串对象,并将引用赋值给变量 lang。
//从内存中读取 lang 当前所指向的字符串 "Java"。
//将 "Java" 和 "Script" 拼接成新的字符串 "JavaScript",并在内存中创建一个新的字符串对象。
//将 lang 变量存储的引用更新为新创建的字符串 "JavaScript"的引用
lang = lang + "Script";
console.log(lang)Boolean
Boolean(布尔值)类型有两个字面值:
true
和false
通过
Boolean
可以将其他类型的数据转化成布尔值:1
2
3
4
5数据类型 转换为 true 的值 转换为 false 的值
String 非空字符串 ""
Number 非零数值(包括无穷值) 0 、 NaN
Object 任意对象 null
Undefined N/A (不存在) undefined注意:在js中
负数
转化成布尔值也是true
Symbol
Symbol
关键字的主要用途是用来创造一个唯一
的标识符,用作对象属性,确保不会产生属性冲突
。1
2
3
4
5
6
7let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
console.log(genericSymbol == otherGenericSymbol); // false
//传入符号主要为了标识,符号相同并不代表值也相同
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(fooSymbol == otherFooSymbol); // falseNull
Null类型同样只有一个值,即特殊值
null
逻辑上讲, null 值表示一个空对象,这也是给
typeof
传一个null
会返回"object"
的原因。1
2let car = null;
console.log(typeof car); // "object"Undefined
Undefined
类型只有一个值,就是特殊值undefined
,如果一个变量声明了但是未被赋值,那么这个变量的值就是undefined。1
2
3let message; // 这个变量被声明了,只是值为 undefined
console.log(message); // "undefined"
console.log(age); // 没有声明过这个变量,报错
引用数据类型
引用数据类型统称为Object
主要包括以下三种:
Object
通常使用字面量表示法来创建对象,这样创建的对象是
Object
构造函数的实例。1
2
3
4
5let person = {
name: "Nicholas",
"age": 29,
5: true
};Array
js数组是一组
有序
的数据,但跟其他语言不同的是,数组中每个槽位可以存储任意类型
的数据。并且,数组也是动态大小
的,会随着数据添加而自动增长。通常通过字面量表示法
创建数组。1
2let colors = ["red", 2, {age: 20 }]
colors.push(2)或者通过
Array
来创建数组1
2
3const arr = new Array(4)//创建一个长度为4的数组,虽然创建的时候指定了长度,但是长度还是可以变化的
arr.fill(0) //数组中的每个元素初始值为undefined,我们把它初始化为1
arr.map(i=>new Array(4).fill(0))//把数组中的每个元素替换为数组,实现二维数组的创建。Function
函数实际上是
对象
,每个函数都是Function
类型的实例,而Function
也有属性和方法,跟其他引用类型一样。其他类型
除了上述说的三种之外,还包括
Date
、RegExp
、Map
、Set
等,他们都是Object
类型的子类
。
typeof和instanceof
typeof
typeof
操作符返回一个字符串,表示值的数据类型。
1 | typeof operand |
这两种使用方法都是可以的。下面是一些例子。
1 | typeof 1 // 'number' |
instanceof
主要用来判断某个构造函数
是否在某个实例对象
的原型链上。
1 | object instanceof constructor |
区别
typeof返回的是字符串,instanceof返回的是
布尔值`
typeof能判断基本数据
的类型,但是不能准确
判断引用数据
的类型。
intanceof能准确判断引用数据
的类型, 但是不能判断基本数据
的类型
可以看到,上述两种方法都有弊端,并不能满足所有场景的需求
Object.prototype.toString()
还有一种通用的判断方式**Object.prototype.toString()**,简单来说就是Object
原型对象上挂载的toString
方法。
1 | Object.prototype.toString({}) // "[object Object]" |
可以看到返回的结果是一个字符串,第一位都是object
,这与js中万物皆对象的思想符合。所有类型的数据都能调用toString()
方法,如果是基本数据类型,会先进行数据装箱
,转化成对象
,再调用这个方法。Object是所有对象的父类(Object在所有对象的原型链上),所以所有对象都能访问到这个方法,那为什么不直接让数据调用这个方法呢?因为对象的原型链上可能还存在同名方法
。使用函数.call(对象)
的方式,能确保对象调用的就是指定的函数/方法。
谈谈 JavaScript 中的类型转换机制
前面我们讲到,JS
中有六种简单数据类型:undefined
、null
、boolean
、string
、number
、symbol
,以及引用类型:object
常见的类型转换有:
- 强制转换(显示转换)
- 自动转换(隐式转换)
显示转换
显示转换,即我们很清楚可以看到这里发生了类型的转变,常见的方法有:
- Number()
- parseInt()
- String()
- Boolean()
Number()
将任意类型的值转化为数值
1 | Number(324) // 324 |
从上面可以看到,Number
转换的时候是很严格的,只要有一个字符无法转成数值,整个字符串就会被转为NaN
parseInt()
parseInt
相比Number
,就没那么严格了,parseInt
函数逐个解析字符,遇到不能转换的字符就停下来
1 | parseInt('32a3') //32 |
String()
可以将任意类型的值转化成字符串
1 | // 数值:转为相应的字符串 |
可以看到,对于基本数据类型
,强制转化成字符串,就是加个双引号
就好了,而应用数据类型就不一样了,需要调用toString
方法
Boolean()
可以将任意类型的值转为布尔值
,转换规则如下:
1 | Boolean(undefined) // false |
隐式转换
在隐式转换中,我们可能最大的疑惑是 :何时发生隐式转换
?
我们这里可以归纳为两种情况发生隐式转换的场景:
- 比较运算(
==
、!=
、>
、<
) - 算术运算(
+
、-
、*
、/
、%
) if
、while
需要布尔值地方
除了上面的场景,还要求运算符两边的操作数不是同一类型
自动转化成布尔值
在需要
布尔值
的地方,就会将非布尔值的参数自动转为布尔值,系统内部会调用Boolean
函数自动转换成字符串
遇到预期为
字符串
的地方,就会将非字符串的值自动转为字符串常发生在
+
运算中,一旦存在字符串,则会进行字符串拼接操作1
2
3
4
5
6
7
8'5' + 1 // '51'
'5' + true // "5true"
'5' + false // "5false"
'5' + {} // "5[object Object]"
'5' + [] // "5" 因为[]转换成字符串是空串
'5' + function (){} // "5function (){}"
'5' + undefined // "5undefined"
'5' + null // "5null"对于基本数据类型和函数,字符串拼接的时候直接参与拼接,对于其他引用数据类型,需要先调用
toString
方法。自动转换成数值
除了
+
号,其他运算符
都会把参与运算的数据自动转成数值
1
2
3
4
5
6
7
8
9
10'5' - '2' // 3
'5' * '2' // 10
true - 1 // 0
false - 1 // -1
'1' - 1 // 0
'5' * [] // 0
false / '5' // 0
'abc' - 1 // NaN
null + 1 // 1
undefined + 1 // NaN
==和===的区别
一个是宽松比较
,一个是严格比较
;==
比较值
是否相等,不比较类型
是否相同,允许隐式转换。===
比较的是值
和类型
是否相同,都相同才会返回true。
难点在于==
非严格比较,比较规则如下
undefined == null 返回true
在非严格比较中,
undefined
和nul
l,只与undefined
或者null
相等。NaN == NaN 返回false
NaN
和任何数比较,包括本身,都返回false。两个都为
简单数据类型
,字符串和布尔值都会转换成数值,再比较。如果一个操作数是
对象
,另一个操作数不是,则调用对象的valueOf()
方法取得其原始值,再根据前面的规则进行比较。两个都为引用类型,则比较它们是否指向同一个对象,也就是比较地址是否相同。
Javascript 数字精度丢失的问题
为什么会出现精度丢失
对于某些小数,计算机无法用有限的二进制位精确的表示,比如0.1用二进制表示思路如下:
1 | 0.1*2=0.2<1 --0 |
假设0.1
的二进制表示是0.xxxx
,每次对0.1×2
,都会让二进制表示中的小数点右移
一位(就像我们给十进制小数×10
会让小数点右移一样,每个数的权值都变大了),即x.xxxx
,如果0.1×2<1
,说明第一个x为0,依此类推,0.1
的二进制表示为0.0001xxxx
,然后我们继续计算后续的x的值。0.1×16 = 1.6
,对应的二进制表示为1.xxxx
,显然0.xxxx
应该表示的是0.6
,所以我们就把问题转化为求0.6
的二进制表示了。简单的来说,我们只需要对小数不断的×2
,如果结果小于1
,填入0
,反之填入1
,然后对乘法的结果-1
,然后继续计算,填入的位置是从小数点的高位到地位。
1 | 0.6*2=1.2>1 --1 |
因此我们可以得出0.1
的二进制表示是0.000110011....
很明显这是一个无限循环小数
,我们无法用有限的二进制位来精确的存储这个小数,因为存储的时候,数据就没有被准确的存储,所以下次再取出使用的时候就会有精度损失。
在JavaScript
中,现在主流的数值类型是Number
,而Number
采用的是IEEE754
规范中64位双精度浮点数编码,如何理解这个双
字呢,这个双
表示使用2个机器字
(word)来表示浮点数,通常现代计算机的一个机器字
是 32 位,双精度意思就是用64位
来表示浮点数。这样的存储结构优点是可以统一处理整数和小数,节省存储空间,具体如何处理可以自行搜索或者参考:
面试官:说说 Javascript 数字精度丢失的问题,如何解决? | web前端面试 - 面试官系列
如何解决精度缺失问题
- 先把
小数
转换成整数
再参与运算。 - 借助第三方工具库,比如
Math.js
、BigDecimal.js
,通过调用相关方法
来模拟加减乘除运算。
说说 JavaScript 中内存泄漏的几种情况
如何理解内存泄漏
内存泄漏(Memory leak)指的是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存。
对于持续运行的进程
,必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃
垃圾自动回收机制
在C
语言中,因为是手动管理内存,内存泄露是经常出现的事情。
这很麻烦,所以大多数语言提供自动内存管理
,减轻程序员的负担,这被称为”垃圾自动回收机制”
js也有垃圾自动回收机制。
原理:垃圾收集器
会定期(周期性)找出那些不再继续使用的变量,然后释放其内存。
通常情况下有两种实现方式,用来判断哪些变量不再使用:
- 标记清除
- 引用计数
标记清除
清除
那些被标记
的变量,释放它们的内存,是JavaScript
最常用的垃圾收回机制,
当变量进入执行环境是,就标记这个变量为“进入环境“。进入环境的变量所占用的内存就不能释放,当变量离开环境时,则将其标记为“离开环境“
垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉
在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了
随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存
1 | var m = 0,n = 19 // 把 m,n,add() 标记为进入环境。 |
引用计数
语言引擎有一张”引用表”,保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0
,就表示这个值不再用到了,因此可以将这块内存释放。
如果一个值不再需要了,引用数却不为0
,垃圾回收机制无法释放这块内存,从而导致内存泄漏。
1 | const arr = [1, 2, 3, 4]; |
上面代码中,数组[1, 2, 3, 4]
是一个值,会占用内存。变量arr
是仅有的对这个值的引用,因此引用次数为1
。尽管后面的代码没有用到arr
,它还是会持续占用内存
如果需要这块内存被垃圾回收机制释放,只需要设置如下:
1 | arr = null |
通过设置arr
为null
,就解除了对数组[1,2,3,4]
的引用,引用次数变为 0,就被垃圾回收了。
注意
有了垃圾自动回收机制,并不代表不用担心内存泄漏问题,对于那些占用内存很大的变量,确保它们不再被使用的时候,不存才对它们的引用。
常见内存泄漏情况
意外的全局变量
1 | function foo(arg) { |
给一个未声明的标识符赋值,JavaScript 引擎会认为你在引用一个已经存在的全局变量;如果找不到这个变量,则会自动在全局对象(浏览器环境中为 window
,Node.js 环境中为 global
)上创建它。
1 | function foo() { |
上述使用严格模式,可以避免意外的全局变量。
定时器
定时器开启后,除非显式的清除,否则将一直存在,如果定时器中引用了不再使用的变量,又未及时清除定时器,就会造成内存泄漏。
1 | var someResource = getData(); |
如果id
为Node的元素从DOM
中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource
的引用,定时器外面的someResource
也不会被释放。
闭包
1 | function bindEvent() { |
说说你对闭包的理解?闭包使用场景
是什么
闭包由一个内部函数
和它引用的外部函数
的作用域组成。
使用场景
- 创建私有变量
- 延长变量的生命周期
示例
1 | //立即执行函数 |
说说你对防抖和节流的理解
是什么
本质上是优化高频率执行代码造成的性能损耗
的一种手段。
如:浏览器的 resize
、scroll
、keypress
、mousemove
等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能。
为了优化体验,我们需要限制这类事件的调用次数,对此我们就可以采用 防抖(debounce) 和 节流(throttle) 的方式来减少调用频率
防抖
定义
事件被触发后,且在n秒内不再触发该事件,则执行对应的回调函数,如果在n秒内再次触发该事件,则重新开始计时,可以用操作系统中的资源被剥夺来理解,这里的资源就是定时器
。
手写防抖函数
1 | <body> |
1 | //传入一个函数,返回一个实现了防抖的函数 |
节流
定义
在n秒内无论触发多少次事件,只执行第一次触
发对应的回调函数
,可以用操作系统中的资源不可被剥夺来理解
,这里的资源就是定时器
手写节流
1 | <body> |
1 | document.querySelector('button').addEventListener('click', myThrottle(func, 1000)) |
区别与联系
相同点:
- 都可以通过使用
setTimeout
实现 - 目的都是,降低回调执行频率。节省计算资源
不同点:
- 函数防抖,在一段连续操作结束后,只执行最后一次触发对应的回调。函数节流,在一段连续操作中,每一段时间只执行一次,在频率较高的事件中被使用来提高性能。
- 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次。
应用场景
防抖在连续的事件,只需触发一次回调的场景有:
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求
- 手机号、邮箱验证输入检测
- 窗口大小
resize
。只需窗口调整完成后,计算窗口大小。防止重复渲染。
节流在间隔一段时间执行一次回调的场景有:
- 滚动加载,加载更多或滚到底部监听
- 搜索框,搜索联想功能
数组的常用方法
我们可以从增删查改,是否会修改原数组这几个角度来给数组的常用方法归类
增
push():可以传入任意个数的元素,这些元素会被添加到数组的末尾,返回
新数组的长度
,会修改原数组。1
2
3let colors = []; // 创建一个数组
let count = colors.push("red", "green"); // 推入两项
console.log(count) // 2unshift():也是可以传入任意个数的元素,这些元素会被添加到数组的首部,返回
新数组的长度
,会修改原数组。1
2
3let colors = new Array(); // 创建一个数组
let count = colors.unshift("red", "green"); // 从数组开头推入两项
console.log(count); // 2splice():第一个参数传入开始位置,第二个参数传入0,表示不删除元素,后续参数传入插入的元素。
1
2
3
4let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 0, "yellow", "orange")
console.log(colors) // red,yellow,orange,green,blue(插入的元素从开始下标开始排序)
console.log(removed) // [],返回包含被删除元素的数组,因为没有元素被删除所以是空数组concat():首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组。
1
2
3
4let colors = ["red", "green", "blue"];
let colors2 = colors.concat("yellow", ["black", "brown"]);
console.log(colors); // ["red", "green","blue"]
console.log(colors2); // ["red", "green", "blue", "yellow", "black", "brown"]
删
pop():方法用于删除数组的最后一项,同时减少数组的
length
值,返回被删除的项
1
2
3
4let colors = ["red", "green"]
let item = colors.pop(); // 取得最后一项
console.log(item) // green
console.log(colors.length) // 1shift():法用于删除数组的第一项,同时减少数组的
length
值,返回被删除的项
1
2
3
4let colors = ["red", "green"]
let item = colors.shift(); // 取得第一项
console.log(item) // red
console.log(colors.length) // 1splice():第一个参数传入开始位置,第二个参数传入要删除元素的个数,返回包含被删除元素的数组。
1
2
3
4let colors = ["red", "green", "blue"];
let removed = colors.splice(0,1); // 删除第一项
console.log(colors); // green,blue
console.log(removed); // ["red"],只有一个元素的数组slice():本质是返回一个数组切片,并不会修改原数组,截取区间遵循
左闭右开
原则。1
2
3
4
5
6let colors = ["red", "green", "blue", "yellow", "purple"];
let colors2 = colors.slice(1);
let colors3 = colors.slice(1, 4);
console.log(colors) // red,green,blue,yellow,purple
concole.log(colors2); // green,blue,yellow,purple
concole.log(colors3); // green,blue,yellow
改
一般通过下标修改数组元素的值,也可以使用splice先删除元素再添加元素。
1 | let colors = ["red", "green", "blue"]; |
查
一般也是通过下标来查找数组元素。
indexOf():传入一个元素,返回数组中
第一个
与该元素相等的元素,使用的是严格比较
,如果没有则返回-1
,NaN
不与任何数相等,所以indexOf(NaN)返回值必定为-1
1
2
3let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1,NaN];
numbers.indexOf(4) // 3
console.log(numbers.indexOf(NaN)) // -1includes():判断某个元素是否再数组中存在,也是严格比较,存在返回
true
,否则返回false
,对NaN
做了特殊处理,能判断是它否存在。1
2
3let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1,NaN];
numbers.includes(4) //true
numbers.includes(NaN) //返回truefind():传入一个返回值是布尔类型的回调函数,用于判断满足某个条件的元素是否存在,通常用于
对象数组
。1
2
3
4
5
6
7
8
9
10
11const people = [
{
name: "Matt",
age: 27
},
{
name: "Nicholas",
age: 29
}
];
people.find((element, index, array) => element.age < 28) // // {name: "Matt", age: 27}
- findIndex():语法和用途和find相同,不过返回的是元素的下标,未找到返回-1。
排序方法
reverse():反转数组
1
2
3let values = [1, 2, 3, 4, 5];
values.reverse();
alert(values); // 5,4,3,2,1sort():给数组排序,sort()方法接受一个比较函数,用于判断哪个值应该排在前面
1
2
3
4
5
6
7
8function compare(value1, value2) {
//return value1-value2 升序排序
//return value2-value1 降序排序
//value1[key]-value2[key] 根据某个属性升序排序,反之降序排序
}
let values = [0, 1, 5, 10, 15];
values.sort(compare);
alert(values);
转换方法
join():把数组中的元素拼接成一个字符串,用传入的符号连接。
1 | let colors = ["red", "green", "blue"]; |
迭代方法
some():传入一个返回值为布尔值的回调函数,如果数组中的
有个
元素传入该回调函数能使返回值为true,则该方法返回true,否则返回false。every():传入一个返回值为布尔值的回调函数,如果数组中的
每个
元素传入该回调函数能使返回值为true,则该方法返回true,否则返回false。forEach():遍历数组中的每个元素并执行一定操作,可以
修改
原数组。1
2
3
4let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.forEach((item, index, array) => {
// 执行某些操作
});filter():传入一个返回值为布尔值的回调函数,返回一个包含所有能让这个回调函数返回值为true的数组。
1
2
3let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let filterResult = numbers.filter((item, index, array) => item > 2);
console.log(filterResult); // 3,4,5,4,3map():根据传入的回调函数修改数组中的每一个元素并返回一个新的数组。
1
2
3let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let mapResult = numbers.map((item, index, array) => item * 2);
console.log(mapResult) // 2,4,6,8,10,8,6,4,2
字符串常用方法
操作方法
concat
用于将一个或多个字符串拼接成一个新字符串,返回一个新的字符串,不会修改原来的字符串。
1 | let stringValue = "hello "; |
slice()/substr()/substring()
作用是返回字符串的切片
1 | let stringValue = "hello world"; |
可以看出slice()
和substring()
的用法是一致的,当传入两个参数的时候,分别表示的是截取的左右区间(左闭右开),而substr()传入两个参数时,第一个表示参数起始位置,第二个参数表示的是要截取的元素的个数
。
当只传入一个参数,三者的效果是相同的。
indexOf()/startWith()/includes()
indexOf:从字符串开头
去搜索传入的字符串,并返回位置(如果没找到,则返回 -1 )
1 | let stringValue = "hello world"; |
startWith():判断字符串是否以某个字符串开头,返回值为布尔类型。
includes():判断字符串中是否包含某个字符串,返回值是布尔类型。
1 | let message = "foobarbaz"; |
由此可见,无论是数组还是字符串中,都有indexOf和includes方法
字符串拆分
把字符串按照指定的分割符,拆分成数组中的每一项
1 | let str = "12+23+34" |
模板匹配
match()
接收一个参数,可以是一个正则表达式字符串,也可以是一个
RegExp
对象,返回数组1
2
3
4let text = "cat, bat, sat, fat";
let pattern = /.at/;
let matches = text.match(pattern);
console.log(matches[0]); // "cat"search()
replace()
深拷贝浅拷贝
当我们拷贝一个基本类型的数据,拷贝的就是它的值
,此时没有深浅拷贝一说,只有当我们拷贝一个对象的时候,才有深浅拷贝的说法。
浅拷贝
浅拷贝顾名思义,就是浅层次的拷贝,只拷贝一层
。当我们要拷贝一个对象的时候,对于这个对象的所有属性,如果属性的值是基本数据类型
,那我们直接拷贝值
,如果属性值为引用数据类型
,则拷贝地址
。示例如下:
1 | function shallowClone(obj) { |
浅拷贝常见方法
Object.assign
把某个对象的所有
可枚举属性
拷贝到另一个对象上。1
2
3
4
5
6
7
8
9
10
11
12let f = Symbol()
var obj = {
age: 18,
nature: ['smart', 'good'],
names: {
name1: 'fx',
},
f:'cindy'//不可枚举,不会被拷贝
}
let obj2 = {}
var newObj = Object.assign(obj2, obj);
console.log(obj2 == newObj) //返回true,说明返回的就是原对象(传入的第一个对象)
使用
扩展运算符
实现的拷贝1
2
3
4
5const fxArr = ["One", "Two", "Three"]
const fxArrs = [...fxArr]
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]
深拷贝
对一个对象进行深拷贝,拷贝多层。当我们要深拷贝一个对象的时候,对于这个对象的所有属性,如果属性的值是基本数据类型
,那我们直接拷贝值
,如果属性值为引用数据类型
,则我们递归拷贝这个引用类型。深拷贝得到的对象与原对象没有任何公共的内存空间。
深拷贝常见方法
_.cloneDeep()
借助第三方库
lodash
1
2
3
4
5
6
7
8const _ = require('lodash');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false,验证是深度拷贝手写递归
JSON.stringify()
但是这种方式存在弊端,会忽略值为
undefined
、symbol
和函数
的属性,因为这些值都是不可被序列化
的,不会出现在序列化的字符串中。1
2
3
4
5
6
7
8const obj = {
name: 'A',
name1: undefined,
name3: function() {},
name4: Symbol('A')
}
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}
总结
浅拷贝和深拷贝都创建出一个新的对象,这个新的对象与原对象内容完全相同,浅拷贝新旧对象可能存在公共的空间,修改新对象属性可能会影响原对象,而深拷贝新旧对象则不存在公共的空间,修改数据不会相互影响。
说说js中的事件模型
事件与事件流
事件就是用户与页面或者浏览器进行的交互操作;
事件流都会经历三个阶段:
- 事件捕获阶段(capture phase)
- 处于目标阶段(target phase)
- 事件冒泡阶段(bubbling phase)
当我们在某个元素上触发某个事件的时候(这个与我们直接交互的元素叫做目标元素
),然后事件流就会从顶级元素开始,通常是DOM
元素,流向目标元素,这个向下流动的过程叫做事件捕获
,再流回顶级元素,这个向上流动的过程叫做事件冒泡
。事件监听通常是在冒泡阶段触发的。
事件模型分类
有三大类:原始事件模型,标准事件模型,ie事件模型(很少用了)
原始事件模型(DOM0级)
绑定方式:
1
<input type="button" onclick="fun()"> //里面的js代码会被执行,所以不要写成函数名
1
2var btn = document.getElementById('.btn');
btn.onclick = fun;解绑方式:
1
btn.onclick = null;
特点:
- 绑定速度快
- 只支持冒泡触发,不支持捕获触发
同一个类型
的事件(比如click事件)只能绑定一次,后面绑定的会覆盖前面绑定的。
标准事件模型(DOM2级)
绑定方式:
1
dom.addEventListener(eventType, handler, useCapture)
参数如下:
eventType
指定事件类型(不要加on)handler
是事件处理函数useCapture
是一个boolean
用于指定是否在捕获阶段进行处理,默认值为false
,与IE浏览器保持一致
示例:
1
2
3
4
5<div id='div'>
<p id='p'>
<span id='span'>点击我</span>
</p >
</div>1
2
3
4
5
6
7
8
9
10
11var div = document.getElementById('div');
var p = document.getElementById('p');
function onClickFn (event) {
var tagName = event.currentTarget.tagName;
var phase = event.eventPhase;
console.log(tagName, phase);
}
div.addEventListener('click', onClickFn, false);
p.addEventListener('click', onClickFn, false);1
2
3//点击后输出
P 3
DIV 3几个常见事件属性
event.currentTarget
,当前事件流所在的元素,是一个dom
对象。event.eventPhase
,代表当前执行阶段的整数值。1为捕获阶段、2为事件对象触发阶段、3为冒泡阶段。event.target
,代表目标元素,即触发事件的元素。
解绑方式:
传入的回调函数必须是
具名函数
,内容相同的两个匿名函数不会被认为相等。1
dom.removeEventListener(eventType, handler, useCapture)
特性:
可以在一个
DOM
元素上绑定多个事件处理器,各自并不会冲突1
2
3btn.addEventListener(‘click’, showMessage1, false);
btn.addEventListener(‘click’, showMessage2, false);
btn.addEventListener(‘click’, showMessage3, false);如果在
目标元素
上绑定了多个对同一事件
的监听,则捕获触发
对应的事件回调会先于冒泡触发
对应的事件回调被执行1
2
3
4
5
6document.querySelector('.box').addEventListener('click', (e) => {
console.log('我是第一个添加的监听')
},)
document.querySelector('.box').addEventListener('click', (e) => {
console.log('我是第二个添加的监听')
}, true)点击box,控制台输出的顺序是:
'我是第二个添加的监听'
'我是第一个添加的监听'
IE事件模型(基本不用,现在再vscode中都无法使用)
IE事件模型只有2个过程,没有事件捕获阶段:
- 事件处理阶段:事件到达目标元素, 触发目标元素的监听函数。
- 事件冒泡阶段:事件从目标元素冒泡到
document
, 依次检查经过的节点是否绑定了事件监听函数,如果有则执行。
绑定方式
1
dom.attachEvent(eventType, handler)
解绑方式
1
dom.detachEvent(eventType, handler)
举个例子:
1
2
3var btn = document.getElementById('.btn');
btn.attachEvent(‘onclick’, showMessage);
btn.detachEvent(‘onclick’, showMessage);
讲讲事件代理
事件代理也叫事件委托
,当我们要监听某个元素某个事件的时候,我们可以选择不给这个元素添加事件监听,而是给这个元素的父元素或者更外层元素
添加对该事件的监听。然后在事件冒泡
阶段触发该事件监听对应的回调函数。
事件代理的优点:
不必为每个
目标元素
绑定事件监听,减少了页面所需内存。自动绑定,解绑事件监听,减少了重复的工作。
事件代理的局限性:
focus
、blur
这些事件没有事件冒泡机制,所以无法进行委托绑定事件
说说你对事件循环的理解
同步与异步任务
首先,JavaScript
是一门单线程的语言,意味着同一时间内只能做一件事,这样就存在线程阻塞的问题,而解决阻塞的方法就是将任务划分为同步任务
和异步任务
- 同步任务:立即执行的任务,同步任务一般会直接进入到
主线程
中执行 - 异步任务:异步执行的任务,比如
ajax
网络请求,setTimeout
定时函数等,交给宿主环境
去执行,时机成熟后放入任务队列
中
微任务与宏任务
异步任务
又可以细分为微任务
和宏任务
,任务队列也被划分为微任务队列和宏任务队列。
什么是微任务,什么是宏任务?
宏任务是指时间粒度
比较大,执行的时间间隔是不能精确控制的任务,实时性不高,微任务则反之。常见的宏任务有setTimeout()
,常见的微任务有Promise.then()
在执行下一个宏任务之前,会先查看微任务队列中
是否有需要执行的微任务
,如果有则先把微任务执行完,再开启新的宏任务。
async与await
async
是异步的意思,await
则可以理解为等待
放到一起可以理解async
就是用来声明一个异步方法,而 await
是用来等待异步方法执行
async
async
函数返回一个promise
对象,下面两种方法是等效的
1 | function f() { |
await
正常情况下,await
命令后面是一个 Promise
对象,返回该对象的结果。如果不是 Promise
对象,就直接返回
对应的值
1 | async function f(){ |
不管await
后面跟着的是什么,await
都会阻塞后面的代码,后面的代码成为异步任务(如果阻塞的是是同步代码就成为微任务)。
1 | async function fn1 (){ |
上述输出结果为:1
,fn2
,3
,2
事件循环
宏任务
是事件循环的基本单位,一个宏任务中可以同时包含同步任务
,宏任务
,微任务
,事件循环指的是,js引擎先执行宏任务
中包含的同步任务
,再查找并执行微任务队列中的所有微任务,再查找宏任务队列,开启新的宏任务,重复上述过程。
1 | console.log("script start"); |
- 宏任务:执行整体代码(相当于
<script>
中的代码,整体是一个宏任务):- 输出:
script start
- 遇到 setTimeout,加入宏任务队列,当前宏任务队列(setTimeout)
- 遇到 promise,加入微任务,当前微任务队列(promise1)
- 输出:
script end
- 输出:
- 微任务:执行微任务队列(promise1)
- 输出:
promise1
,then 之后产生一个微任务,加入微任务队列,当前微任务队列(promise2) - 执行 then,输出
promise2
- 输出:
- 执行渲染操作,更新界面(敲黑板划重点)。
- 宏任务:执行 setTimeout
- 输出:
setTimeout
- 输出:
参考文章:程序员 - 一次搞懂-JS事件循环之宏任务和微任务 - 个人文章 - SegmentFault 思否
说说你对BOM的理解
是什么
BOM
(Browser Object Model),浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象
其作用就是跟浏览器
做一些交互效果,比如如何进行页面的后退
,前进
,刷新
,浏览器的窗口发生变化,滚动条的滚动,以及获取客户的一些信息如:浏览器品牌版本,屏幕分辨率
window
Bom
的核心对象是window
,它表示浏览器的一个实例,location
,navigator
等后续介绍的对象都是window
的属性。
在浏览器中,window
对象有双重角色,即是浏览器窗口的一个接口,又是全局对象
因此所有在全局作用域
中声明的变量
、函数
都会变成window
对象的属性
和方法
window.scrollTo(x,y)
:如果有滚动条,将横向滚动条移动到相对于窗体宽度为x个像素的位置,将纵向滚动条移动到相对于窗体高度为y个像素的位置window.scrollBy(x,y)
: 如果有滚动条,将横向滚动条
向左移动x个像素,将纵向滚动条
向下移动y个像素
window.open()
:window.open()
既可以导航到一个特定的url
,也可以打开一个新的浏览器窗口。window.open()
会返回新窗口的引用,也就是新窗口的window
对象,当使用window.open()
方法打开新窗口时,如果返回值是null
,这通常意味着浏览器阻止了该弹窗的创建。现代浏览器为了防止恶意网站滥用弹窗,通常会限制非用户交互
触发的弹窗。如果你在页面加载时或没有明确的用户动作(如点击事件)的情况下调用window.open()
,浏览器可能会认为这是未经请求的弹窗,并阻止它。新创建的
window
对象有一个opener
属性,该属性指向打开他的原始窗口对象
1
var newWindow = window.open(url, target, features[, replace]);
url (可选)
- 类型:
String
- 描述: 新窗口要加载的
URL
地址。如果省略或设置为null
,则会打开一个空白窗口。
target (可选)
- 类型:
String
- 描述: 指定新窗口的
目标位置
。它可以是以下预定义值之一:_self
: 在当前框架
中加载页面(默认行为)。_blank
: 在新的窗口
或标签页中加载页面。_parent
: 在父框架中加载页面。如果当前页面不在框架(iframe)内,则与_self
的行为相同,在当前标签页中加载新页面。_top
: 在整个窗口中加载页面,取消所有框架。- 或者是一个由开发者定义的名称,用来标识
特定的窗口
或<iframe>
。
features (可选)
- 类型:
String
- 描述: 一系列用
逗号分隔
的字符串
,用于指定新窗口的各种属性
和行为
。每个特征可以带有或不带参数width=600
: 设置窗口宽度为 600 像素。height=400
: 设置窗口高度为 400 像素
replace (可选)
- 类型:
Boolean
- 描述: 如果设置为
true
,则新加载的页面将替换历史记录中的当前条目
;如果为false
或未提供
,则会在历史记录中添加一个新条目
。这对于防止用户多次点击“后退”按钮返回到同一个页面非常有用。
- 类型:
window.close()
:仅用于关闭通过window.open()
打开的窗口如果尝试关闭一个
不同域名
下的窗口,可能会遇到跨域限制
。在这种情况下,window.close()
可能不会工作,因为浏览器的安全模型会阻止你操作不属于同一源的窗口。1
2const myWin = window.open('http://www.vue3js.cn','_blank')
myWin.close()
location
一个url
地址例子如下:
1 | http://www.wrox.com:80/WileyCDA/?q=javascript#contents |
location
属性描述如下:
属性名 | 例子 | 说明 |
---|---|---|
hash | “#contents” | url中,#后面的字符,没有则返回空串 |
host | www.wrox.com:80 | 服务器名称和端口号 |
hostname | www.wrox.com | 域名,不带端口号 |
href | http://www.wrox.com:80/WileyCDA/?q=javascript#contents | 完整url |
pathname | “/WileyCDA/“ | 服务器下面的文件路径 |
port | 80 | url的端口号,没有则为空 |
protocol | http: | 使用的协议 |
search | ?q=javascript | url的查询字符串,通常为?后面的内容 |
- 除了
hash
之外,只要修改location
的一个属性,就会导致页面重新加载新URL
location.reload()
,此方法可以重新刷新当前页面。这个方法会根据最有效的方式
刷新页面,如果页面自上一次请求以来没有改变过,页面就会从浏览器缓存
中重新加载,这一点和浏览器的缓存策略相关。如果要强制
从服务器中重新加载,传递一个参数true
即可。
navigator
navigator
对象主要用来获取浏览器的属性
,区分浏览器类型。属性较多,且兼容性比较复杂。
screen
保存的纯粹是客户端能力
信息,也就是浏览器窗口外面的客户端显示器
的信息,比如像素宽度和像素高度。
history
history是window对象的一个属性,它本身也是个对象,提供了许多api,主要用来操作浏览器URL
的历史记录,允许我们编程式控制页面被在历史记录之间跳转,也允许我们修改历史记录。
API | 作用 |
---|---|
history.back() | 跳转到前一个页面,如果没有前一个页面,则不做响应,不会改变history.length |
history.forward() | 跳转到后一个页面,如果当前就最新页面,则不做响应,不会改变history.length |
history.go() | 传入数字,正数 表示前进几个页面,负数 表示后退几个页面,0 表示刷新页面,不会改变history.length |
history.length | 获取当前窗口页面历史记录跳数,它是一个只读属性,无法直接修改。 |
history.pushState() | 往历史记录栈顶添加一条记录,历史记录条数加1,但是不会跳转页面。在当前页面调用这个api,你能明显的看到url改变了,但是页面没有跳转。接收三个参数,历史记录对象(state),页面标题,URL路径。 |
history.replaceState() | 不会增加历史记录数目,会修改当前历史记录 |
history.state | 访问当前页面的状态对象。 |
history.scrollRestoraion | 如果值为auto,则在前进或者后退的时候,滚动条会回到原来的位置。如果值为manual,则不会恢复。 |
DOM常见的操作有哪些
DOM是什么
浏览器
根据html
标签生成的js对象
,所有的标签属性
都可以在上面找到(所以说node中没有dom),修改这个对象属性会自动映射
到标签上。
DOM常见的操作
- 创建节点
- 获取结点
- 更新节点
- 添加节点
- 删除节点
创建节点
createElement
创建元素结点
1 | const divEl = document.createElement("div"); |
createTextNode
创建文本结点
1 | const textEl = document.createTextNode("content"); |
createAttribute
创建属性节点,可以是自定义属性
1 | const dataAttribute = document.createAttribute('custom'); |
获取节点
querySelector
传入任何有效的css
选择器,即获得首个
符合条件的Dom元素:
1 | document.querySelector('.element') |
如果页面上没有指定的元素时,返回 null
querySelectorAll
传入任何有效的css
选择器,返回一个伪数组
,包含全部符合匹配条件的DOM元素。
1 | const notLive = document.querySelectorAll("p"); |
其他方法
1 | document.getElementById('id属性值');返回拥有指定id的对象的引用 |
除此之外,每个DOM
元素还有parentNode
、childNodes
、firstChild
、lastChild
、nextSibling
、previousSibling
属性,关系图如下图所示。
更新结点
innerHTML
不但可以修改一个DOM
节点的文本内容
,如果传入的是html片段,还会被解析成dom结点。
1 | // 获取<p id="p">...</p > |
innerText、textContent
自动对字符串进行HTML
编码,保证无法设置任何HTML
标签
1 | // 获取<p id="p-id">...</p > |
两者的区别在于读取属性时,innerText
不返回隐藏元素
的文本(即 display: none
或者 visibility: hidden
),而textContent
返回所有文本
1 | <div id="example"> |
添加结点
appendChild
把一个节点添加到父节点的最后一个子节点之后,如果这个添加的结点已经在页面中存在,那么这个结点会先从原位置删除。
1 | <p id="js">JavaScript</p > |
添加一个p
元素
1 | const js = document.getElementById('js') |
在HTML
结构变成了下面
1 | <div id="list"> |
insertBefore
1 | parentElement.insertBefore(newElement, referenceElement) |
子节点会插入到referenceElement
之前
setAttribute
在指定元素中添加一个属性
节点,如果元素中已有该属性改变属性值
1 | const div = document.getElementById('id') |
删除结点
删除一个节点,首先要获得该节点本身
以及它的父节点
,然后,调用父节点的removeChild
把自己删掉
1 | // 拿到待删除节点: |
删除后的节点虽然不在文档树
中了,但其实它还在内存中
,可以随时再次被添加到别的位置。
如何实现触底加载,下拉刷新?
要明白如何实现功能,我们首先要搞清楚dom元素的一些定位,宽高属性
client
clientWidth/clientHeight
:可视区域的宽/高+内边距,不包含border
scroll
scrollWidth/scrollHeight
:有滚动条元素的元素整体的宽高。
举个例子,我们在一个高度为600px
的盒子box
里放两个背景颜色不同,高度都是400px
的盒子box1
,box2
,并给box
添加css属性
1 | overflow:auto |
1 | 补充一下overflow属性的值 |
这样box
盒子就出现了滚条,可以实现内容的滚动,内部盒子也不会影响外部盒子的布局(开启了BFC
,可以观察添加该条属性前后,body高度的变化,从800px变为600px)。然后我们访问box盒子(有滚动条的盒子)的clientHeight
属性和scrollHeight
属性
1 | box.clientHeight //600px |
这样是不是就很容易理解client和scroll之间的区别呢。对于没有滚动条的元素,clientWidth/clientHeight与scrollWidth/scrollHeight的值是一一相等的。
当我们不断地给body
添加元素,body
的高度总有超过浏览器窗口高度的时候,此时body
标签的父元素,html
标签会自动开启滚动条,html.clientHeight
就是浏览器窗口的高度。
scroll
scroll开头的属性中,还有两个重要的属性。
scrollLeft/scrollTop:表示具有滚动条的元素,顶部滚动出
可视区域
的高度,或者左部滚动出可视区域
的宽度,对于不具有滚动条的元素,这两个属性的值都是0
。这两个属性是可读写的,将元素的scrollLeft
和scrollTop
设置为 0,可以重置元素的滚动位置。
常见的属性中除了以client,scroll开头的属性,还有以offset
开头的属性
offset
offsetWidth/offsetHeight
:可视区域的宽/高+内边距+border+滚动条,这两个属性通常被拿来与clientWidth/clientHeight
属性比较,都是可视区域的宽高,不过范围有所不同。offsetLeft/offsetHeight
:元素左部/顶部距离最近的定位元素的距离,相对的不是视口,通常是固定的,不会随滚动条改变而改变。
如何实现触底加载
方法1
如果html
元素顶部滚出可视区域的高度+html元素的可视区域高度大于html标签的整体高度,则判定为触底加载。
1 | if (html.scrollTop + html.clientHeight >= html.scrollHeight) { |
优点:实现起来非常简单。
缺点:只能判断最后一个元素是否触底,不能判断非底部元素是否触底(如果body很长,那么非底部元素也是有触底事件的)
方法2
使用Intersection Observer API
1 | const func = (entries, observer) => { |
优点:能精确控制某个元素是否触底,令threshold
的值为0,还能实现图片懒加载的效果,即图片一出现在视口,就发送请求获取图片。
缺点:需要调用api实现起来麻烦。
如何实现下拉刷新
监听window
的touchstart
,touchmove
,touchend
,通过e.touches[0].pageY
获得触碰位置。touchend
事件触发后,计算移动的距离,判断是否需要刷新数据。
1 | let start |
如何判断一个元素是否在可视区域中?
在日常开发中,我们经常需要判断目标元素是否在视窗之内或者和视窗的距离小于一个值(例如 100 px),从而实现一些常用的功能,例如:
- 图片的懒加载
- 列表的无限滚动
- 计算广告元素的曝光情况
- 可点击链接的预加载
实现方式
借助dom的布局属性
当一个元素的
html
标签的scrollTop
属性,加上视口的高度
,大于等于一个元素的offsetTop
属性,那么这个元素就出现在视口中。如何获取时候的高度呢?有三种方式:window.innerHeight
document.documentElement.clientHeight
:html标签
的高度就可以认为是视口的高度
document.body.clientHeight
:body
标签的高度通常等于html标签
的高度
,所以也可以被认为是视口的高度。
1
el.offsetTop - document.documentElement.scrollTop <= viewPortHeight
1
2
3
4
5
6
7
8function isInViewPortOfOne (el) {
// viewPortHeight 兼容所有浏览器写法
const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
const offsetTop = el.offsetTop//元素距离html标签顶部的距离,前提html确实是最近的定位元素
const html = document.documentElement
const scrollTop = html.scrollTop
return offsetTop - scrollTop <= viewPortHeight
}IntersectionObserver
Intersection(交叉,交集) Observer
即,重叠观察者,从这个命名就可以看出它用于判断两个元素是否重叠
,因为不用进行scroll
事件的监听,性能方面相比getBoundingClientRect
会好很多。使用步骤主要分为两步:
创建观察者
和传入被观察者
创建观察者
1
2
3
4
5
6
7
8const options = {
// 表示重叠面积占被观察者的比例,从 0 - 1 取值,
// 1 表示完全被包含
threshold: 1.0,
root:document.querySelector('#scrollArea') // 必须是目标元素的父级元素,如果省略,浏览器的视口(viewport)作为根容器
};
const callback = (entries, observer) => { ....}
const observer = new IntersectionObserver(callback, options);通过
new IntersectionObserver
创建了观察者observer
,传入的参数callback
在重叠比例超过threshold
时会被执行关于
callback
回调函数常用属性如下:1
2
3
4
5
6
7
8
9
10// 上段代码中被省略的 callback
const callback = function(entries, observer) {
//entries是一个数组,包含了所有被观察的对象
entries.forEach(entry => {
entry.isIntersecting // 布尔值,表示两元素是否重叠
entry.time; // 触发的时间
entry.intersectionRatio; // 重叠区域占被观察者面积的比例(被观察者不是矩形时也按照矩形计算)
entry.target; // 被观察者
});
};传入被观察者
通过
observer.observe(target)
这一行代码即可简单的注册被观察者1
2const target = document.querySelector('.target');
observer.observe(target);
new操作符到底做了什么
- 创建一个新的对象
- 让这个对象的
[[prototype]]
属性等于构造函数的prototype
,即让新创建的对象的原型等于构造函数的原型对象。 - 调用这个对象的
constructor
方法
如果构造函数的返回值是基本类型,那么这个返回值不起任何效果,但是如果构造函数的返回值是引用类型,new操作返回的对象就是构造函数返回的对象。
1 | function Parent() { |
1 | function Parent() { |
手写new
1 | function Parent(name,age) { |
如何实现继承
继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码
在子类别继承父类别的同时,可以重新定义
某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。
如果大家学过java
,想必对继承的概念都非常熟悉了。
那在js这门语言中是如何实现继承呢?
原型链继承
让父类的一个
实例
作为子类的原型对象
,这样子类的原型对象的原型确实是父类的原型对象1
2
3
4
5
6
7
8function Parent() {
this.name = 'parent1';
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child';
}
Child.prototype = new Parent();Child.prototype._proto_ = Parent.prototype
,在原型链上确实是符合继承关系,但是这也只是在原型链上实现了继承,Child.prototype.constructor
也不指向Child
的构造函数。正确的情况,
Child
的constructor
应该是Child.prototype
自己的属性(ownProperty)正确的状态,比如Parent:
构造函数继承(借助 call)
1
2
3
4
5
6
7
8
9
10
11function Parent(){
this.name = 'parent1';
}
Parent.prototype.getName = function () {
return this.name;
}
function Child(){
Parent1.call(this);
this.type = 'child'
}
let child = new Child();构造函数继承也只是实现了构造函数上的继承,比原型链继承还低智,就纯粹在
Child
的构造函数中调用了Parent
的构造函数,并让this
指向Child构造函数内部的this,在这个例子中,Parent1.call(this)
完全可以被替换为this.name = 'parent1';
这种继承方式唯一的作用,拿这个例子来讲,就是把父类Parent
的name属性终于变成子类Child实例
自己的属性了(对象本身就有的,而不是原型对象上的,可以通过hasOwnProperty
方法来判断)。组合继承
组合继承就是把前面两种方式,即原型链继承和构造函数继承,这两种不完美的方法结合了起来,并更正了
Child
原型对象的指向。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19function 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;
console.log(new Child())可以看到
Child.prototype.constructor
指向是正确的,创建的Child
实例也有自己的name
和play
属性。寄生组合式继承
是对组合式继承的优化,不再使用父类(
Parent
)的实例
作为子类(Child
)的原型对象(Prototype
),而是使用Object.create()
方法单独为子类创造一个原型对象。Object.create()
能以传入的对象为对象原型
,创造一个新的对象。示例:Object.create(Parent.prototype)
以Parent的原型对象为对象原型,创造一个新的对象,意思就是创造的对象的
_proto_
属性=Parent.prototype
就好像创建了一个Parent实例,所以创建的对象显示的类型也是Parent
,不过这个对象没有自己的属性。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24function 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;
}
let person = new Child();
console.log(person); //{friends:"child",name:"parent",play:[1,2,3],__proto__:Parent}
console.log(person.getName()); // parent
console.log(person.getFriends()); // child
extends
使用
extends
关键字实现继承,基于es6
新引入的class
,本质上使用的也是寄生组合式继承。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Person {
//构造函数
constructor(name) {
this.name = name
}
// 原型方法,会被挂载到构造函数的原型上
// 即 Person.prototype.getName = function() { }
getName() {
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() // 成功访问到父类的方法
Javascript本地存储的方式有哪些?区别及应用场景?
javaScript
本地缓存的方法我们主要讲述以下四种:
- cookie
- sessionStorage
- localStorage
- indexedDB
其中sessionStorage
和localStorage
都是H5
新增的。
Cookie
是什么
Cookie
是存储在客户端的小型文本文件
,被用来解决 HTTP
无状态导致的问题,。
作为一段一般不超过 4KB
的小型文本数据(4KB 的大小限制主要针对单个 Cookie),它由一个名称(Name)、一个值(Value)和其它几个用于控制
cookie
有效期、安全性、使用范围的可选属性组成。
但是cookie
在每次请求中都会被发送,如果不使用 HTTPS
并对其加密,其保存的信息很容易被窃取,导致安全风险。举个例子,在一些使用 cookie
保持登录态的网站上,如果 cookie
被窃取,他人很容易利用你的 cookie
来假扮成你登录网站。
关于cookie
常用的属性如下:
Expires
用于设置 Cookie 的过期时间
1
Expires=Wed, 21 Oct 2015 07:28:00 GMT
Max-Age
用于设置在 Cookie 的有效时间(优先级比
Expires
高,书写方式也比Expires友好)1
Max-Age=604800 //单位是s
Domain
指定了
Cookie
在那些域名下生效,或者说在哪些域名下才会自动发送。添加cookie的时候,不指定Domain默认就是
当前域名
;指定了一个域名,则其子域名也总会被包含;比如在
https://www.bilibili.com/
页面下添加一个cookie但是未指定Domain,则Domain就是www.bilibili.com
如果指定Domain为
bilibili.com
则实际为.bilibili.com
,表示在bilibili.com
所有子域名下这个cookie也生效。Path
指定了一个
URL
路径,只有包含这个路径的请求才能携带这个cookieSecure
标记为
Secure
的Cookie
只能通过HTTPS
请求发送给服务器。HttpOnly
标记为HttpOnly的请求只能通过http协议来操作。
操作方式
通过js操作cookie
获取当前页面所有cookie
1
document.cookie
返回一个字符串,包含当前页面的所有cookie的键值对,形如:
key=val;key2=val2;......;keys=vals
如果要查看当前页面的全部cookie的详细信息,可以选择
检查页面
,前往应用程序->存储->cookie
中查看。创建一个cookie
1
document.cookie = 'key=val;Max-age=3600;Domain=www.sanye.blog' //可以继续添加其他限制属性
我们知道,document.cookie,返回一个字符串,包含当前页面的所有cookie的键值对。上述创建cookie的代码的效果貌似是覆盖掉这个字符串,其实不是的,效果真的是添加一个cookie。
修改cookie
关于
cookie
的修改,首先要确定domain
和path
属性都是相同的才可以(这两个属性可以理解为用来限制cookie的作用域的),其中有一个不同的时候都会创建出一个新的cookie
,而不是修改原来的cookie1
document.cookie = 'name=bb; domain=aa.net; path=/'
删除cookie
最常用的方法就是给
cookie
设置一个过期的事件,这样cookie
过期后会自动被浏览器删除。1
document.cookie = "id=1;Max-age=0"
通过http操作cookie
添加cookie
http通过在响应头中添加
Set-Cookie
字段在客户端种cookie,如果有多个 Cookie 就在响应头中设置多个Set-Cookie
字段。通过http操作的cookie的方式与js操作cookie的方式在形式上是不同的,但本质上还是相同的。
1
Set-Cookie: <cookie-name>=<cookie-value>; [Expires=<date>]; [Max-Age=<non-zero-digit>]; [Domain=<domain-value>]; [Path=<path-value>]; [Secure]; [HttpOnly]; [SameSite=<none|lax|strict>]
更新或者删除cookie
要更新现有的Cookie,只需再次发送带有
相同名称
的新Set-Cookie
头。这将覆盖旧的同名Cookie。要删除一个Cookie,可以通过设置其Expires
或Max-Age
为过去的时间戳来实现
浏览器行为
- 浏览器会在每次请求时自动附加与目标URL相匹配的所有Cookies。
- 如果某个Cookie被标记为
HttpOnly
,那么JavaScript代码不能读取或修改这个Cookie,增加了安全性。 - 当一个Cookie过期后,浏览器会自动将其从存储中移除,不再随请求一起发送。
localStorage
HTML5
新方法,IE8及以上浏览器都兼容
特点
持久化的本地存储,除非主动删除数据,否则数据永不过期
存储的信息在
同一域
中是共享的,这个同一域包括子域名
,也就是说若两个域名即便只有子域名不同,也不会被认为是同一域名。子域名指的是主域名(二级域名+顶级域名)之前的部分,比如
www.sanye.blog
中的www
就是子域名,sanye.blog
就是主域名,一般域名购买,购买的就是主域名。当本页操作(新增、修改、删除)了
localStorage
中的数据的时候,本页面不会触发storage
事件,但是别的页面会触发storage
事件,这里的其他页面指的是同源
的其他页面。大小:5M(跟浏览器厂商有关系),
localStorage
的大小限制主要指的是 整个域名下所有存储数据的总和,而不是单个键值对的大小。localStorage
本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡受同源策略的限制
storage事件补充
假设你有两个标签页(Tab A 和 Tab B)打开了同一个网站 example.com
,并且这两个标签页都在使用 localStorage
来存储一些数据。
Tab A 中执行以下 JavaScript 代码来设置一个新的
localStorage
项:1
localStorage.setItem('key', 'value');
Tab A 不会触发
storage
事件,因为它就是触发这次变更的操作源。Tab B 中监听
storage
事件,并打印出事件详情:1
2
3
4
5
6
7window.addEventListener('storage', function(event) {
console.log('Storage event received:', event);
console.log('Key:', event.key);
console.log('Old value:', event.oldValue);
console.log('New value:', event.newValue);
console.log('URL:', event.url);
});当你在 Tab A 中设置了
localStorage
后,Tab B 会立即接收到storage
事件,并输出类似如下的信息:1
2
3
4
5Storage event received: StorageEvent {…}
Key: key
Old value: null
New value: value
URL: https://example.com/如果你在 Tab B 中也设置了相同的
localStorage
项,比如:1
localStorage.setItem('key', 'newValue');
Tab B 自身不会触发
storage
事件,但 Tab A 会接收到storage
事件,并显示相应的更新信息。
常见使用语法
设置
1 | localStorage.setItem('username','cfangxu'); |
获取
1 | localStorage.getItem('username') |
获取键名
1 | localStorage.key(0) //获取第一个键名 |
删除
1 | localStorage.removeItem('username') |
一次性清除所有存储
1 | localStorage.clear() |
localStorage
也不是完美的,它有两个缺点:
- 无法像
Cookie
一样设置过期时间 - 只能存入
字符串
,无法直接存对象,如果尝试存储一个对象,它会自动调用该对象的toString()
方法,这通常会导致数据丢失或无法正确恢复原始对象,存入对象之前必须先序列化
。Cookies 同样只能存储字符串
。这意味着如果你想要存储对象或其他复杂的数据结构,也需要进行序列化和反序列化操作。
和cookie的区别
localStorage
无法像Cookie
一样设置过期时间
,数据在本地的存储是持久化
的,除非主动删除数据,否则数据永不过期
。localStorage
中的数据必须手动存取
,而cookie
中的数据是自动存取
的localStorage
严格遵循同源策略,同源页面才能共享同一份localStorage
中的数据,虽然 Cookies 也默认遵循同源策略,但可以通过特定的设置来实现跨子域的数据共享
。- 二者都有存储大小的限制,但是
cookie
的存储大小限制是4kB,localStorage
的存储大小限制一般是5MB,明显更大。
sessionStorage
sessionStorage
和 localStorage
使用方法基本一致,唯一不同的是生命周期
,一旦页面(会话)关闭,sessionStorage
将会删除数据。
前端扩展存储方式
虽然 Web Storage
对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用。IndexedDB
提供了一个解决方案。
indexedDB
是一种低级API,用于客户端存储大量结构化数据(包括, 文件/ blobs)。该API使用索引(index)
来实现对该数据的高性能搜索。
优点
- 储存量理论上没有上限
- 所有操作都是
异步
的,相比LocalStorage
同步操作性能更高,尤其是数据量较大时 - 原生支持储存
JS
的对象 - 是个正经的数据库,意味着数据库能干的事它都能干
缺点
- 操作非常繁琐
- 本身有一定门槛
关于indexedDB
的使用基本使用步骤如下:
- 打开数据库并且开始一个事务
- 创建一个
object store
- 构建一个请求来执行一些数据库操作,像增加或提取数据等。
- 通过监听正确类型的
DOM
事件以等待操作完成。 - 在操作结果上进行一些操作(可以在
request
对象中找到)
关于使用indexdb
的使用会比较繁琐,大家可以通过使用Godb.js
库进行缓存,最大化的降低操作难度
应用场景
- 标记用户与跟踪用户行为的情况,推荐使用
cookie
- 适合长期保存在本地的数据(令牌),推荐使用
localStorage
- 敏感账号一次性登录,推荐使用
sessionStorage
- 存储大量数据的情况、在线文档(富文本编辑器)保存编辑历史的情况,推荐使用
indexedDB
ajax
定义
是一种创建交互式网页应用的开发技术, 可以在不重新加载整个网页的情况下,与服务器交换数据,并且局部更新网页。
Ajax
的原理简单来说就是通过XmlHttpRequest(xhr)
对象向服务器发送异步请求,收到服务器响应的数据后,用Js
操作DOM
来更新页面。
实现过程
创建
Ajax
的核心对象XMLHttpRequest
对象1
const xhr = new XMLHttpRequest();
通过
XMLHttpRequest
对象的open()
方法与服务端建立连接1
xhr.open(method, url, [async][, user][, password]) #与服务器建立连接
method
:表示当前的请求方式,常见的有GET
、POST
url
:服务端地址async
:布尔值,表示是否异步执行操作,默认为true
user
: 可选的用户名用于认证用途;默认为null
password
: 可选的密码用于认证用途,默认为null
构建请求所需的
数据内容
,并通过XMLHttpRequest
对象的send()
方法发送给服务器端1
xhr.send([body])//如果请求体中不需要携带数据,什么都不要传入
通过
XMLHttpRequest
对象提供的onreadystatechange
事件(即监听(on)准备状态(readystate)改变(change)
)监听服务器端的通信状态。关于
XMLHttpRequest.readyState
属性有五个状态,用数字来区分,只要readyState
属性值一变化,就会触发一次readystatechange
事件。- 0(unsent):
open
方法还未调用,连接还未建立。 - 1(opened):
open
方法调用了,但是还未发送请求(还未调用send
方法) - 2(headers_recieved):请求发送了,
响应头
和响应状态
已经接收到了,但是还未开始下载。 - 3(loading):
响应体
下载中 - 4(done):响应体下载完毕,请求完成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const request = new XMLHttpRequest()
//request的三个常用属性
//request.readyState 查看请求的状态
//request.status 响应状态码
//request.responseText 响应文本
request.onreadystatechange = function(e){
if(request.readyState === 4){ // 整个请求过程完毕
if(request.status >= 200 && request.status <= 300){
console.log(request.responseText) // 服务端返回的结果
}else if(request.status >=400){
console.log("错误信息:" + request.status)
}
}
}
//用于初始化一个 HTTP 请求。这个方法并不发送请求,而是为后续的 request.send() 调用做准备
request.open('POST','http://xxxx')
request.send()- 0(unsent):
接受并处理服务端向客户端响应的数据结果
将处理结果更新到
HTML
页面中
axios
定义
axios
是一个基于promise
的网络请求库,在浏览器端借助XHR
,在node.js
中借助http
模块
有如下特点:
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换
JSON
数据 - 客户端支持防御
XSRF
开始使用
在浏览器中可以通过script
标签直接引入
1 | <script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
在node模块化开发环境中,可以通过npm包的形式下载,需要使用的时候再导入
1 | npm install axios --S |
1 | import axios from 'axios' |
常见用法
在线文档关于axios
的介绍说实话没有让人看下去的动力,这里写点自己的东西。
创建axios实例
创建出来的实例具有与axios一样的功能,这样就相当于为每个请求都配置了相同的
基地址
,超时时间
,响应头
,这不就起到了封装
的作用吗?创建axios实例
的js代码我们都写在index.js
文件中。1
2
3
4
5
6
7
8import axios from 'axios'
const instance = axios.create({
baseURL: 'https://smart-shop.itheima.net/index.php?s=/api',
timeout: 10000,
headers: {
'platform': 'H5',
}
})给axios实例添加响应拦截器,请求拦截器。
请求,响应拦截器返回(return)的数据,其实就是axios请求返回的数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24instance.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
// 只要有token,就在请求时携带,便于请求需要授权的接口
// 每次请求都会获取token,也就是说token每次都是现用现取的,如果删除了就取不到了
const token = store.getters.token
if (token) {
config.headers['Access-Token'] = token
}
return config
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error)
})
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 2xx 范围内的状态码(response.status)都会触发该函数。
// 对响应数据做点什么
return response.data //默认会被包装成resolved类型的promise对象
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error) //如果直接返回error,也会被包装成resolved类型的promise对象
})取消请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// 方式一
// axios.CancelToken是一个构造函数,用来获得取消令牌源对象。
// source是一个取消令牌源对象。这个对象包含了两个重要的属性:
// token: 这是一个实际的取消令牌。你可以将这个令牌传递给 Axios 请求配置中的 cancelToken 属性,从而使得该请求可以被取消。
// cancel: 这是一个函数,调用它可以取消所有关联了 source.token 的请求。
// 你可以选择性地提供一个消息参数,这个消息会作为取消原因包含在取消事件中。
const source = axios.CancelToken.source();
axios.get('xxxx', {
cancelToken: source.token
})
// 取消请求 (请求原因是可选的)
// 调用source.cancel('取消原因') 时,它会将关联的 cancelToken 标记为已取消状态,并记录提供的取消原因(如 '取消原因')
// 这个操作不会直接发送网络请求,而是改变了令牌的状态。
source.cancel('主动取消请求');
// 方式二,个人感觉更为复杂,但本质都是一样的
const CancelToken = axios.CancelToken;
let cancel;
axios.get('xxxx', {
cancelToken: new CancelToken(function executor(c) {
cancel = c;
})
});
cancel('主动取消请求');总结
想要取消请求,需要先使用
axios.CancelToken
成一个取消令牌源对象source
,然后再请求中配置cancelToken
属性的值为source.token
,当想要取消请求的时候,就调用source.cancel()
方法,传入取消请求的原因。调用
source.cancel('取消原因') 时
,它会将关联的cancelToken
标记为已取消状态,并记录提供的取消原因(如 ‘取消原因’)
这个操作不会直接发送网络请求,而是改变了令牌的状态。1
2
3
4const CancelToken = axios.CancelToken;
//获得取消请求源对象
const source = CancelToken.source();
console.log(source.token)//输出token,结构如下1
2
3
4const CancelToken = axios.CancelToken;
const source = CancelToken.source();
source.cancel('主动取消')
console.log(source.token)Axios 的
响应拦截器
会检查每个正在处理的请求是否关联了被标记为已取消的cancelToken
。如果匹配,则立即停止该请求的进一步处理。导出配置好的axios实例
1
export default instance
使用axios发送请求
我们通常把同一业务功能的
api
放到一个js文件中,比如和购物车cart
相关的接口都放在cart.js
文件中,在这些文件中引入导出的axios实例来发送请求。1
import request from 'index.js'
发送请求有两种常用写法
request({})
这种写法是直接传入一个
配置对象
,请求方法(method)
等所有信息都包含在内,我们需要对大部分配置属性都熟悉1
2
3
4
5
6
7request({
url:
method:'post',
params:
data:
headers:
})request.method()
这种写法是把
请求方法
提取到外面,然后传入多个参数来实现的。第一个参数指定请求的 URL
第二个参数,如果是
get/delete
等请求就是除了请求体
外的配置对象
,即不包括data属性的配置对象。如果是put/post
请求,则是data
,即请求体
数据对象第三个参数,只有
put/post
请求可能需要配置第三个参数,即不包括data
属性的配置对象。总结
1
2
3
4axios(config) // 直接传入配置
axios(url[, config]) // 传入url和配置
axios[method](url[, option]) // 直接调用请求方式方法,传入url和配置
axios[method](url[, data[, option]]) // 直接调用请求方式方法,传入data、url和配置
和
接口文档
的对应关系path:需要在url中直接配置,嵌入在url的
资源路径
中1
/users/{userId} ---> /users/123
query:在配置对象的
params
属性中配置,会被放到url的?
之后,并且多个参数之间用与号&
分隔1
{name:"tom",age:18} ---> /users?name=tom&age=18
body:即请求体,在
data
属性中配置header:在配置对象的
headers
属性中配置
响应结果结构分析
响应错误对象的response
属性则有与响应成功对象一样
的结构
实现一个简易版的axios
构建一个Axios
构造函数,核心代码为request
1 | class Axios { |
1 | // 导出request方法 |
上述代码就已经能够实现axios({})
这种方式的请求
下面是来实现下axios.method()
这种形式的请求
1 | const methodsArr = ['get', 'delete', 'head', 'options', 'put', 'patch', 'post']; |
至此,这些方法与request
方法一样,都挂载到Axios.prototype
上,虽然这些方法本质是在调用request
方法,但是它们还是属于同级别
的关系。所以也不能通过导出的axios(本质是request方法)
来访问,使用这些新挂载的方法。
所以我们可以尝试把这些方法挂载
到request
函数上,函数上能挂载方法,因为函数本质也是个对象。
如果我们直接把新定义的get
等方法挂载到request
方法上,当get
函数是被request调用,函数内部的this
指向就是request
函数了,这是我们不想看到的,应该指向的是axios
实例
首先实现个工具类
,实现将b
方法混入到a
,并且修改this
指向
1 | const utils = { |
修改导出的方法
1 | function CreateAxiosFn() { |
使用
1 | const axios = CreateAxiosFn() |