这是我参与「第四届青训营 」笔记创作活动的的第18天。 今天JS进阶内容做了实践,将经验收获分享给大家!
一、本节课重点内容:
本节课将介绍常用的五大高阶函数: 防抖(debounce)、节流(throttle)、回调、预置、偏函数。
复制代码
二、课堂知识点纪要:
-
高阶函数定义
高阶函数就是一个将函数作为参数或者返回值的函数 function add(x,y,fn){ return fn(x) + fn(y); } //当我们调用add(-5,6,Math.abs)时,我们不难发现,函数的执行过程为 x = -5; y = 6; f = Math.abs; f(x) + f(y) ==> Math.abs(-5) + Math.abs(6) ==> 11; 复制代码 -
柯里化
柯里化函数首先会接收一些参数,接收后,继续返回另一个函数,传入的参数在函数形成的闭包中被保存。待到函数被真正需要时,所有保存的参数都会被一次性用于求值。 function curry(company, department) { //接收固定的公司、部门 return function (name, age) { console.log(`我是 ${company} ${department} 部门的 ${name},${age} 岁`); } } let print = curry('ali', 'F77'); // 传递固定的公司、部门 print('zhangsan', 20);//调用 return 出来的函数并传变化的参数 print('lisi', 30); 复制代码 -
五个高阶函数:
-
回调函数
回调是一种将函数坐会返回值执行的高阶函数,借助回调可以实现递归。
// 作为返回值输出 function fn(){ return function(){} } fn() 复制代码 -
偏函数
偏函数是将一个“纯函数的返回值”作为独立变量,可以理解为初始化默认值,方便在新函数中调用、返回。
function isType(type) { return function(obj) { return Object.prototype.toString.call(obj) === `[object ${type}]` } } const isArray = isType('Array'); const isString = isType('String'); console.log(isArray([1, 2, [3,4]])); // true console.log(isString({})); // false 复制代码 -
预置函数
预置函数即预先设置了返回条件的高阶函数。
function after(time, cb) { return function() { if (--time === 0) { cb(); } } } //预置条件:吃三碗才能吃饱 let eat = after(3, function() { console.log('吃饱了'); }); eat(); eat(); eat(); // eat函数(条件)只有执行3次的时候才会输出'吃饱了' 复制代码 -
防抖函数
const debounce = (func, wait) => { //定义一个计时器 let timer; return () => { clearTimeout(timer); timer = setTimeout(func, wait); }; }; function debounce(fn, delay) { // timer 是在闭包中的 let timer = null; let that = this; let _args = arguments; return function() { if (timer) { clearTimeout(timer) } timer = setTimeout(() => { fn.apply(that, _args) timer = null }, delay) } } 复制代码 -
节流函数
const throttle = function(fn, interval){ let that = fn, //保存需要被延迟执行的函数引用 timer, //定时器 firstTime = true;//是否为第一次调用 return function(){ let args = arguments; _this = this; if(firstTime){ that.apply(_this, args); return firstTime = false; } if(timer){//如果定时器还在,则说明前一次延迟执行还没有完成 return false; } timer = setTimeout(function(){ clearTimeout(timer); timer = null; that.apply(_this, args); },interval || 1000) } } 复制代码
-
三、实践练习案例:
-
Styled-Compnent样式:
//Authored by iiru //powerby styled.js import styled from "styled-components"; export const MainContain =styled.div` width:100%; height:1000px; background-image: linear-gradient(to right, #f49ecb,#d94bcc); .mainContent{ margin:5% 25% 0 25%; width:500px; height:16%; background:#391a33; } ` export const MainContent =styled.div` margin:4% 45%; width:200px; float:right; font-size:32px; color:#efefef; 复制代码 -
TS 防抖代码:
//Authored by iiru
//powerby styled.js
import React from "react";
import { MainContain,MainContent} from "./bounce";
import { Input,Button,InputRef,message} from 'antd';
//输入框接口
interface Login {
account: string|InputRef|null;
password: string|InputRef| null;
}
//防抖函数
const debounce=(func:any, delay:number)=>{
let timer: NodeJS.Timeout | null;
return ()=>{
let context = this;
if(timer) clearTimeout(timer);
timer = setTimeout( function(){
func.apply(context,arguments);
},delay)
}
}
//页面组件通过Button和Input调用防抖函数
export function Debounce(){
const loginData:Login = {
account: ' ',
password:' '
}
const tips=(e:any)=>{
console.log(e)
if(e.account.input.value && e.password.input.value ) message.success("Hello!"+e.account.input.value)
else if(!e.account.input.value) message.error("No focus account!")
else if(!e.password.input.value) message.error("No focus password!")
}
const submit:() => void =debounce(()=>tips(loginData),3000)
return (
<>
<MainContain>
<MainContent
>
<Input className={"Inputy"} placeholder="input account" ref={input => loginData.account = input} />
<Input.Password className={"Inputy"} placeholder="input password" ref={input => loginData.password = input} />
<Button onClick={submit}>登录</Button>
</MainContent>
</MainContain>
</>
)
};
复制代码
JS的一些八股
作用域
执行环境中的变量或函数的作用范围,定义了变量和函数有权访问的其他数据。作用域都有一个变量对象(例如全局作用域的变量对象为window)
全局作用域
- 在页面打开时被创建,页面关闭时被销毁
- script标签中的函数或方法的作用域为全局
- 全局作用域可以认为是window,因为所有的全局变量和函数都是作为window对象的属性和方法创建的
局部作用域
- 局部作用域在函数调用时创建,函数执行完毕时销毁
- 每调用一次函数就会创建一个新的函数作用域,他们之间是相互独立的
- 函数作用域可以访问上层作用域,但是不能访问相邻作用域,因为他们之间是相互独立的
块级作用域
- es6之前没有块级作用域
- es6中的块级作用域是通过let和const声明创建的,同样对外不可见
作用域链:
需要引入某个变量,首先会在当前作用域中寻找,若未找到,则继续向上一层级的作用域中寻找,直到全局作用域。我们称这种链式的查询关系为作用域链。作用:保证对执行环境有权访问的所有变量和函数进行有序访问。
从代码执行上看
在构建函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在函数内部的Scope属性中。当调用函数时,会为函数创建一个执行环境,然后通过复制函数的scope属性中的对象构建起执行环境的作用域链,然后创建活动对象并推入执行环境的作用域链。在fn执行完成后,作用域就会被销毁。
预编译
预编译是上下文创建后,js代码执行前,对js代码进行的处理
全局预编译
- 全局上下文创建后,会生成变量对象VO(variable object)
- VO首先寻找变量声明,将var声明的变量作为VO对象的属性名,值为undefined
- 寻找函数声明,属性值为函数本身
- 如果函数名和变量名相同,函数声明会覆盖变量声明
console.log(a)//undefined
var a = 100
var a = function () { }
function b() { }
console.log(a)//f(){}
预编译:
VO{
a:undefined 100 f(){ }
}
//函数声明和函数表达式的区别:
//函数声明会连通命名和函数体一起被提升至作用域顶部
//函数表达式只有命名会被提升,定义的函数体则不会
console.log(a)//f a(){ }
var a = 100
function a() { }
function b() { }
console.log(a)//100
预编译:
VO{
a:undefined f a(){ } 100
}
函数预编译
- 函数上下文创建后,会生成变量对象
- 寻找变量声明,变量名作为对象的属性名,属性值为undefined
- 寻找形参,形参名为对象的属性名,属性值为undefined
- 将实参的值赋给形参,即替换对象中的形参的属性值
- 寻找函数声明,函数名作为对象的属性名,属性值为函数本身
- 如果函数名与变量名重复,函数声明会覆盖变量名
function fn(a,c){
console.log(a);
var a = 123
console.log(a);
console.log(c);
function a (){}
if(false){
var d = 678
}
console.log(d)
console.log(b)
var b = function () {}
console.log(b)
function c(){}
console.log(c)
}
fn(1,2)
函数预编译:
VO:{
a undefined 1 funa
d undefined
b undefined fun
c undefined 2 func
}
js的数据类型
基本数据类型
Number, String, null, Boolean, undefined, Symbol
引用数据类型
Object(function,array,Object, Set,Map)
深拷贝与浅拷贝
浅拷贝
浅拷贝只会拷贝引用数据类型的地址,当某个值被修改后,也会影响到另一个拷贝的数据
//拷贝数据在堆内存中的地址
let obj = {
name: 'cxh',
age: 22
}
let obj1 = obj
console.log(obj1 === obj);//true
obj1.age = 3
console.log(obj1 === obj);//true
console.log(obj, obj1)
深拷贝
深拷贝会拷贝多层,对于每一级的数据都会拷贝
//在内存中重新开辟一个空间,将对象每一级都进行拷贝
let obj = {
name: 'cxh',
age: 22
}
let obj1 = {}
for (let i in obj) {
obj1[i] = obj[i]
}
console.log(obj1 === obj);//false
obj1.age = 3
console.log(obj1 === obj);//false
console.log(obj, obj1)//{name: 'cxh', age: 22} {name: 'cxh', age: 3}
垃圾回收机制
执行环境会负责管理代码执行过程中使用的内存, js 中常用的垃圾回收机制包括标记清除和引用计数,当变量被标识为无用后就会自动清除,下面介绍一个这两种垃圾回收机制。
标记清除
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,去除掉环境中的变量以及被环境引用的变量的标记。然后剩下的仍有标记的变量就是准备删除的变量,销毁这些值并回收他们占用的内存空间。
引用计数
这个方法会跟踪记录每个值被引用的次数,当引用次数变为0时,就可以将其占用的内存空间回收。但这个方法存在一个问题,当两个变量互相引用时,他们的引用次数永远不会是0,导致这部分内存永远不会回收,示例如下:
function fn(){
var objA = new Object()
var objB = new Object()
objA.otherobj = objB
objB.otherobj = objA
}
闭包
能够访问到其他函数作用域内的对象的函数叫做闭包
//闭包两种写法
//嵌套函数,函数作为返回值
function books(){
var book = 'book'
return function(){
console.log(book)
}
}
var bag = books()
bag()//book
//回调函数,函数作为参数
function books(cb){
var book = 'book'
cb(book)
}
callback(book){
console.log(book)
}
books(callback)
分析过程
- 全局执行上下文 创建作用域链,作用域包含了全局变量对象[作用域链:【全局变量对象】]
- books函数调用时创建作用域链,具体操作为先复制全局的作用域,然后创建活动对象AO推入当前作用域的顶端[作用域链:【book活动变量,全局变量对象】 ],books函数执行完毕后当前作用域会被销毁
- bag函数调用创建作用域,首先复制上层作用域[作用域链:[book活动变量,全局变量对象]],然后创建活动对象AO推入当前作用域的顶端[作用域链:【匿名函数func,book活动变量,全局变量对象】],bag函数执行完毕后当前作用域会被销毁
this
js中的this不是固定不变的,它随着执行环境的变化而变化
var obj = {
foo: function() {
console.log(this.bar)
},
bar: 1
}
var foo = obj.foo
var bar = 2
obj.foo() // this指向的是obj
foo() // 调用的是window下的foo;foo中的this.bar this指向的是window
this的使用场景
-
在一般函数调用时,this指向window
-
构造函数中,this指向实例化出来的对象
function fn() { this.a = 'hello' this.b = { b:'world', getB(a){ console.log(a+this.b) } } } var obj = new fn() obj.b.getB(obj.a)//hello world -
作为对象方法调用,this指向调用对象
-
在箭头函数中,this指向上一层的this
-
call和apply调用,this指向第一个调用的对象
var x = 2 function fn() { console.log(this.x) } var obj1 = { x: 1 } var obj2 = {} obj2.fn = fn obj2.fn.apply()//2 apply第一个参数不写,调用对象为window obj2.fn.apply(obj1)//1
EventLoop(事件循环)
虽然说js是一个单线程执行,但是还是分为同步和异步,同步任务一般都会在主线程去执行,主线程在执行过程中遇到异步任务时,将异步任务放到事件循环中,待主线程同步任务执行完成后,取出异步任务依次执行,上诉不断地过程,叫做循环事件(Event loop)
js执行上下文
全局执行上下文、函数执行上下文、eval函数创建的上下文
宏任务与微任务
宏任务:setTimeout sertInterval Ajax Dom事件
微任务:Promise
微任务的执行优先级大于宏任务
顺序:先执行同步任务,后执行异步任务(异步任务中先执行微任务 后执行宏任务)
ajax的实现原理
1.创建ajax对象
var xhr = new XMLHttpRequest()
2.传入请求方式和请求地址
xhr .open('get', 'http://192.168.1.1:8888/getData')
3.发送请求
xhr.send()
4.获取服务器与客户端的响应数据
xhr.onload = function(){
console.log(xhr.responseText)
}
同源策略
同协议 同域名 同端口号都相同
跨域网络访问:
跨域读操作,跨域写操作(link),跨域资源嵌入
例如:
script标签嵌入跨域脚本
link标签嵌入css
通过img展示的图片(是嵌入式资源)
通过video audio播放的多媒体资源
通过object embed嵌入的插件
通过@font-face引入的字体
通过iframe载入的任何资源
如何解决跨域
1.跨域资源共享 cors(给了web服务器权限,即服务器可以通过设置响应头,允许访问他们的资源)
2.JSONP(通过script标签返回一段编译后可执行的代码)
面向对象
功能封装在功能内部,实现功能通过调用对象方法来实现
优点:
- 每个对象都是功能中心,分工明确
- 灵活,代码可复用,容易维护和开发
三大特性
封装,继承,多态
封装
-
工厂模式(通过执行函数创建对象)
function createPerson (name, age) { return { name: name, age: age, sayName: function () { console.log(this.name) } } } var obj = createPerson('cxh',22) -
构造函数模式(没有显示的创造对象,将属性和方法赋值给this,没有return)
构造函数创建对象内部的处理:
- 创建一个新对象(隐式)
- 将构造函数的作用域赋给新对象
- 执行构造函数中的代码
- 返回新对象
function Person (name, age) { //没有显示的创造对象,隐式创建 this.name = name //将属性和方法赋值给this this.age = age this.sayName = function () { console.log(this.name) } //没有return } var p1 = new Person('张三', 18) -
原型模式
可以把所有对象实例需要共享的属性和方法直接定义在
prototype对象上function Person (name, age) { this.name = name this.age = age } Person.prototype = { constructor: Person, // => 手动将 constructor 指向正确的构造函数 type: '学生', sayHello: function () { console.log('我叫' + this.name + ',我今年' + this.age + '岁了') } }
原型和原型链
原型:
原型链:
当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的proto隐式原型上查找(即它的构造函数的prototype),如果还没有找到就会再在构造函数的prototype的proto中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链。
原型继承
让父类中的属性和方法在子类实例的原型链上
可分为:
- 原型链继承(Child.prototype=new Parent())
- 构造函数继承(在构造函数里面使用call)
- 组合继承(原型链继承+构造继承)
- 寄生组合式继承(寄生式+组合式,用了object.create,是class出现前的终极继承方案)
- 对象冒充(不知道谁先想出来的怪招)
- class继承(大家都嫌寄生组合太麻烦了,所以出现了它,屠龙术)
Tips:因果关系记忆法: 因为原型链继承不能传参,所以有了构造继承,但是构造继承不能继承父级的原型,所以出现了结合两种方式的组合继承。 因为组合继承实例化了两次父类,性能有缺陷,强迫症的前端们忍不了,所以想出了原型式继承,再增强成寄生继承,把寄生继承跟组合继承一结合,变成了寄生组合继承,用来解决组合继承的小缺陷 有一位脑洞清奇的人才,发现了对象冒充,让大家的面试题又多了一个答案。 最后大家都觉得写寄生组合继承太费劲了,所以出现了class
原型链继承
原理:让子类的原型对象指向父类的实例(Child.prototype=new Parent()),当子类的实例找不到对应的属性和方法时,就会往父类上查找
优点:继承了父类的所有,包括原型 缺点: 1、不能给父类传参 2、引用类型的属性会被子类修改
构造函数继承
原理:在子类的构造函数里面去执行父类的构造函数,为其绑定子类的this,让父类的构造函数把属性和方法都挂载到子类的this上去
优点: 1、可以传 2、引用类型的属性不会被子类修改 缺点: 1、不能继承原型,因为只是把属性用call绑定到了this上 2、每次构造函数都要多走一个函数(call)
function Parent(name) {
console.log(this);
this.name = [name]
}
Parent.prototype.getName = function () {
return this.name
}
function Child() {
Parent.call(this, 'cxh')//实现super//这是构造函数继承的关键
}
const child1 = new Child()
const child2 = new Child()
child1.name[0] = '666'
console.log(child1.name)
console.log(child2.name)
child1.getName()//child1.getName is not a function//因为不能继承原型
组合式继承
原型链继承+构造继承
优点: 1、可以传参又继承了原型 2、引用类型的属性不会被子类修改 缺点: 每次创建子类实例多执行了new
每次构造函数都要多走一个函数(call)//构造函数继承的缺点
function Parent(name) {
console.log(this);
this.name = [name]
}
Parent.prototype.getName = function () {
return this.name
}
function Child() {
Parent.call(this, 'cxh')//实现super
}
Child.prototype=new Parent()//原型链继承的关键
Child.prototype.constructor = Child
const child1 = new Child()
const child2 = new Child()
child1.name[0] = '666'
console.log(child1.name)
console.log(child2.name)
寄生组合式继承
原理:Child.prototype=new Parent()换成了Child.prototype = Parent.prototype,再优化成了Child.prototype = Object.create(Parent.prototype)
优点: 以上所有的优点 缺点: 1、写起来复杂 2、多执行了call、create
创建js对象的方法和区别
- 对象字面量(字面量简写)
- new Object(字面量)
- 构造函数
- Object .create( )
// 对象字面量
var dog1 = {
name: '大黄',
age: 2,
speak: function () {
console.log("汪汪");
}
}
// 使用Object
var dog2 = new Object();
dog2.name = "大黄";
dog2.age = 2;
dog2.speak = function () {
console.log("汪汪");
}
// 使用构造函数
function Dog(name, age) {
this.name = name;
this.age = age;
}
Dog.prototype.speak = function () {
console.log("汪汪");
};
var dog3 = new Dog("大黄", 2);
// 使用Object.create
var dog4 = Object.create(dog1);
区别:
对象字面量和new Object方式创建的对象结构是一样的。
使用构造函数创建的对象,方法是在其原型上的。
而使用Object .create创建的对象,原对象的属性和方法都会挂到新对象的原型上。
防抖和节流
防抖
触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间。
实现方式
每次触发事件时设置一个延迟调用方法,并且取消之前的延时调用方法
function debounce(fun, wait) {
var timer = null
return function () {
if (timer !== null) {
clearTimeout(timer)
}
timer = setTimeout(fun, wait)
}
}
window.addEventListener('scroll', debounce(function () {
console.log('debounce')
}, 300))
节流
高频事件触发,但在一定时间内只会执行一次,所以节流会稀释函数的执行频率。
实现方式
每次触发事件时,如果当前有等待执行的延时函数,则直接忽略这次事件
function throttle(fn, delay) {
var timer = null
return function () {
if (!timer) {
timer = setTimeout(() => {
fn()
timer = null
}, delay)
}
}
}
window.addEventListener('scroll', throttle(function () {
console.log('throttle')
}, 300))
性能优化
什么是性能?
Web性能是客观的衡量标准,是用户对加载时间和运行时的直观体验。Web性能指页面加载到可交互和可响应所消耗的时间,以及页面在交互时的流畅度--滚动是否顺滑?按钮能否点击?弹窗能否快速打开,动画是否平滑?Web性能既包括客观的度量如加载时间,每秒帧数和到页面可交互的时间;也包括用户的对页面内容加载时间的主观感觉。
如何进行Web性能优化?
1.首先需要了解性能指标-多块才算快? 2.使用专业的工具可量化的评估出网站或应用的性能表现 3.从网站页面响应的声明周期,分析出造成较差性能表现的原原因 4.进行技术改造,可行性分析等具体的优化实施 5.迭代优化
浏览器缓存策略
强缓存
强缓存可以通过设置两种HTTP Header实现: Expires和Cache--Control 可设定浏览器缓存,减少网络请求次数。
协商缓存
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓 存,主要有以下两种情况:
精炼js代码
减少业务复杂度
算法上优化
总结
页面级
减少 HTTP请求数
- 合理设置 HTTP缓存
- 资源合并与压缩
- 小图片可组合成一个icon库
将外部脚本置底
外链脚本在加载时却会阻塞其他资源,如果将脚本放在比较靠前的位置,则会影响整个页面的加载速度从而影响用户体验
将 CSS放在 HEAD中
如果将 CSS放在其他地方比如 BODY中,则浏览器有可能还未下载和解析到 CSS就已经开始渲染页面了,这就导致页面由无 CSS状态跳转到 CSS状态,用户体验比较糟糕。
代码级
减少DOM操作
比如说:减少浏览器的repaint和reflow
避免使用eval,慎用with
因为他们都会改变作用域,从而影响js的预解析,过多使用的话会降低性能
减少作用域链查找
循环中需要访问非本作用域下的变量时可以在遍历之前用局部变量缓存该变量
HTML优化
比如减少使用iframe内联框架
在符合产品需求的基础上适当压缩图片
let const var
不存在变量提升
var命令
会发生“变量提升”现象(也就是预编译)
let命令
- 改变了语法行为,声明的变量一定要在声明后使用,否则报错( 也就是暂时性死区:在代码块内,使用let命令声明变量前,该变量都是不可用的。这在语法上,称为“暂时性死区” )
- 在同一个块级作用域,不能重复声明变量。
- let 声明的变量具有块作用域的特征
const
除了具有 let 的上述特点外,其还具备一个特点,即 const 定义的变量,一旦定义后,就不能修改,即 const 声明的为常量
console.log(a)//undefined
var a = 1
console.log(b)//Cannot access 'b' before initialization
let b = 1
全局声明
与var不同,let在全局作用域中声明的变量不可以通过window对象的属性。
不过let声明仍然是在全局作用域中发生的,相应变量会在页面的生命周期内存续。
var message = "hi"
console.log(window.message) //hi
let message = "hi"
console.log(window.message) //undefined
tips:
for (let i = 0; i < 3; i++) {
let i = 'abc'
console.log(i)//abc abc abc
}
上面代码正确运行,输出了三次abc。这表明函数内部的变量i 和循环变量i 不在同一个作用域,有各自单独的作用域。
总结
- var 声明的变量属于函数作用域,let 和 const 声明的变量属于块级作用域;
- var 存在变量提升现象,而 let 和 const 没有此类现象;
- var 变量可以重复声明,而在同一个块级作用域,let 变量不能重新声明,const 变量不能修改。
解构赋值
ES6允许按照一定模式,可以将属性/值从对象/数组中取出,赋值给其他变量
数组
// 不需要第二个元素
let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
alert( title ); // Consul
配合...rest
let [name1, name2, ...titles] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
// 现在 titles = ["Consul", "of the Roman Republic"]
和 forEach
const obj = [['name', 'cxh'], ['call', '666']]
obj.forEach(([key, val]) => {
console.log(key, val);//name cxh;call 666
});
对象
let {var1, var2} = {var1:…, var2:…}
比如
let options = {
title: "Menu",
width: 100,
height: 200
};
let {title, width, height} = options;
alert(title); // Menu
alert(width); // 100
alert(height); // 200
//提取部分
let options = {
title: "Menu",
width: 100,
height: 200
};
// 仅提取 title 作为变量
let { title } = options;
alert(title); // Menu
源属性: 目标变量
对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量,真正被赋值的是后者,而不是前者
let options = {
title: "Menu",
width: 100,
height: 200
};
// { 源属性: 目标变量 }
let {width: w, height: h, title} = options;
// width -> w
// height -> h
// title -> title
alert(title); // Menu
alert(w); // 100
alert(h); // 200
解构赋值和数组一样可嵌套
const user = {
userName: "光脚丫思考",
blog: "https://blog.csdn.net/gjysk",
details: {
nickName: "光脚丫思考",
address: {
domicileAddress: "户籍地",
abodePlace: "居住地"
},
signature: "勿以善小而不为,勿以恶小而为之"
}
}
const { userName, details: { signature, address: { domicileAddress, abodePlace } } } = user;
console.info(userName);//光脚丫思考
console.info(signature);//勿以善小而不为,勿以恶小而为之
console.info(domicileAddress);//户籍地
console.info(abodePlace);//居住地
继承可取到
const obj1 = {}
const obj2 = { foo: 'bar' }
Object.setPrototypeOf(obj1, obj2)
obj1.__proto__ = obj2
const { foo } = obj1
console.log(foo)//bar
设置默认值
let { x = 3 } = {};x//3
let { x,y = 3 } = {x:2};
x//2
y//3
let { x: y = 3 } = { x: 2 };
x//报错,未定义
y//2
//默认值生效的条件是:对象的属性值严格等于undefined
let { x = 3 } = { x: undefined }
x//undefined
let {x = 3} = { x : null}
x//null
Symbol
表示独一无二的值
声明
let s = Symbol()
console.log(typeof (s));//symbol
作用
保证每个对象的名字都是独一无二的
let a = { a: 1 }
let b = { b: 2 }
let c = {}
c[a] = 3
c[b] = 4
console.log(c);//{[object Object]: 4}
//解决
let a = Symbol({ a: 1 })
let b = Symbol({ b: 2 })
let c = {}
c[a] = 3
c[b] = 4
console.log(c);//{Symbol([object Object]): 3, Symbol([object Object]): 4}
//Symbol的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个Symbol
//相同参数的Symbol的返回值是不相等的,因为参数只是对当前参数的描述
接受字符串为参数
Symbol函数可以接受一个字符串为参数,表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
let a = Symbol('a')
let b = Symbol('a')
console.log(a);//Symbol(a)
console.log(b);//Symbol(a)
console.log(a === b);//false
Symbol与其他类型
- 不能与其他类型的值进行运算,会报错
- 可以显式转为字符串
- 可以转为布尔值,但是不能转成数值
let a = Symbol('a')
console.log(a = a + 'a')//Cannot convert a Symbol value to a string
console.log(String(a))//Symbol(a)
console.log(Boolean(a))//true
console.log(Number(a))//Cannot convert a Symbol value to a number
Object.getOwnPropertySymbols()方法
let a = Symbol({ a: 1 })
let b = Symbol({ b: 2 })
let c = {}
c[a] = 3
c[b] = 4
console.log(Object.getOwnPropertySymbols(c));
//(2) [Symbol([object Object]), Symbol([object Object])]
Set
是新的引用数据类型,类似于数组,但是成员的值都是唯一的,没有重复的值
- Set本身是一个构造函数,用来生成Set数据结构
- Set函数可以接受一个数组作为参数,用来初始化
基本用法
const set = new Set([1,2,3,4,4])
console.log(set)//Set(4){1,2,3,4}
证明Set是新的引用数据结构
之前常用判断
- instanceof
- Object.prototype.toString.call()
let arr = [1,2,3]
console.log(arr instanceof Array)//true
console.log(Object.prototype.toString.call(arr))//[object,Array]
所以
let set = new Set([1,2,3])
console.log(set instanceof Set)//true
console.log(Object.prototype.toString.call(set))//[object,Set]
转化成数组
- 通过...(拓展运算符)duowei
- Array.from
let set = new Set([1, 2, 3, 3])
console.log(Array.from(set))//[1, 2, 3]
console.log([...set])//[1, 2, 3]
set判断两个值是否不同的算法
类似于===,但是NaN===NaN返回的值是false,而Set数据内部认为相等
console.log(new Set([NaN,NaN]))//{NaN}
//但是!{}没有例外
console.log({} === {});//false
console.log({} == {});//false
let t = new Set([{}, {}])
console.log([...t])//(2) [{…}, {…}]
Set的属性和方法
- 属性:size
- 方法:add( ) , delete( ) , has( ) , clear( )
let t = new Set([2, 3])
console.log(t.add(4))//Set(3) {2, 3, 4}
console.log(t.delete(4))//true
console.log(t.has(2))//true
console.log(t.has(4))//false
console.log(t.size)//2
console.log(t.clear())//undefined
console.log(t.size)//0
Set的遍历
let t = new Set([2, 3])
set.forEach((key,val)=>console.log(key + ':'+val))//2:2;3:3
取并集,交集,差集
let t = new Set([1, 2, 3])
let t1 = new Set([2, 3, 4])
//并集
console.log([...t, ...t1]);// [1, 2, 3, 2, 3, 4]
//交集
console.log(new Set([...t].filter((x) => t1.has(x))));//Set(2) {2, 3}
//差集
console.log(new Set([...t].filter((x) => !t1.has(x))));//Set(1) {1}
Map
Object提供了“字符串 - 值”的对应,Map提供了“值 - 值”的对应,是一种更完善的“键值对”结构实现
声明
const map = new Map()
- 属性:size
- 方法:set( ) , delete( ) , has( ) , clear( ) ,get( )
let t = new Map()
let p = 3
t.set(p, 3)
t.set('6',6)
console.log(t);
t.get(3)
t.set('6', 5)
t.delete(3)//true
t//Map(1) { '6' => 5 }
t.size//1
t.clear()//undefined
t//Map(0) { size: 0 }
t.set('6', 5)//Map(1) { '6' => 5 }
t.has('6')//true
Map自带方法
keys(),values(),entries()
证明Map是新的引用数据结构
let t = new Map()
console.log(t instanceof Map)//true
console.log(Object.prototype.toString.call(t))//[object,Map]
与之前对象比较(当对象作为键的时候)
let o1 = {a:1}
let o2 = {b:2}
let o3 = {}
o3[o1]=1
o3[o2]=2
console.log(o3);//{[object Object]: 2}//对象做属性名会调用Object.prototype.toString方法转成字符串,最后属性名都是[object Object]
let o1 = { a: 1 }
let o2 = { b: 2 }
let o3 = new Map()
o3.set(o1, 1)
o3.set(o2, 2)
console.log(o3);//Map(2) {{…} => 1, {…} => 2}
可传入数组为参数
let m = new Map([
[123, 'abc1'],
["a", { x: 1 }],
[3 > 1, 666],
[false, 777]
])
console.log(m.get(true));//666
console.log(m.get(false));//777
注意
let t = new Map([[{ k: 3 }, 3]])
console.log(t.get({ k: 3 }))//undefined
//对象作为键名,传输的是地址,所以即使{ k: 3 }长得一样,但是实际的值不同(储存在堆内存的两个位置)
let a = { k: 3 }
let t = new Map([[a, 3]])
console.log(t.get(a))//3
Iterator和for...of循环
iterator概念
ES6 添加了Map和Set。这样就有了四种数据集合需要一种统一的接口机制,来处理所有不同的数据结构遍历器(Iterator)就是这样一种机制。
它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
iterator的作用
Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for…of循环(详见下文)。当使用for...of循环遍历某种数据结构时,该循环会自动去寻找 Iterator接口。
for...of循环的遍历过程
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
- 第一次调用指对象的next方法,可以将指针指向数据结构的第一个成员。
- 第二次调用指针对象的next方法,指就指向数据结构的第二个成员。
- 不断调用指针对象的next方法,直到它指向数据结构的结束位置。
每次调用next方法返回一个包含value和done两个属性的对象。 共中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。
实现过程:
var it = makeIterator(['a', 'b'])
console.log(it.next()); //{ value: 'a', done: false }
console.log(it.next()); //{value: undefined, done: true}
function makeIterator(arr) {
var nextIndex = 0
return {
next: function () {
return nextIndex < arr.length ?
{ value: arr[nextIndex++], done: false } :
{ value: undefined, done: true }
}
}
}
重点:
Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器对象(该对象的特征就是具有next方法。每次调用next方法,都会返回一个代表当前成员的信息对象,具有value和done两个属性)。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性。(对象的Symbol.iterator属性,指向该对象的默认遍历器方法)
const obj = {
[Symbol.iterator]: function () {
return {
next: function () {
return {
value: 1,
done: true
}
}
}
}
}
//上述代码实现
对象不具备Iterator接口
对象之所以没有默认部署Iterator接口,是因为对象哪个属性先遍历,哪个属性后遍历的顺序是不确定的,需要开发者手动指定。如果对象先具备Iterator接口需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历
let obj = {
data: ['hello', 'world']
}
obj.iterator = function (obj) {
var nextIndex = 0
return {
next: function () {
return nextIndex < obj.length ?
{ value: obj[nextIndex++], done: false } :
{ value: undefined, done: true }
}
}
}
console.log(...obj);
箭头函数
写法
var fn = () => {}
//函数有返回值
var fn = v => v+1
//返回值不是表达式
var fn = v =>({obj: v})
箭头函数和普通函数的区别
this指向
箭头函数本身是没有this的,它的this是从它的作用域链的上一层继承来的,并且无法通过call和apply改变this指向
1、
var fn = function(){
return ()=>{ console.log(this.name) }
}
var obj1 = { name:'张三'}
var obj2 = { name:'李四'}
var name = '王五'
obj1.fn = fn
obj2.fn = fn
obj1.fn()()//张三
obj2.fn()()//李四
fn()()//王五
2、
var user = {
name: 'zhangsan',
fn: function () {
var obj = {
name: 'lisi'
}
var f = () => this.name
return f.call(obj)
}
}
console.log(user.fn())//zhangsan
//定义在哪里就是哪里,call无法改变指向
不能作为构造函数 没有prototype属性
没有arguments对象
可以通过 ...rest 来获取参数
不能使用yield命令,因此箭头函数不能用作Generator函数
实际应用
var name = 'window'
var obj = {
name: 'obj',
methods: () => {
console.log(this.name);
},
fn: function (cb) {
cb()
},
a: function () {
console.log(this, "@@");
}
}
obj.fn1 = function () {
obj.fn(() => { console.log(this.name); })
}
var fn1 = obj.fn1
obj.methods()//window 上一级是window 与谁调用无关,定义在哪里看哪里
obj.fn(() => { console.log(this.name); })//window 同理
fn1()//window 相当于 function () {obj.fn(() => { console.log(this.name); })()
obj.fn1()//obj 上一层是function的作用域,此时的this指向obj,见下一条就懂了
obj.a()//{name: 'obj', methods:...}'@@'
设置默认值
function m1({ x = 0, y = 0 } = {}) {
return [x, y]
}
function m1({ x, y } = { x: 0, y: 0 }) {
return [x, y]
}
console.log(m1());//[0,0]
console.log(m2());//[0,0]
console.log(m1({ x: 3 }));//[3,0]
console.log(m2({ x: 3 }));//[3,undefined]
有趣的0
0==false !0==true //true
0===false //false
0=="" !0==!"" //true
0==="" //false
0==null //false
!0==1==!null//true
0==undefined//false
问题:
let obj = {
data: ['hello', 'world']
}
obj.Symbol.iterator = function (obj) {
var nextIndex = 0
return {
next: function () {
return nextIndex < obj.length ?
{ value: obj[nextIndex++], done: false } :
{ value: undefined, done: true }
}
}
}
console.log(...obj);//Uncaught TypeError: Found non-callable @@iterator
Promise
是异步编程的一种解决方案
new一个promise
let p = new Promise(function(resolve, reject){
if(//判断条件){
resolve('成功');
}else{
reject('失败')
}
});
三种状态
- Pending(进行中)
- Resolved(已完成)
- Rejected(已拒绝)
new Promise((resolve,reject)=>{})//pending
new Promise((resolve,reject)=>{
resolve('very good')
})//fulfilled
new Promise((resolve,reject)=>{
reject('so sad')
})//rejected
状态一旦发生改变,就不会再变
const promise = new Promise(function (resolve, reject) {
resolve('666')
reject("hello")//改变失败
console.log(promise);
}).then((val) => {
console.log(val);
}).catch(function (err) {
console.log(err);
})
//666
then
then函数里的参数是两个异步回调函数
第一次参数函数用于接收res返回来的数据
第二个参数函数用于接收rej返回的数据
const promise = new Promise(function (resolve, reject) {
// resolve('666')
reject("hello")
console.log(promise);
}).then((val) => {
console.log(val);//resolve时输出666
}, (err) => {
console.log(err)//reject时输出hello
})
//不写第二个参数,then后面接一个catch接受reject返回的数据
const promise = new Promise(function (resolve, reject) {
// resolve('666')
reject("hello")
console.log(promise);
}).then((val) => {
console.log(val);
}).catch(function (err) {
console.log(err);//hello
})
//不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。所以,如果状态没有改变,不会进入finally。
new Promise((res,rej)=>{
res('666')
}).then(function(){
console.log('success');
}).catch(function(){
console.log('catch');
}).finally(function(){
console.log('finally');
});
优点
- 将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数(比如回调地狱)。流程更加清晰,代码更加优雅。
- Promise对象提供统一的接口,使得控制异步操作更加容易
比如:
//解决回调地狱
setTimeout(function () { //第一层
console.log('武林要以和为贵');
setTimeout(function () { //第二程
console.log('要讲武德');
setTimeout(function () { //第三层
console.log('不要搞窝里斗');
}, 1000)
}, 2000)
}, 3000)
//用promise
fn('武林要以和为贵')
.then((data) => {
console.log(data);
return fn('要讲武德');
})
.then((data) => {
console.log(data, fn('要讲武德'));
return fn('不要搞窝里斗')
})
.then((data) => {
console.log(data, fn('不要搞窝里斗'));
})
.catch((data) => {
console.log(data);
})
//回调2
function add(x, y, delay, cb) {
setTimeout(() => {
cb(x + y)
}, delay);
}
add(1, 2, 1000, function (num) {
console.log(num);//3
add(num, 1, 1000, function (num) {
console.log(num);//4
add(num, 2, 1000, function (num) {
console.log(num)//6
add(num, 3, 1000, function (num) {
console.log(num)//9
})
})
})
})
//用promise
function add(x, y, delay) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(x + y)
}, delay);
})
}
add(1, 2, 1000).then(val => {
console.log(val);
return add(val, 1, 1000)
}).then(val => {
console.log(val);
return add(val, 2, 1000)
}).then(val => {
console.log(val);
return add(val, 3, 1000)
}).then(val => console.log(val))
缺点
- 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
- 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
Promise.all( )
//参数为数组参数
接收一个数组参数,这组参数为需要执行异步操作的所有方法,里面的值最终都算返回Promise对象。
该方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后并且执行结果都是成功的时候才执行回调。
function promiseClick1() {
let p = new Promise(function (resolve, reject) {
setTimeout(function () {
var num = Math.ceil(Math.random() * 10); //生成1-10的随机数
console.log('2s随机数生成的值:', num)
if (num <= 10) {
resolve(num);
}
else {
reject('2s数字太于10了即将执行失败回调');
}
}, 2000);
})
return p
}
function promiseClick2() {
let p = new Promise(function (resolve, reject) {
setTimeout(function () {
var num = Math.ceil(Math.random() * 10); //生成1-10的随机数
console.log('3s随机数生成的值:', num)
if (num <= 10) {
resolve(num);
}
else {
reject('3s数字太于10了即将执行失败回调');
}
}, 3000);
})
return p
}
function promiseClick3() {
let p = new Promise(function (resolve, reject) {
setTimeout(function () {
var num = Math.ceil(Math.random() * 10); //生成1-10的随机数
console.log('4s随机数生成的值:', num)
if (num <= 10) {
resolve(num);
}
else {
reject('4s数字太于10了即将执行失败回调');
}
}, 4000);
})
return p
}
Promise
.all([promiseClick3(), promiseClick2(), promiseClick1()])
.then(function (results) {
console.log(results);
});
Promise.race( )
//参数为数组参数
数组内谁先执行完成就先执行回调。先执行完的不管是进行了race的成功回调还是失败回调,其余的将不会再进入race的任何回调
Promise
.race([promiseClick3(), promiseClick2(), promiseClick1()])
.then(function (results) {
console.log('成功', results);
}, function (reason) {
console.log('失败', reason);
});
结果如下:
2s随机数生成的值: 6
成功 6//只有这里调用了成功回调
3s随机数生成的值: 5
4s随机数生成的值: 8
Promise.resolve( )
返回一个新的Promise实例,状态为Fulfilled
Promise.reject( )
返回一个新的Promise实例,状态为Rejected
Promise.reject('666').catch(err => {
console.log(err)
})
async函数
async的出现让我们可以用一种更简洁的方式写出基于Promise的异步行为
function p() {
return new Promise(resolve => {
setTimeout(() => {
resolve('yibujieguo')
}, 1000);
})
}
function fn() {
p().then(val => {
console.log(val);
})
}
fn()
//async+await
async function fn1() {
const res = await p()
console.log(res);
}
fn1()
async函数返回值为一个promise,通过then和catch捕获内部值
async function fn1() {
return 'res'
//下面特性1
//等价于
return Promise.resolve('res')
}
console.log(fn1());
// Promise {<fulfilled>: 'res'}
// [[Prototype]]: Promise
// [[PromiseState]]: "fulfilled"
// [[PromiseResult]]: "res"
特性
- async函数内部会返回一个promise对象,如果看起来不是promise,那么它将会隐式的包装在promise中
- await能获取到promise状态改变后的值,如果后面不是一个promise,await会把该值转换为已正常处理的Promise
- await后面promise的状态是reject,则await后的代码不会执行,async函数将返回状态为reject的promise
- async函数内部如果存在await,await表达式会暂停整个async函数的执行,等当前位置promise状态改变后才能恢复
async function fn1() {
const res = await 1
//等价于 特性2
Promise.resolve(1).then(()=>undefined)
}
async function fn1() {
const res = await Promise.reject(1)
console.log(2);
}
fn1()//Uncaught (in promise) 1//特性3
async function fn() {
setTimeout(() => {
console.log(1);
}, 0);
Promise.resolve().then(() => console.log(4))
await setTimeout(() => {
console.log(5);
}, 0);
await Promise.resolve().then(() => console.log(6))//运行到这里,要输出6,此时微任务还有4,先4,后6
Promise.resolve().then(() => console.log(7))
console.log(3);
}
fn()//4 6 3 7 1 5
.await 等到之后,做了一件什么事情?
那么右侧表达式的结果,就是await要等的东西。
等到之后,对于await来说,分2个情况
- 不是promise对象
- 是promise对象
如果不是 promise , await会阻塞后面的代码,先执行async外面的同步代码,同步代码执行完,再回到async内部,把这个非promise的东西,作为 await表达式的结果
如果它等到的是一个 promise 对象,await 也会暂停async后面的代码,先执行async外面的同步代码,等着 Promise 对象 fulfilled,然后把 resolve 的参数作为 await 表达式的运算结果。
和promise什么时候用好一点:
如果是几个事件没有承载性的话,用Promise.all( )方法好,如果有先后顺序的话,用await好一点
test:
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
async1();
new Promise(function (resolve) {
console.log('promise1')
resolve();
}).then(function () {
console.log('promise2')
})
console.log('script end')
script start
async1 start
async2
promise1
script end
promise2
promise4
setTimeout
promise5
class
ES6的写法:
class Person{
// 定义构造函数 是初使化类的
constructor(uname){
// console.log("constructor");
this.uname = uname
}
// 定义一个方法 普通方法 属于对象的
showName(){
console.log(this.uname);
}
//定义静态方法 属于类的,只有本类才能调用,对象无权调用
static eat(){
console.log("吃肉");
}
}
let obj = new Person("小张");
obj.showName();
Person.eat();
class的特性
1.class的数据类型是一个函数
typeof class A {}//function( ){ }
2.class的原型的constructor指向class
class A {}
A.prototype.constructor===A//true
3.通过 new 关键字创建出的实例的constructor指向该class
class A {}
var a = new A()
a.constructor === A//true
4.class内部的方法实际上都是定义在类的prototype上
class A { I
fn () {}
tostring() {}
}
var a =new A()
5.通过类创建对象的本质是调用类的constructor,如果类未定义constructor,则会在使用时默认添加 6.class不能直接调用,需要通过new关键字 7.class内部方法指向的是实例,class内部是严格模式
构造函数与class的区别?
上面黑体
class的继承
类的继承通过extends关键字
class F {
money = '100w'
fn () {}
}
class s extends F{
}
子类中的constructor必须调用super,否则就会报错。
在ES6中使用继承时,constructor中必须调用super()方法,其本质是在调用父类的constructor方法。通过这样的方式来达到属性继承的效果
//如果子类想要覆盖父类的构造方法,子类在构造方法当中,心须调用super()方法。
class f {
money = '100w'
fn () {}
}
class S extends F{
constructor(){
super()
}
}
子类调用super会触发父类的constructor并将参数传递过去
子类中的constructor必须调用super,否则就会报错。`js class f { money = '100w' fn () {}} class S extends F{ constructor(){}} 子类调用super会触发父类的constructor并将参数传递过去
class F {
constructor(sMoney){
this.money = 100+sMoney
}
fn () {}
}
class S extends F{
constructor(money){
//在super调用前子类是没有this,如果使用会报错
super(money)
}
}
console.log(new s(10))//110
继承的属性在实例中,方法在原型上
extends的继承本质上还是原型链的继承
事件捕获 事件冒泡
DOM事件流(event flow )存在三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。
事件捕获(event capturing): 当鼠标点击或者触发dom事件时(被触发dom事件的这个元素被叫作事件源),浏览器会从根节点 =>事件源(由外到内)进行事件传播。
事件冒泡(dubbed bubbling): 事件源 =>根节点(由内到外)进行事件传播。 无论是事件捕获还是事件冒泡,它们都有一个共同的行为,就是事件传播。
dom标准事件流的触发的先后顺序为:先捕获再冒泡。即当触发dom事件时,会先进行事件捕获,捕获到事件源之后通过事件传播进行事件冒泡。
addEventListener的第三个参数 在我们平常用的addEventListener方法中,一般只会用到两个参数,一个是需要绑定的事件,另一个是触发事件后要执行的函数,然而,addEventListener还可以传入第三个参数:
element.addEventListener(event, function, useCapture);
第三个参数默认值是false,表示在事件冒泡阶段调用事件处理函数;如果参数为true,则表示在事件捕获阶段调用处理函数。
webstorage本地存储
webstorage是本地存储,存储在客户端,包括localStorage和sessionStorage。
- 存放大小基本为5MB,仅存储在客户端
- localStorage和sessionStorage只能存储字符串类型,对于复杂的对象可以使用ECMAScript提供的JSON对象的stringify和parse来处理
- 获取方式: localStorage:window.localStorage; sessionStorage:window.sessionStorage;
- 都返回一个storage类型的对象
cookie、sessionStorage和localStorage的区别
共同点
都是保存在浏览器端,是同源的。
cookie数据始终在同源的http请求中携带(即使不需要),即在服务器与客户端之间来回传送,有路径(path)的概念,且存储容量很少4k左右。而sessionStorage
和localStorage仅保存在本地,不会自动传输数据给服务器。
sessionStorage仅在当前浏览器窗口关闭之前有效,不能持久保存。
localStorage窗口或者浏览器关闭都一直有效,用作持久保存。
cookie在设置的cookie到期时间之前一直有效,即使窗口或浏览器关闭。
localStorage和cookie都是同源窗口中都是共享的。sessionStorage不共享。
cookie不安全,相较起来localStorage和sessionStorage安全点,并且webstorage的速度要比cookie快,因为从本地直接获取