JavaScript

说说js的数据类型

js的数据类型可以分为两类,基本数据类型引用数据类型

基本数据类型

基本数据类型主要有6种:Number,String,Boolean,Symbol,Null,Undefined

  • number

    最常见的整数类型格式则为十进制,还可以设置八进制(零开头)、十六进制(0x开头)

    1
    2
    3
    let intNum = 55 // 10进制的55
    let num1 = 070 // 8进制的56
    let hexNum1 = 0xA //16进制的10

    浮点类型则在数值中必须包含小数点,还可通过科学计数法表示。

    1
    2
    3
    4
    let floatNum1 = 1.1;
    let floatNum2 = 0.1;
    let floatNum3 = .1; // 有效,但不推荐
    let floatNum = 3.125e7; // 等于 31250000

    在数值类型中,存在一个特殊数值NaN,意为“不是数值”,用于表示数值运算操作失败了(而不是抛出错误

    1
    2
    console.log(0/0); // NaN
    console.log(-0/+0); // NaN
  • string

    字符串使用双引号(”)、单引号(’)或反引号(`)表示都可以。

    在js中,字符串是不可变的,意思是一旦创建,它们的值就不能变了

    1
    2
    3
    4
    5
    6
    7
    let lang = "Java";//这行代码会在内存中创建一个包含 "Java" 的字符串对象,并将引用赋值给变量 lang。

    //从内存中读取 lang 当前所指向的字符串 "Java"。
    //将 "Java" 和 "Script" 拼接成新的字符串 "JavaScript",并在内存中创建一个新的字符串对象。
    //将 lang 变量存储的引用更新为新创建的字符串 "JavaScript"的引用
    lang = lang + "Script";
    console.log(lang)
  • Boolean

    Boolean(布尔值)类型有两个字面值: truefalse

    通过Boolean可以将其他类型的数据转化成布尔值:

    1
    2
    3
    4
    5
    数据类型                    转换为 true 的值                    转换为 false 的值
    String 非空字符串 ""
    Number 非零数值(包括无穷值) 0NaN
    Object 任意对象 null
    Undefined N/A (不存在) undefined

    注意:在js中负数转化成布尔值也是true

  • Symbol

    Symbol关键字的主要用途是用来创造一个唯一的标识符,用作对象属性,确保不会产生属性冲突

    1
    2
    3
    4
    5
    6
    7
    let genericSymbol = Symbol();
    let otherGenericSymbol = Symbol();
    console.log(genericSymbol == otherGenericSymbol); // false
    //传入符号主要为了标识,符号相同并不代表值也相同
    let fooSymbol = Symbol('foo');
    let otherFooSymbol = Symbol('foo');
    console.log(fooSymbol == otherFooSymbol); // false
  • Null

    Null类型同样只有一个值,即特殊值 null

    逻辑上讲, null 值表示一个空对象,这也是给typeof传一个 null 会返回 "object" 的原因。

    1
    2
    let car = null;
    console.log(typeof car); // "object"
  • Undefined

    Undefined 类型只有一个值,就是特殊值 undefined,如果一个变量声明了但是未被赋值,那么这个变量的值就是undefined。

    1
    2
    3
    let message; // 这个变量被声明了,只是值为 undefined
    console.log(message); // "undefined"
    console.log(age); // 没有声明过这个变量,报错

引用数据类型

引用数据类型统称为Object

主要包括以下三种:

  • Object

    通常使用字面量表示法来创建对象,这样创建的对象是Object构造函数的实例。

    1
    2
    3
    4
    5
    let person = {
    name: "Nicholas",
    "age": 29,
    5: true
    };
  • Array

    js数组是一组有序的数据,但跟其他语言不同的是,数组中每个槽位可以存储任意类型的数据。并且,数组也是动态大小的,会随着数据添加而自动增长。通常通过字面量表示法创建数组。

    1
    2
    let colors = ["red", 2, {age: 20 }]
    colors.push(2)

    或者通过Array来创建数组

    1
    2
    3
    const arr = new Array(4)//创建一个长度为4的数组,虽然创建的时候指定了长度,但是长度还是可以变化的
    arr.fill(0) //数组中的每个元素初始值为undefined,我们把它初始化为1
    arr.map(i=>new Array(4).fill(0))//把数组中的每个元素替换为数组,实现二维数组的创建。
  • Function

    函数实际上是对象,每个函数都是 Function类型的实例,而 Function也有属性和方法,跟其他引用类型一样。

  • 其他类型

    除了上述说的三种之外,还包括DateRegExpMapSet等,他们都是Object类型的子类

typeof和instanceof

typeof

typeof 操作符返回一个字符串,表示值的数据类型。

1
2
typeof operand
typeof(operand)

这两种使用方法都是可以的。下面是一些例子。

1
2
3
4
5
6
7
8
9
10
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof [] // 'object'
typeof {} // 'object'
typeof console // 'object'
typeof console.log // 'function'

instanceof

主要用来判断某个构造函数是否在某个实例对象的原型链上。

1
object instanceof constructor

区别

typeof返回的是字符串,instanceof返回的是布尔值`

typeof能判断基本数据的类型,但是不能准确判断引用数据的类型。

intanceof能准确判断引用数据的类型, 但是不能判断基本数据的类型

可以看到,上述两种方法都有弊端,并不能满足所有场景的需求

Object.prototype.toString()

还有一种通用的判断方式**Object.prototype.toString()**,简单来说就是Object原型对象上挂载的toString方法。

1
2
3
4
5
6
Object.prototype.toString({})       // "[object Object]"
Object.prototype.toString.call({}) // 同上结果,加上call也ok
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('1') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(function(){}) // "[object Function]"

可以看到返回的结果是一个字符串,第一位都是object,这与js中万物皆对象的思想符合。所有类型的数据都能调用toString()方法,如果是基本数据类型,会先进行数据装箱,转化成对象,再调用这个方法。Object是所有对象的父类(Object在所有对象的原型链上),所以所有对象都能访问到这个方法,那为什么不直接让数据调用这个方法呢?因为对象的原型链上可能还存在同名方法。使用函数.call(对象)的方式,能确保对象调用的就是指定的函数/方法。

谈谈 JavaScript 中的类型转换机制

前面我们讲到,JS中有六种简单数据类型:undefinednullbooleanstringnumbersymbol,以及引用类型:object

常见的类型转换有:

  • 强制转换(显示转换)
  • 自动转换(隐式转换)

显示转换

显示转换,即我们很清楚可以看到这里发生了类型的转变,常见的方法有:

  • Number()
  • parseInt()
  • String()
  • Boolean()

Number()

将任意类型的值转化为数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Number(324) // 324
// 字符串:如果可以被解析为数值,则转换为相应的数值
Number('324') // 324
// 字符串:如果不可以被解析为数值,返回 NaN
Number('324abc') // NaN
// 空字符串转为0,空数组也转换成0
Number('') // 0
// 布尔值:true 转成 1,false 转成 0
Number(true) // 1
Number(false) // 0
// undefined:转成 NaN
Number(undefined) // NaN
// null:转成0
Number(null) // 0
//对象:通常转换成NaN(除了只包含单个数值的数组)
Number({a: 1}) // NaN
Number([1, 2, 3]) // NaN
Number([5]) // 5
Number([]) // 0

从上面可以看到,Number转换的时候是很严格的,只要有一个字符无法转成数值,整个字符串就会被转为NaN

parseInt()

parseInt相比Number,就没那么严格了,parseInt函数逐个解析字符,遇到不能转换的字符就停下来

1
parseInt('32a3') //32

String()

可以将任意类型的值转化成字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
// 数值:转为相应的字符串
String(1) // "1"
//字符串:转换后还是原来的值
String("a") // "a"
//布尔值:true转为字符串"true",false转为字符串"false"
String(true) // "true"
//undefined:转为字符串"undefined"
String(undefined) // "undefined"
//null:转为字符串"null"
String(null) // "null"
//对象
String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"

可以看到,对于基本数据类型,强制转化成字符串,就是加个双引号就好了,而应用数据类型就不一样了,需要调用toString方法

Boolean()

可以将任意类型的值转为布尔值,转换规则如下:

1
2
3
4
5
6
7
8
Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false
Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // 因为返回的是一个对象Boolean {false},所以转化成布尔值是true

隐式转换

在隐式转换中,我们可能最大的疑惑是 :何时发生隐式转换

我们这里可以归纳为两种情况发生隐式转换的场景:

  • 比较运算(==!=><
  • 算术运算(+-*/%
  • ifwhile需要布尔值地方

除了上面的场景,还要求运算符两边的操作数不是同一类型

  • 自动转化成布尔值

    在需要布尔值的地方,就会将非布尔值的参数自动转为布尔值,系统内部会调用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

    在非严格比较中,undefinednull,只与undefined或者null相等。

  • NaN == NaN 返回false

    NaN和任何数比较,包括本身,都返回false。

  • 两个都为简单数据类型字符串布尔值都会转换成数值,再比较。

  • 如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf()方法取得其原始值,再根据前面的规则进行比较。

  • 两个都为引用类型,则比较它们是否指向同一个对象,也就是比较地址是否相同。

Javascript 数字精度丢失的问题

为什么会出现精度丢失

对于某些小数,计算机无法用有限的二进制位精确的表示,比如0.1用二进制表示思路如下:

1
2
3
4
0.1*2=0.2<1  --0
0.2*2=0*4<1 --0
0.4*2=0.8<1 --0
0.8*2=1.6>1 --1

假设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
2
3
4
5
0.6*2=1.2>1  --1
0.2*2=0.4<1 --0
0.4*2=0.8<1 --0
0.8*2=1.6>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.jsBigDecimal.js,通过调用相关方法来模拟加减乘除运算。

说说 JavaScript 中内存泄漏的几种情况

如何理解内存泄漏

内存泄漏(Memory leak)指的是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存

对于持续运行的进程,必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃

垃圾自动回收机制

C语言中,因为是手动管理内存,内存泄露是经常出现的事情。

这很麻烦,所以大多数语言提供自动内存管理,减轻程序员的负担,这被称为”垃圾自动回收机制”

js也有垃圾自动回收机制。

原理垃圾收集器会定期(周期性)找出那些不再继续使用的变量,然后释放其内存。

通常情况下有两种实现方式,用来判断哪些变量不再使用:

  • 标记清除
  • 引用计数

标记清除

清除那些被标记的变量,释放它们的内存,是JavaScript最常用的垃圾收回机制,

当变量进入执行环境是,就标记这个变量为“进入环境“。进入环境的变量所占用的内存就不能释放,当变量离开环境时,则将其标记为“离开环境“

垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉

在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了

随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存

1
2
3
4
5
6
7
8
var m = 0,n = 19 // 把 m,n,add() 标记为进入环境。
add(m, n) // 把 a, b, c标记为进入环境。
console.log(n) // a,b,c标记为离开环境,等待垃圾回收。
function add(a, b) {
a++
var c = a + b
return c
}

引用计数

语言引擎有一张”引用表”,保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。

如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏。

1
2
const arr = [1, 2, 3, 4];
console.log('hello world');

上面代码中,数组[1, 2, 3, 4]是一个值,会占用内存。变量arr是仅有的对这个值的引用,因此引用次数为1。尽管后面的代码没有用到arr,它还是会持续占用内存

如果需要这块内存被垃圾回收机制释放,只需要设置如下:

1
arr = null

通过设置arrnull,就解除了对数组[1,2,3,4]的引用,引用次数变为 0,就被垃圾回收了。

注意

有了垃圾自动回收机制,并不代表不用担心内存泄漏问题,对于那些占用内存很大的变量,确保它们不再被使用的时候,不存才对它们的引用。

常见内存泄漏情况

意外的全局变量

1
2
3
function foo(arg) {
bar = "this is a hidden global variable";
}

给一个未声明的标识符赋值,JavaScript 引擎会认为你在引用一个已经存在的全局变量;如果找不到这个变量,则会自动在全局对象(浏览器环境中为 window,Node.js 环境中为 global)上创建它。

1
2
3
4
5
function foo() {
this.variable = "potential accidental global";
}
// foo 调用自己,this 指向了全局对象(window)
foo();

上述使用严格模式,可以避免意外的全局变量。

定时器

定时器开启后,除非显式的清除,否则将一直存在,如果定时器中引用了不再使用的变量,又未及时清除定时器,就会造成内存泄漏。

1
2
3
4
5
6
7
8
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// 处理 node 和 someResource
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);

如果id为Node的元素从DOM中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放。

闭包

1
2
3
4
5
6
7
8
9
10
function bindEvent() {
var obj = document.createElement('XXX');
var unused = function () {
console.log(obj, '闭包内引用obj obj不会被释放');
};
return unused
}
const func = bindEvent()
//解决方法,清除引用
func = null

说说你对闭包的理解?闭包使用场景

是什么

闭包由一个内部函数和它引用的外部函数的作用域组成。

使用场景

  • 创建私有变量
  • 延长变量的生命周期

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//立即执行函数
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();

说说你对防抖和节流的理解

是什么

本质上是优化高频率执行代码造成的性能损耗的一种手段。

如:浏览器的 resizescrollkeypressmousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能。

为了优化体验,我们需要限制这类事件的调用次数,对此我们就可以采用 防抖(debounce)节流(throttle) 的方式来减少调用频率

防抖

定义

事件被触发后,且在n秒内不再触发该事件,则执行对应的回调函数,如果在n秒内再次触发该事件,则重新开始计时,可以用操作系统中的资源被剥夺来理解,这里的资源就是定时器

手写防抖函数

1
2
3
<body>
<input type="text">
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//传入一个函数,返回一个实现了防抖的函数
function myDebounce(func, wait) {
let timer = null
//形成了一个闭包,内部函数引用了外部函数的变量timer,func,wait
return function (e) {
if (timer) {
//抢夺资源,清除定时器
clearTimeout(timer)
}
//开启新的定时器
timer = setTimeout(() => {
//修改this指向,传入e
func.call(this, e)
timer = null//运行完后释放资源
}, wait)
}
}
function func(e) {
console.log(e.target.value)
}
document.querySelector('[type=text]').addEventListener('input', myDebounce(func, 500))

节流

定义

在n秒内无论触发多少次事件,只执行第一次触发对应的回调函数,可以用操作系统中的资源不可被剥夺来理解,这里的资源就是定时器

手写节流

1
2
3
<body>
<button>click</button>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
document.querySelector('button').addEventListener('click', myThrottle(func, 1000))
function func() {
console.log(1)
console.log(this)
}
//传入一个函数,返回一个实现了节流的函数
function myThrottle(func, wait) {
//声明一个定时器
let timer = null
//返回一个新的函数,这个函数引用了func,wait和timer,构成闭包
return function (...args) {
if (timer) {
//如果定时器已经开启直接返回
return
}
timer = setTimeout(() => {
//虽然实际调用的是返回的新函数,但是在函数内部还是调用了传入的func函数,而且我们使用apply模拟了直接调用
func.apply(this, args)
timer = null//释放资源
}, wait)
}
}

区别与联系

相同点:

  • 都可以通过使用 setTimeout 实现
  • 目的都是,降低回调执行频率。节省计算资源

不同点:

  • 函数防抖,在一段连续操作结束后,只执行最后一次触发对应的回调。函数节流,在一段连续操作中,每一段时间只执行一次,在频率较高的事件中被使用来提高性能。
  • 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次。

应用场景

防抖在连续的事件,只需触发一次回调的场景有:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求
  • 手机号、邮箱验证输入检测
  • 窗口大小resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。

节流在间隔一段时间执行一次回调的场景有:

  • 滚动加载,加载更多或滚到底部监听
  • 搜索框,搜索联想功能

数组的常用方法

我们可以从增删查改,是否会修改原数组这几个角度来给数组的常用方法归类

  • push():可以传入任意个数的元素,这些元素会被添加到数组的末尾,返回新数组的长度,会修改原数组。

    1
    2
    3
    let colors = []; // 创建一个数组
    let count = colors.push("red", "green"); // 推入两项
    console.log(count) // 2
  • unshift():也是可以传入任意个数的元素,这些元素会被添加到数组的首部,返回新数组的长度,会修改原数组。

    1
    2
    3
    let colors = new Array(); // 创建一个数组
    let count = colors.unshift("red", "green"); // 从数组开头推入两项
    console.log(count); // 2
  • splice():第一个参数传入开始位置,第二个参数传入0,表示不删除元素,后续参数传入插入的元素。

    1
    2
    3
    4
    let 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
    4
    let 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
    4
    let colors = ["red", "green"]
    let item = colors.pop(); // 取得最后一项
    console.log(item) // green
    console.log(colors.length) // 1
  • shift():法用于删除数组的第一项,同时减少数组的length 值,返回被删除的项

    1
    2
    3
    4
    let colors = ["red", "green"]
    let item = colors.shift(); // 取得第一项
    console.log(item) // red
    console.log(colors.length) // 1
  • splice():第一个参数传入开始位置,第二个参数传入要删除元素的个数,返回包含被删除元素的数组。

    1
    2
    3
    4
    let 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
    6
    let 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
2
3
4
let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 1, "red", "purple"); // 插入两个值,删除一个元素
console.log(colors); // red,red,purple,blue
console.log(removed); // green,只有一个元素的数组

一般也是通过下标来查找数组元素。

  • indexOf():传入一个元素,返回数组中第一个与该元素相等的元素,使用的是严格比较,如果没有则返回-1,NaN不与任何数相等,所以indexOf(NaN)返回值必定为-1

    1
    2
    3
    let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1,NaN];
    numbers.indexOf(4) // 3
    console.log(numbers.indexOf(NaN)) // -1
  • includes():判断某个元素是否再数组中存在,也是严格比较,存在返回true,否则返回false,对NaN做了特殊处理,能判断是它否存在。

    1
    2
    3
    let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1,NaN];
    numbers.includes(4) //true
    numbers.includes(NaN) //返回true
  • find():传入一个返回值是布尔类型的回调函数,用于判断满足某个条件的元素是否存在,通常用于对象数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const 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
    3
    let values = [1, 2, 3, 4, 5];
    values.reverse();
    alert(values); // 5,4,3,2,1
  • sort():给数组排序,sort()方法接受一个比较函数,用于判断哪个值应该排在前面

    1
    2
    3
    4
    5
    6
    7
    8
    function 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
