目录
一、作用域
1.1 作用域
全局作用域
局部作用域
- 函数作用域 函数内部
- 块作用域 {}
作用域链
作用域链本质上是底层变量查找机制
在执行函数中优先查找当前函数作用域中的变量,如果找不到会逐级查找父级作用域直到全局作用域
1.2 垃圾回收机制
js中的内存的分配和回收都是自动完成,内存在不使用时会被垃圾回收器自动回收
不在用的内存,没有及时释放,就是内存泄露
内存的声明周期
- 内存声明
- 内存使用
- 内存回收
一般全局变量在页面关闭时才回收,局部变量不使用时自动回收
1.3 闭包
闭包 = 内层函数 + 外部函数的变量
作用:外部可以访问内部函数的变量
记录函数调用次数
不用闭包,全局变量容易被修改
let count = 0
function fn() {
count ++
console.log(`函数被调用${count}次`);
}
fn()
fn()
使用闭包,数据私有,外部无法修改
function fn() {
let count = 0
return function() {
count ++
console.log(`函数被调用${count}次`);
}
}
const result = fn()
result()
result()
闭包可能会引起内存泄露
1.4 变量提升
变量提升是js的缺点,它允许变量声明前被访问
<script>
// 访问变量 str
console.log(str + 'world!'); // undefinedworld!
// 声明变量 str
var str = 'hello ';
</script>
- 变量在未声明即被访问时会报语法错误
- 变量在声明之前即被访问,变量的值为 undefined
- let 声明的变量不存在变量提升,推荐使用 let
- 变量提升出现在相同作用域当中
- 实际开发中推荐先声明再访问变量
二、函数进阶
2.1 函数提升
js会把函数(除函数表达式)声明调到作用域的前面,可以在任意的位置调用函数。
但是函数表达式必须先声明和赋值,最后调用否则会报错
// 1.会把所有函数声明调用到作用域的前面
// 2.只会提升函数声明不会提升函数调用
// js中函数调用可以写任意位置(除函数表达式)
fn()
function fn() {
console.log('函数提升');
}
// 函数表达式必须先声明和赋值 后调用 否则会报错
fun()
var fun = function() {
console.log('函数表达式提升');
}
总结:
- 函数提升能够使函数的声明调用更加灵活
- 函数表达式不存在函数提升现象
- 函数提升出现在相同的作用域中
2.2 函数参数
当我们不确定传入几个实参的时候怎么办?
动态参数
arguments函数内部的伪数组变量,它包含了调用函数时传入的所有实参,可以通过for获取伪数组的参数
function getSum() {
// arguments 动态参数 只存在函数里
// 1. 是伪数组
console.log(arguments);
let sum = 0
for (let i = 0;i < arguments.length;i ++) {
sum += arguments[i]
}
console.log(sum);
}
getSum(1,2,3,4,5)
getSum(2,4,5,6,7,8,9)
剩余参数
在最末函数函数前使用...
获取剩余的实参,是个真数组,使用的使用不用...
。开发中提倡使用剩余参数
// 剩余运算符 开发中常用
function getSum(a,b,...arr) {
// 真数组
console.log(a);
console.log(b);
console.log(arr);
}
getSum(1,2)
getSum(2,4,5,6,7,8,9)
拓展:展开运算符也是...
可以将数组展开,常用于求数组最大值和合并数组
const arr1 = [1,2,3]
// 展开运行算符 可以展开数组
console.log(...arr1);
// ...arr === 1,2,3 但是实际输出是 1 2 3
// 1. 求数组的最大值
console.log(Math.max(...arr1));
// 2. 和并数组
const arr2 = [3,4,5]
const arr = [...arr1,...arr2]
console.log(arr);
展开运算符主要用于数组展开,平时用的很少
剩余运算符在函数内部使用
2.3 箭头函数
基础语法
箭头函数是匿名函数的简写形式,有五种常见语法
// 普通的匿名函数
const fn = function () {
console.log(123);
}
语法一:箭头函数的基本语法
const fn = () => {
console.log(123);
}
fn()
语法二:只有一个形参的时候可以省略小括号
const fn = x => {
console.log(x);
}
fn('alice')
语法三:只有一行代码的时候可以省略大括号
const fn = x => console.log(x);
fn('bob')
语法四:只有一行代码的时候,可以用省略return
const fn = x => x+x
console.log(fn(1));
语法五:箭头函数可以直接返回一个对象
因为对象的大括号和函数大括号冲突,所以外部使用小括号包裹
const fn = (uname) => ({uname:uname}) // 第一个时属性第二个是形参传入的值
console.log(fn('alice'));
箭头函数参数
箭头函数没有arguments动态参数,是有...
剩余参数...args
箭头函数的this指向
箭头函数不会创建自己的this,它只会从自己的作用域链的上层沿用this
事件回调函数使用箭头函数时,this 为全局的 window,因此DOM事件回调函数为了简便,还是不太推荐使用箭头函数
2.4 解构赋值
数组解构
数组结构是快速批量赋值的简洁语法
基础语法:
const [min,middle,max] = [1,2,3]
console.log(max);
console.log(min);
数组解构的细节
- 变量多 ,单元值少 ,undefined
- 变量少 单元值多
- 剩余参数 变量少 单元值多
const [a,b,...c] = [1,2,3,4]
console.log(a);
console.log(b);
console.log(c); //[3,4] 真数组
- 防止undefined 传递
const [a = 0,b = 0] = [3,4]
// const [a = 0,b = 0] = []
console.log(a);
console.log(b);
- 按需导入赋值
const [a,b, , d] = [1,2,3,4]
console.log(a);
console.log(b);
console.log(d);
- 多重解构
// const [a,b,c] = [1,2,[3,4]]
// console.log(a); //1
// console.log(b); //2
// console.log(c); //[3,4]
const [a,[b,c]] = [1,[3,4]]
console.log(a); //1
console.log(b); //2
console.log(c); //[3,4]
js中有两种情况要加分号?
- 立即执行函数
- 数组结构
// 1. 立即执行函数要加
(function() {})();
// 2. 使用数组的时候 前面有代码使用数组的时候
const str = 'alice'
;[1,2,3].map(function(item) {
console.log(item);
})
let a = 1
let b = 2;
[b,a] = [a,b]
console.log(a,b);
对象解构
对象结构语法 属性名和变量被必须相同
变量名被占用 可以重新改名 旧变量名:新变量名
const uname = 'bob'
const {uname:username ,age} = {uname: 'alice',age:'18'}
console.log(username);
console.log(age);
解构数组对象
const pig = [
{
uname: '佩奇',
age: 6
}
]
const [{uname,age}] = pig
console.log(uname);
console.log(age);
多级对象解构
const pig = {
name: '佩奇',
family: {
mother: '猪妈',
father: '猪爸',
sister: '乔治'
},
age: 6
}
const {name,family:{mother,father,sister}} = pig
console.log(name);
console.log(mother);
console.log(father);
console.log(sister)
const person = [
{
name: '佩奇',
family: {
mother: '猪妈',
father: '猪爸',
sister: '乔治'
},
age: 6
}
]
const [{name,family:{mother,father,sister}}] = person
console.log(name);
console.log(mother);
console.log(father);
console.log(sister);
2.5 forEach语法
forEach
只遍历不返回值 是加强版的for循环,适合遍历数组对象
const arr = ['red','green','pink']
arr.forEach(function(item,index) {
console.log(item);
console.log(index);
})
三、深入对象
3.1 构造函数
构造函数是专门用于创建对象的函数,如果一个函数使用 new 关键字调用,那么这个函数就是构造函数。
<script>
// 定义函数
function foo() {
console.log('通过 new 也能调用函数...');
}
// 调用函数
new foo;
</script>
总结:
- 使用 new 关键字调用函数的行为被称为实例化
- 实例化构造函数时没有参数时可以省略 ()
- 构造函数的返回值即为新创建的对象
- 构造函数内部的 return 返回的值无效!
注:实践中为了从视觉上区分构造函数和普通函数,习惯将构造函数的首字母大写。
3.2 实例成员
通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员。
<script>
// 构造函数
function Person() {
// 构造函数内部的 this 就是实例对象
// 实例对象中动态添加属性
this.name = '小明'
// 实例对象动态添加方法
this.sayHi = function () {
console.log('大家好~')
}
}
// 实例化,p1 是实例对象
// p1 实际就是 构造函数内部的 this
const p1 = new Person()
console.log(p1)
console.log(p1.name) // 访问实例属性
p1.sayHi() // 调用实例方法
</script>
总结:
- 构造函数内部 this 实际上就是实例对象,为其动态添加的属性和方法即为实例成员
- 为构造函数传入参数,动态创建结构相同但值不同的对象
注:构造函数创建的实例对象彼此独立互不影响。
3.3 静态成员
在 JavaScript 中底层函数本质上也是对象类型,因此允许直接为函数动态添加属性或方法,构造函数的属性和方法被称为静态成员。
<script>
// 构造函数
function Person(name, age) {
// 省略实例成员
}
// 静态属性
Person.eyes = 2
Person.arms = 2
// 静态方法
Person.walk = function () {
console.log('^_^人都会走路...')
// this 指向 Person
console.log(this.eyes)
}
</script>
总结:
- 静态成员指的是添加到构造函数本身的属性和方法
- 一般公共特征的属性或方法静态成员设置为静态成员
- 静态成员方法中的 this 指向构造函数本身
四、内置构造函数
JS底层将基础的数据类型包装成复杂数据类型,所有有自带属性方法
4.1 Object
Object 是内置的构造函数,用于创建普通对象。
<script>
// 通过构造函数创建普通对象
const user = new Object({name: '小明', age: 15})
// 这种方式声明的变量称为【字面量】
let student = {name: '杜子腾', age: 21}
// 对象语法简写
let name = '小红';
let people = {
// 相当于 name: name
name,
// 相当于 walk: function () {}
walk () {
console.log('人都要走路...');
}
}
console.log(student.constructor);
console.log(user.constructor);
console.log(student instanceof Object);
</script>
- keys 获取所有的键
- values 获取所有的值
- assign 对象的拷贝
4.2 Array
Array 是内置的构造函数,用于创建数组。
<script>
// 构造函数创建数组
let arr = new Array(5, 7, 8);
// 字面量方式创建数组
let list = ['html', 'css', 'javascript']
</script>
数组赋值后,无论修改哪个变量另一个对象的数据值也会相当发生改变。
总结:
- 推荐使用字面量方式声明数组,而不是 Array 构造函数
- 实例方法 forEach 用于遍历数组,替代 for 循环 (重点),只遍历不返回
- 实例方法 filter 过滤数组单元值,生成新数组(重点)
- 实例方法 map 迭代原数组,生成新数组(重点)
- 实例方法 join 数组元素拼接为字符串,返回字符串(重点)
- 实例方法 find 查找元素, 返回符合测试条件的第一个数组元素值,如果没有符合条件的则返回 undefined(重点)
- 实例方法every 检测数组所有元素是否都符合指定条件,如果所有元素都通过检测返回 true,否则返回 false(重点)
- 实例方法some 检测数组中的元素是否满足指定条件 如果数组中有元素满足条件返回 true,否则返回 false
- 实例方法 concat 合并两个数组,返回生成新数组
- 实例方法 sort 对原数组单元值排序
- 实例方法 splice 删除或替换原数组单元
- 实例方法 reverse 反转数组
- 实例方法 findIndex 查找元素的索引值
from()
把伪数组转换为真数组
几个Array的常见用法
const arr = [
{
name: 'xiaomi',
price: 1999
},
{
name: 'huawei',
price: 3999
}
]
// 1. find() 查找
const item = arr.find(function(item) {
return item.name === 'xiaomi'
})
// const item = arr.find(item => item.name === 'xiaomi')
console.log(item);
// 2. every() some()返回bool值
const arr1 = [10,20,30,35,45,12]
const flag = arr1.every(item => item >= 10)
console.log(flag);
arr.reduce(function(累计值, 当前元素){}, 起始值)
4.3 包装类型
String
String 是内置的构造函数,用于创建字符串。
<script>
// 使用构造函数创建字符串
let str = new String('hello world!');
// 字面量创建字符串
let str2 = '你好,世界!';
// 检测是否属于同一个构造函数
console.log(str.constructor === str2.constructor); // true
console.log(str instanceof String); // false
</script>
总结:
- 实例属性 length 用来获取字符串的度长(重点)
- 实例方法 split('分隔符') 用来将字符串拆分成数组(重点)
- 实例方法 substring(需要截取的第一个字符的索引[,结束的索引号]) 用于字符串截取(重点)
- 实例方法 startsWith(检测字符串[, 检测位置索引号]) 检测是否以某字符开头(重点)
- 实例方法 includes(搜索的字符串[, 检测位置索引号]) 判断一个字符串是否包含在另一个字符串中,根据情况返回 true 或 false(重点)
- 实例方法 toUpperCase 用于将字母转换成大写
- 实例方法 toLowerCase 用于将就转换成小写
- 实例方法 indexOf 检测是否包含某字符
- 实例方法 endsWith 检测是否以某字符结尾
- 实例方法 replace 用于替换字符串,支持正则匹配
- 实例方法 match 用于查找字符串,支持正则匹配
注:String 也可以当做普通函数使用,这时它的作用是强制转换成字符串数据类型。
split是将字符串分割成数组,join将数组换成字符串
Number
Number 是内置的构造函数,用于创建数值。
<script>
// 使用构造函数创建数值
let x = new Number('10')
let y = new Number(5)
// 字面量创建数值
let z = 20
</script>
总结:
- 推荐使用字面量方式声明数值,而不是 Number 构造函数
- 实例方法 toFixed 用于设置保留小数位的长度
五、原型
5.1 编程思想
面向过程:分析问题的解决步骤一步一步的实现
面向对象:将事物分解成一个对象,让对象分工合作
- 封装
- 继承
- 多态
5.2 构造函数
js的对象可以通过构造函数实现封装,构造函数创建的对象彼此独立、互不影响
问题?构造函数会造成内存的浪费
5.3 原型
5.3.1 原型对象
js中所有构造方法都有一个原型prototype
属性,也称为原型对象。使用构造函数时将公共的函数挂载到原型上解决浪费内存的问题。构造函数和原型对象的this都指向实例化对象
公共的属性写在构造函数里面
公共方法写原型上
原型是什么?
- 一个对象,我们也称为prototype为原型对象
原型的作用是什么?
- 共享方法
- 可以把那些不变的方法,直接定义在prototype对象上
5.3.2 constructor属性
每个原型对象都有个constructor
属性,该属性指向原型对象的构造函数
console.log(Star.prototype.constructor === Star)
如果原型对象重新赋值,覆盖了原来的原型对象,使用constroctor
重新指向原来的构造函数
// constructor 构造器
function Star() {
}
// 1. 挂载到Star上
// Star.prototype.sing = function() {
// console.log("唱歌");
// }
// Star.prototype.dance = function() {
// console.log("跳舞");
// }
// 会覆盖实例对象的原型 constroctor就找不到原型对象的构造函数
Star.prototype = {
// 所以使用constroctor 重新指向创建这个对象的构造函数
constructor:Star,
sing: function() {
console.log("唱歌");
},
dance: function() {
console.log("跳舞");
}
}
5.3.3 对象原型
对象原型__proto__
指向构造函数的原型对象
注意:
__proto__
是js的非标准属性,在新版的浏览器显示[[prototype]]
- __proto__对象原型里面有个constroctor属性,指向创建该示例对象的构造函数
总结:
prototype是什么?
- 原型对象
- 构造函数都有原型对象
constructor属性在哪里?作用是什么?
- 在原型对象和对象原型都有
- 都指向创建的实例对象或原型的构造函数
__proto__属性是什么?指向谁?
- 在实例对象中
- 指向原型对象
5.3.4 原型继承
js中有多种继承方式,这里我们主要了解原型继承
- 封装-提取公共部分
-
- 把男人女人公共部分提取出来放在人类中
- 继承-让实例对象的原型对象继承公共对象
-
- 把抽取的对象赋值给man的原型对象
- 注意constroctor要重新指向man
const Person = {
eyes: 2,
head: 1,
say: function() {
console.log('hello');
}
}
function Man() {
}
function Woman() {
}
Woman.prototype = Person
// constroctor会被覆盖 要重新指向
Woman.prototype.constroctor = Woman
const alice = new Woman()
console.log(alice);
- 问题?继承不能添加私有方法
但是,我们给女人添加一个生孩子的方法,男人也会自动添加到这个方法
这是因为男人和女人都引用了同个对象,添加或修改两个都会被影响
- 解决--构造函数
我们使用new每次就会创建一个新的对象
// 将对象修改成构造函数新建构造函数
function Person() {
this.eyes = 2
this.head = 1
this.say = function() {
console.log('hello');
}
}
Man.prototype = new Person()
5.3.5 原型链
基于原型对象继承使不同构造函数的原型对象以链状的形式关联在一起
原型链的查找规则
- 当访问一个对象的属性(方法),先在查找对象自身有没有属性
- 如果没有就找他的原型(__proto__指向的prototype原型对象)
- 如果还没有就查找原型对象的原型(Object的原型对象)
- 以此类推知道找到object为止(null)
- __proto__对象原型是给对象成员提供一个查找方向
- 可以使用instanceof检测函数prototype是否在原型链上
六、深浅拷贝
基础数据类型保存在栈内存中
引用类型的数据保存在堆内存中,栈内存存放堆内存数据的地址
当我们在开发中给一个对象赋值,如果使用直接赋值会出现如下问题
const person = {
name: 'alice',
age: 18
}
const p1 = person
console.log(p1); // {name: 'alice', age: 18}
p1.age = 20
console.log(p1); // {name: 'alice', age: 20}
// person对象里的值也发送改变
console.log(person); // {name: 'alice', age: 20}
原因是因为js栈中存放的是堆的地址,直接赋值是复制一份堆地址,修改的话原对象也会发生改变
6.1 浅拷贝
浅拷贝只拷贝外面的一层,深层次的引用类型则共享内存地址
如果属性是基础数据类型,则拷贝基础数据类型的值,如果属性是引用类型,则拷贝内存地址。
常见方法:
- 拷贝对象:
Object.assign
或展开运算符{ ...obj }
- 拷贝数组:
Array.prototype.concat()
或者[...arr]
6.2 深拷贝
深拷贝就是开辟一个新的栈,两个属性完全相同,但对应两个不同地址,修改一个对象属性时,不会改变另外一个对象的属性
- 使用函数递归的方式实现深拷贝
const obj = {
uname: 'alice',
age: 18,
hobby: ['篮球','足球']
}
const o = {}
// 拷贝函数
function deepCopy(newObj,oldObj) {
for(let k in oldObj) {
// 处理数组的问题
if (oldObj[k] instanceof Array) {
newObj[k] = []
deepCopy(newObj[k],oldObj[k])
}else if(oldObj[k] instanceof Object){
// 一定先写Array 再 Object 不能颠倒
newObj[k] = {}
deepCopy(newObj[k],oldObj[k])
}else{
// k是属性名 oldObj[k] 属性值
// newObj[k] === o.uname 不能写.因为k是个变量,[]支持变量
newObj[k] = oldObj[k]
}
}
}
deepCopy(o,obj)
console.log(o);
o.age = 20
o.hobby[0] = '乒乓球'
console.log(obj);
- js库lodash里面的cloneDeep内部实现深拷贝
const obj = {
uname: 'alice',
age: 18,
hobby: ['篮球','足球'],
family: {
baby: 'coco'
}
}
const o = _.cloneDeep(obj)
console.log(o);
o.family.baby = 'dodo'
console.log(obj);
- 通过JSON.stringify()
const obj = {
uname: 'alice',
age: 18,
hobby: ['篮球','足球'],
family: {
baby: 'coco'
}
}
const o = JSON.parse(JSON.stringify(obj))
console.log(o);
o.family.baby = 'dodo'
console.log(obj);
七、异常处理
7.1 throw
throw会抛出异常程序会终止执行
throw通常和new Error配合,能看到更完成错误信息
7.2 try catch finally
提示浏览器错误信息,但不主动中断浏览器信息,中断要return或throw
7.3 debugger
八、处理this
8.1 this指向
普通函数
谁调用this的值就是谁,普通函数没有指明调用者就是window,在严格模式没有调用者this指明undefined
箭头函数
箭头函数不存在this,箭头函数的this引用的是最近的作用域中的调用者,一层一层的查找知道有this的定义
构造函数,原型,DOM事件中不推荐用箭头函数
8.2 改变this指向
call
使用call调用,同时指定被调用函数this的值
fun.call(thisArg,arg1,arg2...)
thisArg
指定函数中this的指向arg1,arg2
其他参数- 返回值就是函数的返回值
const obj = {
uname: 'Alice'
}
function fn(x,y) {
console.log(this);
console.log(x + y);
}
fn.call(obj,3,4)
apply
和call用法基本相同,第二个参数是数组
const obj = {
uname: 'Alice'
}
function fn(x,y) {
console.log(this);
console.log(x + y);
}
fn.apply(obj,[1,2])
// 使用场景:求数组最大值
// 传统方式
const max = Math.max(100,21,33)
console.log(max);
// 展开运算符
const arr = [100,21,33]
console.log(Math.max(...arr));
// apply
const maxValue = Math.max.apply(Math,arr)
const minValue = Math.min.apply(null,arr)
console.log(maxValue,minValue);
bind
不会调用函数,会改变this指向,返回值是函数
fun.bind(thisArg,arg1,arg2...)
thisArg
指定fun函数运行时的this指向arg1,arg2
其他参数- 改边函数的this值,并返回原函数的拷贝
const obj = {
age: 18
}
function fn() {
console.log(this);
}
// 返回值是个函数 但是这个函数里面的this是修改过了
const fun = fn.bind(obj)
console.log(fun);
fun()
// 需求,有一个按钮,点击需要金庸,2秒后开启
const btn = document.querySelector('button')
btn.addEventListener('click',function() {
// 禁用按钮
this.disabled = true
setTimeout(function() {
// 在这个普通函数里面,我们要this由原来的window 改btn
this.disabled = false
}.bind(this),2000)
})
总结:
- 相同点
-
- 都是改变函数内部的this指向
- 区别点
-
- call和apply会直接调用函数,并改变this指向
- call和apply的参数不一样,apply的参数必须是数组形式
- bind不会调用函数,会改变this指向
- 应用场景
-
- call调用函数并传递参数
- apply经常跟数组有关,比如借助数学函数求最大值
- bind不调用函数,但还想改变this指向,比如改变定时器内部的this指向
九、性能优化
9.1 防抖(bounce)
单位时间内,频繁触发事件,只执行最后一次(打断就要重新执行)
使用场景:
- 搜索框搜索输入。用户最后一次输入完才会发出请求
- 手机号码和邮箱的验证
手写防抖函数
防抖的核心是利用定时器(setTimeout)来实现
- 声明一个定时器变量
- 每次鼠标滑动时判断是否有定时器,如果有先清除之前的定时器
- 如果没有定时器则开启定时器,记得存放在变量里面
- 在定时器里面调用要执行的函数
案例:利用防抖实现鼠标滑过时显示文字
<script>
// 需求:鼠标在盒子上移动,里面的数字会+1
const box = document.querySelector('.box')
let count = 0
function mouseMove() {
box.innerHTML = count ++
}
box.addEventListener('mousemove',mouseMove())
// 1. 利用lodash库实现防抖 -500毫秒之后+1
// 语法:_.debounce(fun,时间)
// box.addEventListener('mousemove',_.debounce(mouseMove,50))
// 2. 防抖函数
function debounce(fn,t) {
let timer
// return 返回一个匿名函数
return function() {
if (timer) clearTimeout(timer)
timer = setTimeout(function(){
fn()
},t)
}
}
box.addEventListener('mousemove',debounce(mouseMove,500))
</script>
9.2 节流(throttle)
单位时间内,频繁触发事件,只执行一次(在冷却时间无法重复触发)
使用场景:
鼠标移动mousemove
页面尺寸缩放resize
滚动条滚动scroll等
手写节流函数
- 声明一个定时器变量
- 当鼠标每次滑动判断是否有定时器,如果有定时器则不开启新定时器
- 如果没有定时器则开启定时器,记得存放到变量里
-
- 定时器里面调用执行的函数
- 定时器里面要把定时器清空(null)
9.3 综合案例
需求:页面打开记录上次视频播放的位置
- ontimeupdate 事件在视频/音频(audio/video)当前的播放位置发送改变时触发
- onloadeddata 事件在当前帧的数据加载完成且还没有足够的数据播放视频/音频(audio/video)的
下一帧时触发
谁需要节流?
ontimeupdate , 触发频次太高了,我们可以设定 1秒钟触发一次
思路:
1. 在ontimeupdate事件触发的时候,每隔1秒钟,就记录当前时间到本地存储
2. 下次打开页面, onloadeddata 事件触发,就可以从本地存储取出时间,让视频从取出的时间播放,如
果没有就默认为0s
3. 获得当前时间 video.currentTime
const video = document.querySelector('video')
video.ontimeupdate = _.throttle(() => {
console.log(video.currentTime);
// 把当前得时间存储到本地存储
localStorage.setItem('currentTime',video.currentTime)
},1000)
video.onloadeddata = () => {
video.currentTime = localStorage.getItem('currentTime') || 0
}