2
3
let colors = ["red", "green", "blue"];
alert(colors.join(",")); // red,green,blue
alert(colors.join("||")); // red||green||blue

迭代方法

  • some():传入一个返回值为布尔值的回调函数,如果数组中的有个元素传入该回调函数能使返回值为true,则该方法返回true,否则返回false。

  • every():传入一个返回值为布尔值的回调函数,如果数组中的每个元素传入该回调函数能使返回值为true,则该方法返回true,否则返回false。

  • forEach():遍历数组中的每个元素并执行一定操作,可以修改原数组。

    1
    2
    3
    4
    let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
    numbers.forEach((item, index, array) => {
    // 执行某些操作
    });
  • filter():传入一个返回值为布尔值的回调函数,返回一个包含所有能让这个回调函数返回值为true的数组。

    1
    2
    3
    let 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,3
  • map():根据传入的回调函数修改数组中的每一个元素并返回一个新的数组。

    1
    2
    3
    let 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
2
3
4
let stringValue = "hello ";
let result = stringValue.concat("world");
console.log(result); // "hello world"
console.log(stringValue); // "hello"

slice()/substr()/substring()

作用是返回字符串的切片

1
2
3
4
5
6
7
8
let stringValue = "hello world";
console.log(stringValue.slice(3)); // "lo world"
console.log(stringValue.substring(3)); // "lo world"
console.log(stringValue.slice(3, 7)); // "lo w"
console.log(stringValue.substring(3,7)); // "lo w"

console.log(stringValue.substr(3)); // "lo world"
console.log(stringValue.substr(3, 7)); // "lo worl"

可以看出slice()substring()的用法是一致的,当传入两个参数的时候,分别表示的是截取的左右区间(左闭右开),而substr()传入两个参数时,第一个表示参数起始位置,第二个参数表示的是要截取的元素的个数

当只传入一个参数,三者的效果是相同的。

indexOf()/startWith()/includes()

indexOf:从字符串开头去搜索传入的字符串,并返回位置(如果没找到,则返回 -1 )

1
2
let stringValue = "hello world";
console.log(stringValue.indexOf("o")); // 4

startWith():判断字符串是否以某个字符串开头,返回值为布尔类型。

includes():判断字符串中是否包含某个字符串,返回值是布尔类型。

1
2
3
4
5
let message = "foobarbaz";
console.log(message.startsWith("foo")); // true
console.log(message.startsWith("bar")); // false
console.log(message.includes("foo")); // true
console.log(message.includes("bar")); // true

由此可见,无论是数组还是字符串中,都有indexOf和includes方法

字符串拆分

把字符串按照指定的分割符,拆分成数组中的每一项

1
2
3
let str = "12+23+34"
let arr = str.split("+") // [12,23,34]
let arr2 = str.split("") //['1', '2', '+', '2', '3', '+', '3', '4']

模板匹配

  • match()

    接收一个参数,可以是一个正则表达式字符串,也可以是一个RegExp对象,返回数组

    1
    2
    3
    4
    let text = "cat, bat, sat, fat";
    let pattern = /.at/;
    let matches = text.match(pattern);
    console.log(matches[0]); // "cat"
  • search()

  • replace()

深拷贝浅拷贝

当我们拷贝一个基本类型的数据,拷贝的就是它的,此时没有深浅拷贝一说,只有当我们拷贝一个对象的时候,才有深浅拷贝的说法。

浅拷贝

浅拷贝顾名思义,就是浅层次的拷贝,只拷贝一层。当我们要拷贝一个对象的时候,对于这个对象的所有属性,如果属性的值是基本数据类型,那我们直接拷贝,如果属性值为引用数据类型,则拷贝地址。示例如下:

1
2
3
4
5
6
7
8
9
10
function shallowClone(obj) {
const newObj = {};
for(let prop in obj) {
//只拷贝obj自身的属性
if(obj.hasOwnProperty(prop)){
newObj[prop] = obj[prop];
}
}
return newObj;
}

浅拷贝常见方法

  • Object.assign

    把某个对象的所有可枚举属性拷贝到另一个对象上。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let 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
    5
    const 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
    8
    const _ = 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()

    但是这种方式存在弊端,会忽略值为undefinedsymbol函数的属性,因为这些值都是不可被序列化的,不会出现在序列化的字符串中。

    1
    2
    3
    4
    5
    6
    7
    8
    const 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
    2
    var 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
    11
    var 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
      3
      btn.addEventListener(‘click’, showMessage1, false);
      btn.addEventListener(‘click’, showMessage2, false);
      btn.addEventListener(‘click’, showMessage3, false);

      如果在目标元素上绑定了多个对同一事件的监听,则捕获触发对应的事件回调会先于冒泡触发对应的事件回调被执行

      1
      2
      3
      4
      5
      6
      document.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
    3
    var btn = document.getElementById('.btn');
    btn.attachEvent(‘onclick’, showMessage);
    btn.detachEvent(‘onclick’, showMessage);

讲讲事件代理

事件代理也叫事件委托,当我们要监听某个元素某个事件的时候,我们可以选择不给这个元素添加事件监听,而是给这个元素的父元素或者更外层元素添加对该事件的监听。然后在事件冒泡阶段触发该事件监听对应的回调函数。

事件代理的优点:

  • 不必为每个目标元素绑定事件监听,减少了页面所需内存

  • 自动绑定,解绑事件监听,减少了重复的工作。

事件代理的局限性:

  • focusblur这些事件没有事件冒泡机制,所以无法进行委托绑定事件

说说你对事件循环的理解

同步与异步任务

首先,JavaScript是一门单线程的语言,意味着同一时间内只能做一件事,这样就存在线程阻塞的问题,而解决阻塞的方法就是将任务划分为同步任务异步任务

  • 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行
  • 异步任务:异步执行的任务,比如ajax网络请求,setTimeout定时函数等,交给宿主环境去执行,时机成熟后放入任务队列

微任务与宏任务

异步任务又可以细分微任务宏任务,任务队列也被划分为微任务队列和宏任务队列。

什么是微任务,什么是宏任务?

宏任务是指时间粒度比较大,执行的时间间隔是不能精确控制的任务,实时性不高,微任务则反之。常见的宏任务有setTimeout(),常见的微任务有Promise.then()

在执行下一个宏任务之前,会先查看微任务队列中是否有需要执行的微任务,如果有则先把微任务执行完,再开启新的宏任务。

async与await

async 是异步的意思,await 则可以理解为等待

放到一起可以理解async就是用来声明一个异步方法,而 await 是用来等待异步方法执行

async

async函数返回一个promise对象,下面两种方法是等效的

1
2
3
4
5
6
7
8
function f() {
return Promise.resolve('TEST');
}

//会自动包装成resolved类型的promise对象
async function asyncF() {
return 'TEST';
}

await

正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值

1
2
3
4
5
async function f(){
//等同于return 123
return await 123
}
f().then(v => console.log(v)) // 直接输出123

不管await后面跟着的是什么,await都会阻塞后面的代码,后面的代码成为异步任务(如果阻塞的是是同步代码就成为微任务)。

1
2
3
4
5
6
7
8
9
10
async function fn1 (){
console.log(1)
await fn2()
console.log(2) // 阻塞
}
async function fn2 (){
console.log('fn2')
}
fn1()
console.log(3)

上述输出结果为:1fn232

事件循环

宏任务是事件循环的基本单位,一个宏任务中可以同时包含同步任务宏任务微任务,事件循环指的是,js引擎先执行宏任务中包含的同步任务,再查找并执行微任务队列中的所有微任务,再查找宏任务队列,开启新的宏任务,重复上述过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
console.log("script start");

setTimeout(function () {
console.log("setTimeout");
}, 0);

Promise.resolve()
.then(function () {
console.log("promise1");
})
.then(function () {
console.log("promise2");
});

console.log("script end");
  1. 宏任务:执行整体代码(相当于<script>中的代码,整体是一个宏任务):
    1. 输出: script start
    2. 遇到 setTimeout,加入宏任务队列,当前宏任务队列(setTimeout)
    3. 遇到 promise,加入微任务,当前微任务队列(promise1)
    4. 输出:script end
  2. 微任务:执行微任务队列(promise1)
    1. 输出:promise1,then 之后产生一个微任务,加入微任务队列,当前微任务队列(promise2)
    2. 执行 then,输出promise2
  3. 执行渲染操作,更新界面(敲黑板划重点)。
  4. 宏任务:执行 setTimeout
    1. 输出:setTimeout

参考文章:程序员 - 一次搞懂-JS事件循环之宏任务和微任务 - 个人文章 - SegmentFault 思否

说说你对BOM的理解

是什么

BOM (Browser Object Model),浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象

其作用就是跟浏览器做一些交互效果,比如如何进行页面的后退前进刷新,浏览器的窗口发生变化,滚动条的滚动,以及获取客户的一些信息如:浏览器品牌版本,屏幕分辨率

window

Bom的核心对象是window,它表示浏览器的一个实例,locationnavigator等后续介绍的对象都是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
    2
    const 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中,#后面的字符,没有则返回空串
hostwww.wrox.com:80服务器名称和端口号
hostnamewww.wrox.com域名,不带端口号
hrefhttp://www.wrox.com:80/WileyCDA/?q=javascript#contents完整url
pathname“/WileyCDA/“服务器下面的文件路径
port80url的端口号,没有则为空
protocolhttp:使用的协议
search?q=javascripturl的查询字符串,通常为?后面的内容
  • 除了 hash之外,只要修改location的一个属性,就会导致页面重新加载新URL
  • location.reload(),此方法可以重新刷新当前页面。这个方法会根据最有效的方式刷新页面,如果页面自上一次请求以来没有改变过,页面就会从浏览器缓存中重新加载,这一点和浏览器的缓存策略相关。如果要强制从服务器中重新加载,传递一个参数true即可。

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
2
const dataAttribute = document.createAttribute('custom');
consle.log(dataAttribute);

获取节点

querySelector

传入任何有效的css 选择器,即获得首个符合条件的Dom元素:

1
2
3
4
5
document.querySelector('.element')
document.querySelector('#element')
document.querySelector('div')
document.querySelector('[name="username"]')
document.querySelector('div + p > span')

如果页面上没有指定的元素时,返回 null

querySelectorAll

传入任何有效的css 选择器,返回一个伪数组,包含全部符合匹配条件的DOM元素。

1
const notLive = document.querySelectorAll("p");

其他方法

1
2
3
4
5
6
7
8
9
document.getElementById('id属性值');返回拥有指定id的对象的引用
document.getElementsByClassName('class属性值');返回拥有指定class的对象集合
document.getElementsByTagName('标签名');返回拥有指定标签名的对象集合
document.getElementsByName('name属性值'); 返回拥有指定名称的对象结合
document/element.querySelector('CSS选择器'); 仅返回第一个匹配的元素
document/element.querySelectorAll('CSS选择器'); 返回所有匹配的元素
document.documentElement; 获取页面中的HTML标签
document.body; 获取页面中的BODY标签
document.all['']; 获取页面中的所有元素节点的对象集合型

除此之外,每个DOM元素还有parentNodechildNodesfirstChildlastChildnextSiblingpreviousSibling属性,关系图如下图所示。

更新结点

innerHTML

不但可以修改一个DOM节点的文本内容,如果传入的是html片段,还会被解析成dom结点。

1
2
3
4
5
6
// 获取<p id="p">...</p >
var p = document.getElementById('p');
// 设置文本为abc:
p.innerHTML = 'ABC'; // <p id="p">ABC</p >
// 设置HTML:
p.innerHTML = 'ABC <span style="color:red">RED</span> XYZ';

innerText、textContent

自动对字符串进行HTML编码,保证无法设置任何HTML标签

1
2
3
4
5
6
// 获取<p id="p-id">...</p >
var p = document.getElementById('p-id');
// 设置文本:
p.innerText = '<script>alert("Hi")</script>';
// HTML被自动编码,无法设置一个<script>节点:
// <p id="p-id">&lt;script&gt;alert("Hi")&lt;/script&gt;</p >

两者的区别在于读取属性时,innerText不返回隐藏元素的文本(即 display: none 或者 visibility: hidden),而textContent返回所有文本

1
2
3
4
5
6
7
8
9
10
<div id="example">
<p>可见文本</p>
<p style="display: none;">隐藏文本</p>
</div>

<script>
const element = document.getElementById('example');
console.log(element.innerText); // 输出: "可见文本"
console.log(element.textContent); // 输出: "可见文本\n隐藏文本"
</script>

添加结点

appendChild

把一个节点添加到父节点的最后一个子节点之后,如果这个添加的结点已经在页面中存在,那么这个结点会先从原位置删除。

1
2
3
4
5
6
<p id="js">JavaScript</p >
<div id="list">
<p id="java">Java</p >
<p id="python">Python</p >
<p id="scheme">Scheme</p >
</div>

添加一个p元素

1
2
3
4
const js = document.getElementById('js')
js.innerHTML = "JavaScript"
const list = document.getElementById('list');
list.appendChild(js);

HTML结构变成了下面

1
2
3
4
5
6
<div id="list">
<p id="java">Java</p >
<p id="python">Python</p >
<p id="scheme">Scheme</p >
<p id="js">JavaScript</p > <!-- 添加元素 -->
</div>

insertBefore

1
parentElement.insertBefore(newElement, referenceElement)

子节点会插入到referenceElement之前

setAttribute

在指定元素中添加一个属性节点,如果元素中已有该属性改变属性值

1
2
const div = document.getElementById('id')
div.setAttribute('class', 'white');//第一个参数属性名,第二个参数属性值。

删除结点

删除一个节点,首先要获得该节点本身以及它的父节点,然后,调用父节点的removeChild把自己删掉

1
2
3
4
5
6
7
// 拿到待删除节点:
const self = document.getElementById('to-be-removed');
// 拿到父节点:
const parent = self.parentElement;
// 删除,返回被删除的dom元素
const removed = parent.removeChild(self);
removed === self; // true

删除后的节点虽然不在文档树中了,但其实它还在内存中,可以随时再次被添加到别的位置。

如何实现触底加载,下拉刷新?

要明白如何实现功能,我们首先要搞清楚dom元素的一些定位,宽高属性

  • client

    clientWidth/clientHeight:可视区域的宽/高+内边距,不包含border

  • scroll

    scrollWidth/scrollHeight:有滚动条元素的元素整体的宽高。

举个例子,我们在一个高度为600px的盒子box里放两个背景颜色不同,高度都是400px的盒子box1,box2,并给box添加css属性

1
overflow:auto 
1
2
3
4
5
6
7
8
9
补充一下overflow属性的值
visible(默认值):内容不会被裁剪,而是会呈现在元素框之外。
hidden:内容会被裁剪,并且超出的部分不会显示。浏览器不会为溢出内容提供任何滚动机制。
scroll:即使内容并未溢出,也提供滚动条
auto:如果内容溢出了元素框,则浏览器会根据需要提供滚动条。如果内容没有溢出,则不显示滚动条

单个方向上的溢出控制
overflow-x:控制水平方向上的溢出。
overflow-y: 控制垂直方向上的溢出。

这样box盒子就出现了滚条,可以实现内容的滚动,内部盒子也不会影响外部盒子的布局(开启了BFC,可以观察添加该条属性前后,body高度的变化,从800px变为600px)。然后我们访问box盒子(有滚动条的盒子)的clientHeight属性和scrollHeight属性

1
2
box.clientHeight //600px
box.scrollHeight //800px

这样是不是就很容易理解client和scroll之间的区别呢。对于没有滚动条的元素,clientWidth/clientHeight与scrollWidth/scrollHeight的值是一一相等的。

当我们不断地给body添加元素,body的高度总有超过浏览器窗口高度的时候,此时body标签的父元素,html标签会自动开启滚动条,html.clientHeight就是浏览器窗口的高度。

  • scroll

    scroll开头的属性中,还有两个重要的属性。

    scrollLeft/scrollTop:表示具有滚动条的元素,顶部滚动出可视区域的高度,或者左部滚动出可视区域的宽度,对于不具有滚动条的元素,这两个属性的值都是0。这两个属性是可读写的,将元素的 scrollLeftscrollTop 设置为 0,可以重置元素的滚动位置。

常见的属性中除了以client,scroll开头的属性,还有以offset开头的属性

  • offset

    offsetWidth/offsetHeight:可视区域的宽/高+内边距+border+滚动条,这两个属性通常被拿来与clientWidth/clientHeight属性比较,都是可视区域的宽高,不过范围有所不同。

    offsetLeft/offsetHeight:元素左部/顶部距离最近的定位元素的距离,相对的不是视口,通常是固定的,不会随滚动条改变而改变。

如何实现触底加载

方法1

如果html元素顶部滚出可视区域的高度+html元素的可视区域高度大于html标签的整体高度,则判定为触底加载。

1
2
3
if (html.scrollTop + html.clientHeight >= html.scrollHeight) {
console.log('触底')
}

优点:实现起来非常简单。

缺点:只能判断最后一个元素是否触底,不能判断非底部元素是否触底(如果body很长,那么非底部元素也是有触底事件的)

方法2

使用Intersection Observer API

1
2
3
4
5
6
7
8
9
10
11
12
13
const func = (entries, observer) => {
//每观察一个元素,entries的大小就会+1
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('元素进入视口');
//entry.target:表示被观察的元素。
observer.unobserve(entry.target); // 一旦进入视口,停止观察
}
});
}
//第二个参数指明元素完全出现在视口再触发回调函数,符合触底的思想
const observer = new IntersectionObserver(func,{threshold:1});
observer.observe(target) //观察某个元素

优点:能精确控制某个元素是否触底,令threshold的值为0,还能实现图片懒加载的效果,即图片一出现在视口,就发送请求获取图片。

缺点:需要调用api实现起来麻烦。

如何实现下拉刷新

监听windowtouchstart,touchmove,touchend,通过e.touches[0].pageY获得触碰位置。touchend事件触发后,计算移动的距离,判断是否需要刷新数据。

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
27
28
29
30
31
32
33
34
35
36
37
let start
let distance = 0
let load
let tip
//在一次下拉操作中,这个回调函数只会执行一次
window.addEventListener('touchstart', function (e) {
//存储第一次触摸屏幕距离视口顶部的距离
start = e.touches[0].clientY
//还未开始加载
load = false
//还未提示正在下拉
tip = false
})
//在一次下拉操作中,这个回调函数会被执行多次
window.addEventListener('touchmove', function (e) {
//记录下拉的距离
distance = e.touches[0].clientY - start
//如果下拉的距离大于0且未提示过正在下拉刷新,则提示
if (distance > 0 && !tip) {
console.log('正在进行下拉刷新操作')
//本次下拉操作不再提示正在下拉刷新
tip = true
}
//如果下拉的距离超过设定的距离且未提示过松手释放,则提示
if (distance > 50 && !load) {
console.log('松手释放')
////本次下拉操作不再提示松手释放
load = true
}
})
//在一次下拉操作中,这个回调函数只会执行一次
window.addEventListener('touchend', function (e) {
//如果下拉的距离超过了指定距离,则松手后开始更新
if (load) {
console.log('正在进行更新操作')
}
})

如何判断一个元素是否在可视区域中?

在日常开发中,我们经常需要判断目标元素是否在视窗之内或者和视窗的距离小于一个值(例如 100 px),从而实现一些常用的功能,例如:

  • 图片的懒加载
  • 列表的无限滚动
  • 计算广告元素的曝光情况
  • 可点击链接的预加载

实现方式

  • 借助dom的布局属性

    当一个元素的html标签的scrollTop属性,加上视口的高度,大于等于一个元素的offsetTop属性,那么这个元素就出现在视口中。如何获取时候的高度呢?有三种方式:

    • window.innerHeight
    • document.documentElement.clientHeighthtml标签的高度就可以认为是视口的高度
    • document.body.clientHeightbody标签的高度通常等于html标签高度,所以也可以被认为是视口的高度。
    1
    el.offsetTop - document.documentElement.scrollTop <= viewPortHeight
    1
    2
    3
    4
    5
    6
    7
    8
    function 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
    8
    const 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
    2
    const target = document.querySelector('.target');
    observer.observe(target);

new操作符到底做了什么

  • 创建一个新的对象
  • 让这个对象的[[prototype]]属性等于构造函数的prototype,即让新创建的对象的原型等于构造函数的原型对象。
  • 调用这个对象的constructor方法

如果构造函数的返回值是基本类型,那么这个返回值不起任何效果,但是如果构造函数的返回值是引用类型,new操作返回的对象就是构造函数返回的对象。

1
2
3
4
5
6
function Parent() {
this.name = 'parent';
this.play = [1, 2, 3];
return 1
}
console.log(new Parent()) //Parent {name: 'parent', play: Array(3)}
1
2
3
4
5
6
function Parent() {
this.name = 'parent';
this.play = [1, 2, 3];
return [1,2,3]
}
console.log(new Parent()) //输出[1,2,3]

手写new

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Parent(name,age) {
this.name = name
this.age = age
}
function myNew(constructor, ...args) {
//方法1
// const obj = Object.create(constructor.prototype)
//方法2
const obj = {}
//新对象原型指向构造函数原型对象
obj.__proto__ = constructor.prototype
//让这个对象调用constructor方法,并传入参数
const res = obj.constructor(...args)
if (typeof res == 'object') {
return res
} else {
return obj
}
}
console.log(myNew(Parent,'tom',30)) //Parent {name: 'tom', age: 30}

如何实现继承

继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码

在子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。

如果大家学过java,想必对继承的概念都非常熟悉了。

那在js这门语言中是如何实现继承呢?

  • 原型链继承

    让父类的一个实例作为子类的原型对象,这样子类的原型对象的原型确实是父类的原型对象

    1
    2
    3
    4
    5
    6
    7
    8
    function 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的构造函数。

    正确的情况,Childconstructor应该是Child.prototype自己的属性(ownProperty)

    正确的状态,比如Parent:

  • 构造函数继承(借助 call)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function 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
    19
    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;
    console.log(new Child())

    可以看到 Child.prototype.constructor指向是正确的,创建的Child实例也有自己的nameplay属性。

  • 寄生组合式继承

    是对组合式继承的优化,不再使用父类(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
    24
    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;
    }

    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
    21
    class 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

其中sessionStoragelocalStorage都是H5新增的。

是什么

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路径,只有包含这个路径的请求才能携带这个cookie

  • Secure

    标记为 SecureCookie只能通过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的修改,首先要确定domainpath属性都是相同的才可以(这两个属性可以理解为用来限制cookie的作用域的),其中有一个不同的时候都会创建出一个新的cookie,而不是修改原来的cookie

      1
      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,可以通过设置其 ExpiresMax-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 来存储一些数据。

  1. Tab A 中执行以下 JavaScript 代码来设置一个新的 localStorage 项:

    1
    localStorage.setItem('key', 'value');
  2. Tab A 不会触发 storage 事件,因为它就是触发这次变更的操作源。

  3. Tab B 中监听 storage 事件,并打印出事件详情:

    1
    2
    3
    4
    5
    6
    7
    window.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);
    });
  4. 当你在 Tab A 中设置了 localStorage 后,Tab B 会立即接收到 storage 事件,并输出类似如下的信息:

    1
    2
    3
    4
    5
    Storage event received: StorageEvent {…}
    Key: key
    Old value: null
    New value: value
    URL: https://example.com/
  5. 如果你在 Tab B 中也设置了相同的 localStorage 项,比如:

    1
    localStorage.setItem('key', 'newValue');
  6. 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

sessionStoragelocalStorage使用方法基本一致,唯一不同的是生命周期,一旦页面(会话)关闭,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:表示当前的请求方式,常见的有GETPOST
    • 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
    17
    const 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()
  • 接受并处理服务端向客户端响应的数据结果

  • 将处理结果更新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
    8
    import 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
    24
    instance.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
    4
    const CancelToken = axios.CancelToken;
    //获得取消请求源对象
    const source = CancelToken.source();
    console.log(source.token)//输出token,结构如下

    1
    2
    3
    4
    const 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
      7
      request({
      url:
      method:'post',
      params:
      data:
      headers:
      })
    • request.method()

      这种写法是把请求方法提取到外面,然后传入多个参数来实现的。

      第一个参数指定请求的 URL

      第二个参数,如果是get/delete等请求就是除了请求体外的配置对象,即不包括data属性的配置对象。如果是put/post请求,则是data,即请求体数据对象

      第三个参数,只有put/post请求可能需要配置第三个参数,即不包括data属性的配置对象。

    • 总结

      1
      2
      3
      4
      axios(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
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
27
class Axios {
constructor() {
//因为axios实例并没有什么常用的属性,所以这里没有任何初始化代码
}
//核心方法request,会自动挂载到Axios.prototype上,传入配置对象,立即返回一个promise对象(状态为pending)
request(config) {
//axios方法会立即返回一个promise对象
return new Promise((resolve,reject) => {
//对象解构赋值,获取到请求url,method,data,并给这些属性赋予默认值
//实际请求携带的配置参数可能不止这么点
const {url = '', method = 'get', data = {}} = config;
// 发送ajax请求
const xhr = new XMLHttpRequest();

//用于初始化一个 HTTP 请求。这个方法并不发送请求
//第三个参数是一个布尔值,表示是否异步执行请求,默认为true,表示异步。
xhr.open(method, url, true);
xhr.onload = function() {
//当请求被响应,根据响应状态码,改变promise对象的状态
console.log(xhr.responseText)
resolve(xhr.responseText);
}
//发送请求并携带数据
xhr.send(data);
})
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 导出request方法
function CreateAxiosFn() {
let axios = new Axios();
//这样即便不通过axios实例调用request方法,this指向也是axios实例
let req = axios.request.bind(axios);
//function bind(axios,...arg){
// const f = Symbol()
// axios[f] = this//这里的this就是axios.request
// return function(...args2){
//this指向取决于返回的函数被如何调用,但是无论指向什么都不重要
//因为我们已经确保调用request函数的指向是axios
// axios[f](...arg,...args2)
// }
//}
//bind的作用就是返回一个新的函数,这个函数使用bind指定的对象来调用原函数,调用新函数传入的参数也能被原函数接受
return req;
}

}
// 得到最后的全局变量axios,此处的axios本质就是request方法
let axios = CreateAxiosFn();

上述代码就已经能够实现axios({})这种方式的请求

下面是来实现下axios.method()这种形式的请求

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
const methodsArr = ['get', 'delete', 'head', 'options', 'put', 'patch', 'post'];
//在Axios的prototype上挂载这些方法
//这种写法功能等同于直接在Axios类中一个个定义这些方法,不过更简洁。
methodsArr.forEach(method => {
Axios.prototype[method] = function() {
// 处理只可能传入2个参数的方法
if (['get', 'delete', 'head', 'options'].includes(method)) { // 2个参数(url[, config])
//此处的this指向axios实例,所以能调用request方法,同时也说明这些方法本质也是在调用request方法
//arguments 是一个类数组对象,它包含了传递给函数的所有参数
//arguments 对象允许你在不知道具体有多少个参数的情况下访问所有传递给函数的参数,即便函数没有声明形参
return this.request({
method,
url: arguments[0],
...(arguments[1] || {})//如果第二个参数没传入,arguments[1]的值就是undefined,然后展开一个空对象
})
} else { // 3个参数(url[,data[,config]])
return this.request({
method,
url: arguments[0],
data: arguments[1] || {},//arguments[1]是一个数据对象,不需要展开
...arguments[2] || {}//arguments[2]是剩余配置属性对象,需要展开
})
}
}
})

至此,这些方法与request方法一样,都挂载到Axios.prototype上,虽然这些方法本质是在调用request方法,但是它们还是属于同级别的关系。所以也不能通过导出的axios(本质是request方法)来访问,使用这些新挂载的方法。

所以我们可以尝试把这些方法挂载request函数上,函数上能挂载方法,因为函数本质也是个对象。

如果我们直接把新定义的get等方法挂载到request方法上,当get函数是被request调用,函数内部的this指向就是request函数了,这是我们不想看到的,应该指向的是axios实例

首先实现个工具类,实现将b方法混入到a,并且修改this指向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const utils = {
extend(a,b, context) {
//a就是request函数
//这里的b就是axios.prototype
for(let key in b) {
//如果key是b自己的属性,只混入Axios原型自己的属性
if (b.hasOwnProperty(key)) {
//如果属性值为函数,修改函数的指向再赋值给a
if (typeof b[key] === 'function') {
//b[key].bind(context)返回一个新的函数,函数内部使用context调用b[key]方法
//通过a[key]来调用b[key].bind(context)返回的函数,b[key].bind(context)返回的函数内部this指向的是a
//但是函数内部还是通过context来调用b[key]的,所以不会有问题。
a[key] = b[key].bind(context);
} else {
a[key] = b[key]
}
}
}
}
}

修改导出的方法

1
2
3
4
5
6
7
function CreateAxiosFn() {
let axios = new Axios();
let req = axios.request.bind(axios);
// 增加代码
utils.extend(req, Axios.prototype, axios)
return req;
}

使用

1
2
3
4
5
const axios = CreateAxiosFn()
//调用request方法上的get方法
//get方法的this指向确实是axios(即request函数)但是get方法内部是通过axios实例来调用挂载在Axios原型上的get方法
//也就是说这两个方法get是不一样的
axios.get(url)