JavaScript作为一门运行在浏览器上的弱类型,解释型语言。
本文将先从作用域和事件循环机制来讨论js的运行机制,
再讨论三种设计模式如何用js实现及相关应用,
接着是关于js的一些语法,主要分为逻辑,数据类型和函数,再深入讨论ES6的语法,
最后是一些模块封装和常用模块了解,导入等
作用域
概念
一般值变量作用域
全局作用域
什么时候创建和销毁?页面打开,关闭
window全局对象打开页面即创建
全局作用域声明的变量和函数会作为window的属性和方法保存
函数作用域
创建和销毁?调用函数,执行函数完毕
在函数作用域中访问变量和函数,会先在自身作用域寻找
若没有找到,则会到上一级,直到全局作用域
预编译
全局代码执行前期(预编译),会创建执行期上下文的对象GO(全局作用域)
函数代码执行(函数调用)的前期(预编译),会创建执行期上下文的内部对象A0(函数作用域)
全局作用域的预编译
- 创建GO对象
- 找变量声明 作为GO对象的属性名,值是undefined
- 函数声明function a( ){ } 会覆盖变量声明
函数作用域的预编译(常问)
- 创建AO对象
- 找形参和变量的声明 作为AO对象的属性名,值是undefined
- 实参和形参相统一
- 函数声明function a( ){ } 会覆盖变量声明
注意: var b = function ( ){ } 函数表达式,不是函数声明,是变量b声明
作用域链
被保存在隐式的属性中[[scope]],这个属性我们用户访问不到,js引擎访问
AO和GO的集合
<script>
// 全局域编译GO
var gl
function a() {
function b() {
var bbb = 1
var aaa = 2
}
b()
// b函数预编译AO,作用域链:
// 0:bbb,aaa (b函数的AO)
// 1: function b, aa(a函数的AO)
// 2:gl, function a (GO)
var aa = 0
}
a() // a函数预编译AO
</script>
函数执行完销毁与作用域链
<script>
function a(){
var aa = 123
return function b(){ //b函数作用链中,可以拿到aa
var bb = 234
console.log(aa)
}
}
var res = a()
// b函数
// 虽然a执行完,销毁了,但b函数被保存在res里了
// a的销毁,只是a的作用域链销毁,并不影响 b的作用域链,b依然可以拿到aa
res() // 123
</script>
闭包
a函数内 return b函数,a函数内部的变量可以被外部的b函数使用,不会随着a函数执行完毕而被销毁
b函数在出生的时候(第一次调用),能够访问到a函数的变量,存在作用链中,之后即使a函数执行完被销毁,b函数的作用链不会改变,一样可以访问到a函数的变量
应用:
debounce和throttle防抖和节流:定时器的变量都用到闭包
单例模式:res = fn.apply(this,arguments),保留上次的执行结果
防抖节流
有个封装好的工具,lodash,在html页面引入js
_.debounce(函数,间隔时间)
防抖函数
定时器
触发事件的同时,开始定时器
定时期间,再次触发事件,事件不执行,定时器重新开始定时
过了定时期间,再次触发事件,事件执行,定时器再次开始定时
应用场景:动画渲染防止频繁操作,输入框防止频繁提交,解决异步操作的bug()
<input type="text" id="input">
<script>
// 1秒只能用enter键提交一次
function debounce(callback,delay){
let timer
// timer变量,函数执行期间要一直存在内存当中,如果没有闭包会造成内存泄漏
// 解决方法:闭包,函数执行结束即顺带把变量销毁了
return function(value){
clearTimeout(timer) //先clear,之前触发的定时器清除掉,timer变为undefined
timer = setTimeout(function(){ // 再set,重新开始定时
callback(value)
},delay)
}
}
// 功能函数callback
function func(value){
console.log(value)
}
// 交互
var input = document.getElementById('input')
var debounceFn = debounce(func,1000) //返回function(arg){}
// 监听input的keyup事件,enter后将input标签里的内容打印到consloe控制台上
input.addEventListener('keyup', function(e){
debounceFn(e.target.value) // 监听input获取值e.target.value
})
</script>
节流函数
一段时间内,只做执行一次事件
应用场景:表单提交,鼠标点击多次,只生效一次
<button id="button">点击</button>
<script>
function throttle(callback,delay){
let timer
return function(){
timer = setTimeout(function(){
callback()
timer = null
// 完成功能后,后面的操作都不管,等到该定时器销毁,新操作才有效
},delay)
}
}
// 功能函数callback
function func(){
console.log(Math.random()) // 输出0-1之间的数字
}
// 交互
// 监听点击事件onclick = 函数(简写)相对于用addEventListener('事件',函数)
document.getElementById('button').onclick = throttle(handle,2000)
懒加载
<------ 滚动到特定位置的时候 ------>
![A lazy image](lazy.jpg)
结合防抖函数和节流函数,根据可视区域,加载图片
<img src="" data-src="270*270.jpg">
<script>
var num = document.getElementsByTagName('img').length
var img = document.getElementsByTagName('img')
var n = 0 // 存储图片加载到的位置,避免每次都从第一张图片遍历
var isLoadImg = false //图片是否都加载完成
var _clientHeight = document.documentElement.clientHeight //可见区域的高度,有完整兼容写法,见瀑布流功能
var _srollTop = document.documentElement.scrollTop || document.body.scrollTop
// 滚动条距离顶部的高度,有兼容写法,见瀑布流功能
// 监听窗口变化(防抖函数)的功能函数:计算可见区域
function computedClientHeight(){
_clientHeight = document.documentElement.clientHeight
}
// 滚动屏幕(节流函数)的功能函数:根据可视区域,加载图片
function lazyload(){
isLoadImg = n >= num // isLoadImg是 true 还是 false
_srollTop = document.documentElement.scrollTop || document.body.scrollTop
// 从第n张开始遍历,加载
for (var i = n; i < num; i++){
if (img[i].offsetTop < _clientHeight + _srollTop){
// 可见区域+滚动条的高度 大于 图片高度,要将图片显示出来
if (img[i].getAttribute('src') == ''){
// 接着如果src属性为空,要将图片显示出来,需要给属性值
img[i].src = img[i].getAttribute('data-src')
}
n = i + 1 // 不用管判断属性值是否为空,只要图片显示出来,就加1
}
}
}
// 节流函数
function throttle(callback,delay,flag){
let timeout
return function(){
if (flag) { // 图片如果加载完成,直接return退出
return
}
if (!timeout){
timeout = setTimeout(function(){
callback()
timeout = null
// 完成功能前,别的操作都不管,等到该定时器销毁,新操作才有效
},delay)
}
}
}
// 防抖函数
function debounce(callback,delay){
let timer
return function(){
clearTimeout(timer)
timer = setTimeout(function(){
callback()
},delay)
}
}
// 交互
// 先初始化懒加载
lazyload()
// 加载图片需要一定的时间,滚动屏幕限制,节流函数
window.addEventListener('scroll',throttle(lazyload,100,isLoadImg))
// 监听可视区域高度变化,防止频繁滚动屏幕多次计算,防抖函数
window.addEventListener('resize',debounce(computedClientHeigh,800))
单例模式
res一直保存在内存中,用到闭包
判断是否已经存在res,单例模式
// 单例模式(闭包)
var getSingle = function(fn){
var res;
// res这个变量会一直保存在内存中,第一次是undefined,之后就都有值了
// (Boolean(undefined)-->false)
return function(){
return res || (res = fn.apply(this,arguments))
// 第一次前面不成立,走后面
// 之后res都有值了,就直接用res即可,不用走后面了
}
}
内存泄漏
函数内的变量,不会随着函数执行完毕而销毁
有哪些?
- 闭包
- 意外的全局变量(函数内没有声明的变量,默认为全局变量)
- 被遗忘的定时器
- 脱离dom的引用(获取了dom的引用,后面元素被删除了,但是一直保留着对元素的引用)
js的运行机制
单线程:同一时间做一件事情
因为js离不开和用户的操作,不然会造成与用户交互发生混乱
arguments:类数组对象
箭头函数没有arguments对象
经常用于替代函数的形参
function get( ){
console.log(arguments)
}
get(1,2,3)
将类数组对象转化为数组对象
-
展开运算符…(ES6)
-
Array.prototype.slice.call(agruments)
eventLoop
事件循环机制
js内存模型
call stack 调用栈:执行主进程任务(基本数据类型)
函数调用时,会被压入调用栈中,被压入的函数叫帧,当函数执行完毕后会从调用栈中弹出
heap 堆:存储非结构化数据,对象(函数,数组,对象)(引用数据类型)
消息队列(宏任务):异步操作(如fetch setTimeout等)函数,压入到调用栈中的时候里面的消息会进入到消息队列
消息队列中的,会等到调用栈 清空 才会执行
微任务队列:异步操作(如promise,async,await),加入到微任务中
微任务中的,会在调用栈清空的时候 立即执行(先于消息队列宏任务)
promise前面的都会立即执行,resolve里的值会传给then,then后才会进入微任务队列,
task queue 任务队列:存放异步任务与定时任务
js代码执行机制
同步任务与异步任务
- 同步任务在主线程的栈中按顺序执行
- 异步任务有了运行结果,就会在任务队列中放置一个事件
- 栈中所有同步任务执行完毕,系统读取任务队列,选出需要首先执行的任务(顺序由浏览器决定,并不按序)
宏任务与微任务
-
MacroTask(宏观任务) setTimeout, setInterval, , requestAnimationFrame, I/O
-
MicroTask(微观任务) process.nextTick, Promise, Object.observe, MutationObserver
-
**先同步任务,再取出第一个宏任务执行,所有的相关微任务总会在下一个宏任务之前全部 执行完毕 **
如果遇见 就 先微后宏
promise内的直接执行,then后面的为微任务
设计模式
写js的一些思想
单例模式
只有一个实例,可以全局访问
应用场景:一个全局使用的类,频繁的创建和销毁,1. 首页页面的缓存 2.弹窗
如何实现:判断系统中是否已经有这个实例,如果有直接返回,没有则创建
思想:单一职责,耦合度更低
实现登录功能——弹窗:
ES5 (闭包)
res一直保存在内存中,用到闭包
判断是否已经存在res,单例模式
<button id="loginBtn">登录</button>
<script>
// 单例模式(闭包)
var getSingle = function(fn){
var res;
// res这个变量会一直保存在内存中,第一次是undefined,之后就都有值了
// (Boolean(undefined)-->false)
return function(){
return res || (res = fn.apply(this,arguments))
// 第一次前面不成立,走后面
// 之后res都有值了,就直接用res即可,不用走后面了
}
}
// 弹出登录窗口(功能)
var createLogin = function(a,b,c){
var div = document.createElement('div')
div.innerHTML = '登录窗口'
div.style.display = 'none'
document.body.appendChild(div)
return div
}
// 交互(全局作用域),点击事件-调用函数
var create = getSingle(createLogin)
document.getElementById('loginBtn').onclick = function(){
var loginLay = create(1,2,3)
loginLay.style.display = 'block'
}
</script>
ES6 (类静态方法)
类:ES5 构造函数的语法糖
某个类只能new出一个实例,功能写在类里
类的静态方法,类能调用
class Foo{
constructor(name){
this.name = name
}
// 类的静态方法,类可以调用,实例不能调用
// 如果没有实例创建实例,有实例直接返回实例
static getInstance(name){
if(!this.instance){
this.instance = new Foo(name)
}
return this.instance
}
}
let foo = Foo.getInstance('小明')
策略模式
算法封装
将算法的实现(策略)和算法的使用(区分) 分离 开来
// 提供三种策略供选择使用
// 对象的属性值为函数
var strategies = {
'S': function(salary){
return salary * 4
},
'A': function(salary){
return salary * 4
},
'B': function(salary){
return salary * 4
}
}
// 选一种策略使用函数
var getBouns = function (level,salary){
return strategies[level](salary)
}
// 调用函数
var bouns = getBouns('S',10000)
console.log(bouns)
应用场景:表单验证封装
- 构造验证策略
- 将策略的使用封装成构造函数(类)
- 使用类完成验证,实现交互
<form action="" method="post" id="registerForm">
请输入用户名:
<input type="text" name="username">
请输入密码:
<input type="password" name="password">
请输入手机号码:
<input type="text" name="phonenumber">
<button>提交</button>
</form>
<script>
// 1. 验证策略
var strategies = {
isNonEmpty: function(value,errorMsg){
if (value == ''){
return errorMsg
}
},
minLength: function(value,length,errorMsg){
if (value.length < length){
return errorMsg
}
},
isMobile: function(value,errorMsg){
if (!/^1[3|5|8][0-9]{9}$/.test(value)){
return errorMsg
}
},
}
// 2. 策略的使用封装成构造函数(类),add,start方法
var Validator = function(){
this.cache = [] // 保存验证规则的数据
}
// 将 调用不同策略的函数 加入到cache中
Validator.prototype.add = function (dom,rule,errorMsg){
var ruleArr = rule.split(':') // rule = strategy:中间的参数(length)
this.cache.push(function(){ // 往cache数组里添加函数
var strategy = arr.shift()
// 将ruleArr里的第一个元素移出数组ruleArr,并返回给strategy
ruleArr.unshift(dom.value) // 将dom.value加到ruleArr中,放在第一个
ruleArr.push(errorMsg)
// ruleArr = [dom.value,参数比如length,errorMsg]
// 上面的各种操作都是为了将传入的参数改为如下,从而可以去调用不同策略
return strategies[strategy](...ruleArr) //
})
}
// 遍历cache,调用函数
Validator.prototype.start = function(){
for (var i = 0, vaFunc; vaFunc = this.cache[i++];){
var msg = vaFunc()
if (msg) {
return msg //只要有错,立马返回错误信息,并退出循环,不再继续验证
}
}
}
// 3. 调用类(用函数包住),交互
// 验证功能
var registerForm = document.getElementById('registerForm')
// 用函数包住,函数执行完后会,自动销毁内部的变量
var validateFun = function(){
var validator = new Validator()
validator.add(registerForm.username, 'isNonEmpty','用户名不能为空')
validator.add(registerForm.password, 'minLength:6','密码长度不能小于6位')
validator.add(registerForm.phonenumber,'isMoblie','手机格式不正确')
var errorMsg = validator.start()
return errorMsg
}
// 交互,监听提交事件,并将结果展示到页面上
registerForm.onsubmit = function(){
var errorMsg = validateFun()
if (errorMsg){
alert(errorMsg)
return false
}
}
</script>
发布订阅模式
低耦合,发布key和订阅fn分开
登录成功,主组件将信息发布出来,不同子模块订阅展示出来,跨组件传值
主组件发布trigger,其他组件子组件,孙组件等listen,获取值
pubsub.js发布订阅模式实现如下:
// 发布订阅模式封装到utils,export default Event供使用
var Event = (function(){
var list = {},
// list = {
// key0:[fn0,fn1],
// key1:[fn0,fn1]
// }
listen,
trigger,
remove;
// 往list加fn
listen = function(key,fn){
if(!list[key]){
list[key] = []
}
list[key].push(fn)
}
// 取出fn并执行
trigger = function(){
var key = Array.prototype.shift.call(arguments)
// key:数组
var fns = list[key]
if (!fns || fns.length == 0){
return
}
for (var i = 0,fn; fn = fns[i++];){
fn(...arguments)
// fn.apply(arguments)
}
}
// 移除fn
remove = function(key,fn){
var fns = list[key]
if (!fns){
// 没有这个fns数组来查fn
return false
}
if (!fn){
// 没有传入fn,则将fns.length赋值为0
fn && (fns.length = 0)
// 短路表达式:
// &&返回第一个假值,或最后一个真值
// ||返回第一个真值,或最后一个假值
// 0、""、null、false、undefined、NaN都会判定为false
}else{
// 从后往前查找是否有fn,倒序遍历
for (var i = fns.length -1; i>=0; i--){
var _fn = fns[i]
if (_fn == fn){
fns.splice(i,1)
// spliec在index为i的地方,移出1个元素
}
}
}
}
return {
listen,
trigger,
remove
}
})()
export default Event
拓展:
可以按照pubsub-js包
逻辑
if
if ( 判断false还是true ) { }
重点:0、""、null、false、undefined、NaN都会判定为false
三元表达式
Array.isArray(cur) ? flatten(cur) : cur
if (Array.isArray(cur)){
return flatten(cur)
}else{
return cur
}
短路表达式
(函数里不用写return)
&&返回第一个假值,或最后一个真值
||返回第一个真值,或最后一个假值
提前退出(解构)
判断一个对象里的key是否全,解构{ }
const printDetails = ({type,name,gender} = {}) =>{
if(!type) return 'no type'
if(!name) return 'no name'
if(!gender) return 'no gender'
return '${name} is a ${gender} ${type}'
}
console.log(printDetails({type:'dog',name:'wang',gender:'female'}))
数据类型
基本数据类型:number string boolean undefined null 存储在栈里
引用数据类型:obj 存储在堆里
栈指向堆
字符串
.indexof(值) 字符串里如果没有这个值,返回-1(===)
.split(’’).reverse().join(’’)
对象
对象字面量代替switch-case
// 对象字面量
const cases = {
one:['a'],
two:['b']
}
function printCases(num){
return cases[num] || []
}
console.log(printCases(null)) // []
console.log(printCases('one')) // ['a']
key会自动转化为字符串,隐式调用toString
var obj1 ={
name:'zs'
}
var obj2 ={
name:'ls'
}
var obj3 ={
[obj1]:'11',
[obj2]:'22'
}
console.log(obj3) //{[object Object]:'22'}
用Map代替{ },set和get,不会隐形toString
// 对象字面量
const cases = new Map().set('one',['a']).set('two',['b'])
// {one:['a'],two:['b']}
function printCases(num){
return cases.get(num) || []
}
console.log(printCases(null)) // []
console.log(printCases('one')) // ['a']
array
方法
加入,移出元素
array.shift(元素or数组)移出,并返回,没有传入的话,默认第一个元素
.splice(index,个数)从index移出n个元素
ps: Array.prototype.slice.call(div) 不会修改数组,而是返回一个子数组 (ie8以下不能用)
.unshift( )从前加入元素,返回length
.push(元素,元素)从后加入元素,改变原数组
.concat(元素,元素)将参数合并入数组,不会改变原数组,返回一个新数组,可接收
push和concat如果加入的是数组,push会直接把数组加入,而contat会把数组解析为元素后加入
// 合并两个数组
let arr1 = [1,2,3]
let arr2 = [4,5,6]
// 1.apply
Array.prototype.push.apply(arr1,arr2)
console.log(arr1) // [1,2,3,4,5,6]
// 2.扩展运算符...
判断
Array.isArray(参数)判断参数是否为数组
.includes(参数)判断参数是否在数组中
.length
复杂应用
(手写原生特性)
.flat(几层) 扁平化处理
.every(函数)函数的参数item,函数里的每个item都满足,才会返回true
// 判断是否全为红色
const array =[
{name:'a', color:'red'},
{name:'b', color:'yellow'},
]
function isAllred(){
const res = array.every(item => item.color == 'red')
console.log(res)
}
isAllred() // false
.some(函数)函数的参数item,函数里只要有一个item都满足,就会返回true
// 判断是否有为红色
const array =[
{name:'a', color:'red'},
{name:'b', color:'yellow'},
]
function isAllred(){
const res = array.some(item => item.color == 'red')
console.log(res)
}
isAllred() // true
.map(函数) 处理每个元素,返回新的数组,函数的参数item,index
.fliter(函数) 处理每个元素,返回符合条件的新的数组,函数的参数item
.filter( (item)=> ( 条件 ) )ps:直接(条件)箭头函数语法,不用写return
.filter((item)=>( (item.id-0) === id ) )
// item.id-0转化为num类型,传入的id参数直接是num类型即可
// goods数据数组的filter方法:传入函数,该函数的参数为item单条数据,返回符合条件的item集合数组
.reduce(函数,初始值) 回调函数的参数:pre,cur,index,array(调用改方法的数组)
可用于:
-
数组扁平化(concat)
-
计算数组内值的合,乘积
-
计算元素出现的次数
let person = ['a','b','c','a','d']
let calNum = person.reduce((pre,cur) => {
// pre:{'a':2,'b':1}
// cur:'a'
if (cur in pre){
pre[cur]++
}else{
pre[cur] = 1
}
return pre
},{})
-
数组去重
如果要元素去重,先扁平化,再如下去重
var array = [2,3,3,2,4,5,6]
let unrepeated = array.reduce((pre,cur) =>{
if (pre.includes(cur)){
return pre
}else{
return pre.push(cur)
// 扁平化后push和concat均可
}
},[])
对象{ }key是否在里面,直接in
数组[ ]元素是否在里面,array.includes(元素)
拓展:
arguments类数组,箭头函数,其他函数有,用上面方法时,要Array.prototype.shift.call(arguments)用call
区别于调用函数传入类数组arguments参数用apply,也可以用…扩展符
数组扁平化处理
将多维数组变为一维数组
-
array.flat(几层) 还可传入Infinity
-
正则去除
var str = JSON.stringfy(array).replace(/\[|\]/g,'').split(','); var arr = JSON.parse( '['+ str + ']' )
-
for+递归
var arr = []; const flatten = array =>{ for (let i = 0; i<array.length; i++){ // 判断array[i]是否为数组,是的话,递归 if (Array.isArray(array[i])){ fn(array[i]); }else{ arr.push(array[i]) } } } flatten(array);
-
reduce+递归
const flatten = array =>{ return array.reduce( // reduce可以处理数组上次回调返回的值pre(或初始值[])和当前值cur (pre,cur)=>{ return pre.concat(Array.isArray(cur) ? flatten(cur) : cur); // concat把参数合并进数组,将非数组的cur与pre合并后返回 },[]) // []初始值 } var arr = flatten(array);
手写原生的特性
- 特性的功能
Array.prototype.map
1)了解功能:map方法可以传入一个函数,返回新的数组
<script>
var arr = [1,2,3]
// map可以传入一个函数,返回新的数组
var array = arr.map((item,index) => {
return item * 2
})
console.log(array)
</script>
2)开始写map函数:一,检查传入参数是否正确 二,将新的item用for循环push到新的数组中
<script>
var arr = [1,2,3]
// map可以传入一个函数,返回新的数组
var array = arr.map((item,index) => {
return item * 2
})
console.log(array)
// 写map方法,要传入arr
function map(arr,callback){
// 检查参数是否正确,boolean(0)-->false
// 不是数组,数组为空,不是函数
if (!Array.isArray(arr) || !arr.length || typeof callback !== 'function'){
return []
}else{
// 参数正确进行如下操作
let res = []
for (let i=0, len=arr.length; i<len; i++){
res.push(callback(arr[i],i,arr)) // 要传入arr
}
return res
}
}
map(arr,(item)=>{
console.log(item)
})
</script>
Array.flat(n)
数组扁平化
Array.isArray
判断是否为数组
let arr = [1,2,3]
function isArray(array){
return Object.prototype.toString.call(array) === '[object Array]'
// 如果判断是否为对象[object Object]
// [object String]
// [object Null]
}
赋值与拷贝
- 赋值:将栈中的内存地址给新变量,同一个地址(栈)
// 赋值(栈中相同地址)
var person = {
name: "a",
hobby: ["study", "play", ["book", "stamp"]]
};
var person1 = person;
person1.name = "b"
person1.hobby[0] = "learn"
console.log(person)
console.log(person1) // person和person1一样,name和hobby都被修改了
- 浅拷贝:创建一个新对象(栈)
属性是基本数据类型,拷贝的是值(hasOwnProperty)
属性是引用数据类型,拷贝的是内存地址
var person = {
name: "a",
hobby: ["study", "play", ["book", "stamp"]]
};
// 浅拷贝(栈中地址不同,堆中地址相同)
function shallowCopy(obj){
var target = {}
for (var i in obj){
if (obj.hasOwnProperty(i)){
target[i] = obj[i]
}
}
return target
}
var person1 = shallowCopy(person)
person1.name = "b"
person1.hobby[0] = "learn"
console.log(person)
console.log(person1)
// person 和 person1 不一样,一个的name是a,一个是b,但是hobby[0]都为learn
浅拷贝:
Object.assign( )
…
concat
loash:clone
- 深拷贝:栈+堆
修改新对象不会影响原对象
var person = {
name: "a",
hobby: ["study", "play", ["book", "stamp"]]
};
// 深拷贝 栈和堆地址都不同,递归
function deepClone(obj){
var cloneObj = new obj.constructor()
// var cloneObj = {}
if (obj === null) return obj
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
if (typeof obj !== 'object') return obj
for (var i in obj) {
if (obj.hasOwnProperty){
cloneObj[i] = deepClone(obj[i])
}
}
return cloneObj
}
var person1 = deepClone(person)
person1.name = 'b'
person1.hobby[0] = 'learn'
console.log(person)
console.log(person1)
// person 和 person1 name和hobby都不同
拓展:如果对象里没有date和function,可以考虑用json深拷贝
var person1 = JSON.parse( JSON.stringify(person) )
date会被为{},function会直接没了
深拷贝:
$.extend
deepClone
函数
函数即对象,有方法
.call(对象,参数)
.apply(对象,参数类数组) 类数组也可用es6解构…
.bind(obj)
都可以改变this指向:用别人的东西,代码复用
bind不会立即执行,call和apply会
let bind = child.showName.bind(obj)
bind( )
应用场景:伪数组转化为数组
伪数组:arguments,获取的dom元素产生的HTML集合getElementsBy,
- slice+call (ie8以上)
var div = document.getElementsByTagName('div')
var arr = Array.prototype.slice.call(div)
- for循环(兼容)
function listToArray(likeArray){
var arr = []
try{
arr = Array.prototype.slice.call(likeArray)
}catch(e){
for (var i = 0; i < likeArray.length; i++){
arr[arr.length] = likeArray[i]
// 相当于arr.push(likeArray[i])
}
}
}
this
-
在函数中直接使用
function get(content){
console.log(content)
}
get.call(window,“你好”) 的语法糖:get(“你好”)
-
函数作为对象的方法被调用
var person = {
name : “张三”,
fun: function (time) {
console.log(" t h i s . n a m e 在 跑 步 最 多 {this.name} 在跑步 最多 this.name在跑步最多{time}min就不行了")
}
}
person.run.call (person,30) 的语法糖:person.run(30)
输出:张三在跑步 最多30min就不行了
语法糖倒推call,找出this具体指代哪个
var name = 222
var a = {
name: 111,
say: function () {
console.log(this.name)
}
}
var fun = a.say
fun() // fun.call(window) 222
a.say() // a.say(a) 111
var b = {
name: 333,
say: function (fun) {
fun()
}
}
b.say(a.say)
// b.say(b,a.say) fun()没有this,所以b不用管
// 接下来执行内嵌的fun(),相当于fun(window)即a.say.call(window)
// 222
b.say = a.say // 直接把b的say方法换为console.log(this.name)
b.say() //b.say.call(b) 333
-
箭头函数的this
上面的call来确定this的指向,执行时才确定this执行
箭头函数是在定义函数时,this指向就确定了
因为箭头函数没有自己的this,导致内部的this就是外层代码的this
同时,因为没有this,箭头函数也不能用作构造函数
// 箭头函数的this var x = 11; var obj = { x: 22, say: () => { console.log(this.x); } } obj.say(); //箭头函数没有this,用的外部的,所以还是window 11 var ob = { birth: 1990, getAge: function () { var b = this.birth; var fn = () => new Date().getFullYear() - this.birth; return fn(); } } var d = ob.getAge(); // ob.getAge.call(ob),var b = 1990, var fn 箭头函数this指向外部为obj对象 console.log(d) // fn() 2021-1990 = 31
高阶函数
将函数作为 参数或者返回值 的函数
构造函数
首字母大写
属性(this)和方法,可实例化
语法糖为es6的类
// 构造函数
// 属性
function Person(name,sex){
this.name = name
this.sex = sex
}
// 方法
Person.prototype.say = function(){
console.log('自我介绍')
}
// 实例化
let person1 = new Person('小明','男')
console.log(person1.name)
person1.say()
继承
原型链的继承
缺点:
- 继承后的类产生的实例,改变一个实例,会导致另一个实例也跟着改变
- 没有实现super功能(对父类进行传参)
function Parent(){
this.name = ['a']
}
Parent.prototype.getName = function(){
return this.name
}
// 原型链的继承
function Child(){
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'b'
console.log(child1.name)
console.log(child2.name)
// 都输出的[‘b’]
构造函数的继承
在子类的构造函数中,执行父类的构造函数,并为其绑定子类的this(call)
解决:改变一个实例,不会导致另一个实例跟着改变
缺点:不能继承父类原型上的方法和属性
function Parent(name){
this.name = [name]
}
Parent.prototype.getName = function(){
return this.name
}
// 子类的构造函数的继承
function Child(){
Parent.call(this,'a')
}
// 不能继承父类原型上的方法和属性
// Child.prototype.constructor = Child
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'b'
console.log(child1.name)
console.log(child2.name)
// [‘b’] ['a']
console.log(child2.getName())
// 报错,不能继承父类原型上的方法和属性
组合式继承
把上面两个组合一下 构造函数的继承+原型链的继承
缺点:每次生成一个子类的实例,都要call和new,父类执行两次
function Parent(name){
this.name = [name]
}
Parent.prototype.getName = function(){
return this.name
}
// 子类的构造函数的继承
function Child(){
Parent.call(this,'a')
}
// 继承父类原型上的方法和属性(原型链的继承)
Child.prototype = new Parent()
Child.prototype.constructor = Child
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'b'
console.log(child1.name)
console.log(child2.name)
// [‘b’] ['a']
console.log(child2.getName())
// ['a']
寄生式继承
减去一次父类的执行
Child.prototype = new Parent() 改为Parent.prototype
缺点:子类对原型的操作会影响到父类的原型
Parent.prototype改为Object.create(Parent.prototype)
function Parent(name){
this.name = [name]
}
Parent.prototype.getName = function(){
return this.name
}
// 子类的构造函数的继承
function Child(){
Parent.call(this,'a')
}
// 继承父类原型上的方法和属性(原型链的继承)
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'b'
console.log(child1.name)
console.log(child2.name)
// [‘b’] ['a']
console.log(child2.getName())
// ['a']
ES6语法
块级作用域
let(块作用域),const,var
箭头函数
数据结构
魔板字符串
symbol
对象
展开运算符…
对象解构,数组等
class类
es5构造函数的语法糖,面向对象的写法,继承更简单
(属性constructor+方法),继承(extends,super), 实例化(new)
constructor 实例化的时候就会调用,初始化
例子如下:
class Person {
constructor(){
this.name='建林'
this.age=18
}
say() {
console.log('say方法')
}
}
class Teacher extends Person {
constructor(){
super();// 继承必须写super 他就是父类 上面的那个 constructor
this.name='思聪'
}
eat(){
console.log('eat')
}
}
let t1=new Teacher()
console.log(t1)
静态方法(static)
只能被类本身调用,不能被实例调用
class Foo{
static classMethod(){
return 'hello'
}
}
console.log(Foo.classMethod())
Decorator
装饰器,注释或修改类和类方法@,放在类前面或类方法前面,普通函数不要使用
进入代码就会执行
//装饰类Foo
@frozen
class Foo {
//装饰method方法
@configurable(false)
method() {}
//装饰yy方法
@throttle(500)
yy() {}
}
修饰类(@不带参数)
frozen是一个带targer参数的函数,相当于调用frozen,给类Foo加上一些代码
function frozen(target) {
target.isfrozen = true;
}
@frozen
class Foo {
// ...
}
Foo.isfrozen // true
//为它加上了静态属性isfrozen。frozen函数的参数target是Foo类本身。
修饰类(@带参数)
嵌套函数,return函数
function frozen(isfrozen){
return function(target){
target.isfrozen = isfrozen;
}
}
@frozen(true)
class Foo {
// ...
}
Foo.isfrozen // true
@frozen(false)
class MyFoo {
// ...
}
Foo.isfrozen // false
修饰类方法
函数参数三个:1.哪个类target 2. 要修饰的属性名name 3.属性的描述对象descriptor
// descriptor对象原来的值如下
// {
configurable:false,//能否使用delete、能否需改属性特性、或能否修改访问器属性、,
false为不可重新定义,默认值为true
enumerable:false,//对象属性是否可通过for-in循环,flase为不可循环,默认值为true
writable:false,//对象属性是否可修改,flase为不可修改,默认值为true
value:‘xiaoming’ //对象属性的默认值
// };
function frozen(target, name, descriptor){
descriptor.writable = false;
return descriptor;
}
class Person {
@readonly
abc() { console.log('我是person的abc函数') }
}
执行顺序
洋葱模型,由外到内进入,再由内到外执行
异步Promise
async,await
回调地狱
回调函数:在需要的时候就可以调用对应函数,在函数中调用函数
async,await(Generator)
Generator函数的语法糖
语法糖:功能不受影响,增加可读性
Generator函数(生成器):可以返回一系列值,yield
优化代码:将generator函数(yield)改为async函数(await)
const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
改造
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
ps:读文件返回promise对象这个函数多次使用,所以,再抽象出一个带参数的函数返回promise对象,调用该函数,传入文件名,即可读取对应的文件
promise-then
async函数返回promise对象,then方法添加回调函数
回调函数执行时,如果遇到await会先返回
等异步操作完成(then后的微任务),再接着执行函数体内后面的语句
概念
执行顺序
(池子)
- 同步异步,微任务宏任务(setTimeout),pending,resolve,reject
同步代码
resolve——then微任务的异步代码
宏任务的异步代码
数字打印的顺序
const first = () => (new Promise((resolve, reject) => {
console.log(3);
let p = new Promise((resolve, reject) => {
console.log(7);
setTimeout(() => {
console.log(5);
resolve(6);
}, 0)
resolve(1);
});
resolve(2);
p.then((arg) => {
console.log(arg);
});
}));
first().then((arg) => {
console.log(arg);
});
console.log(4);
//1.同步的代码(最高)
//3
//7
//4
//2. 微任务的异步代码(次高,then)
//1
//2
//3. 宏任务的异步代码(最低,setTimeout)
//5
//6 不执行
- then 的第二回调函数和 catch
交叉,继承
// 画出以下异步函数执行的可能的路线。
// https://developers.google.com/web/fundamentals/primers/promises
// 蓝线表示执行的 promise 路径,红路表示拒绝的 promise 路径。
// https://developers.google.com/web/fundamentals/primers/imgs/promise-flow.svg
asyncThing1()
.then(function() {
return asyncThing2();
})
.then(function() {
return asyncThing3();
})
.catch(function(err) {
return asyncRecovery1();
})
.then(
function() {
return asyncThing4();
},
function(err) {
return asyncRecovery2();
}
)
.catch(function(err) {
console.log("Don't worry about it");
})
.then(function() {
console.log("All done!");
});
链式调用
b.then().then().then()
返回自己,或者返回一个 和自己类似的结构
class Test1{
then(){
console.log(6666);
return this;
}
}
var a= new Test1();
a.then().then().then()
class Test2{
then(){
console.log(77777);
return new Test2();
}
}
var b= new Test2();
b.then().then().then()
单元测试
1.准备测试框架
参考jest 官网
# 1.初始化项目
npm init
# 2.安装 jest
yarn add --dev jest
#npm install --save-dev jest
# 3. 支持 es2015+ 和 ts
yarn add --dev babel-jest @babel/core @babel/preset-env @babel/preset-typescript @types/jest
# 4. 添加 文件 babel.config.js
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
};
# 5.配置命令
"test": "jest",
"test:watch": "jest --watchAll"
2.为了检查 Promise 实现的正确性,我们提前准备好单元测试
import MyPromise from "./index";
// 1
test("1.promise 参数函数会立即执行", function() {
var string;
new MyPromise(function() {
string = "foo";
});
expect(string).toBe("foo");
},500);
it("2. promise 在 then 的回调函数中可以拿到 resolve 的数据。", function(done) {
var testString = "foo";
var promise = new MyPromise(function(resolve) {
setTimeout(function() {
resolve(testString);
}, 20);
});
promise.then(function(string) {
expect(string).toBe(testString);
done();
});
},500);
// 3
it("promise 可以有多个 then,并且会依次执行", function(done) {
var testString = "foo";
var promise = new MyPromise(function(resolve) {
setTimeout(function() {
resolve(testString);
}, 20);
});
promise.then(function(string) {
expect(string).toBe(testString);
});
promise.then(function(string) {
expect(string).toBe(testString);
done();
});
},500);
it("4.promise 可以嵌套多个 then,then的回调中可以返回 promise ", function(done) {
var testString = "foo";
var promise = new MyPromise(function(resolve) {
setTimeout(function() {
resolve();
}, 20);
});
promise
.then(function() {
return new MyPromise(function(resolve) {
setTimeout(function() {
resolve(testString);
}, 20);
});
})
.then(function(string) {
expect(string).toBe(testString);
done();
});
},500);
// 5
it("5.promise 可以嵌套多个 then,then的回调中可以返回 一个普通值", function(done) {
var testString = "foo";
var promise = new MyPromise(function(resolve) {
setTimeout(function() {
resolve();
}, 20);
});
promise
.then(function() {
return testString;
})
.then(function(string) {
expect(string).toBe(testString);
done();
});
},500);
// 6
it("6.resolved 状态的promise ,如果调用 then 方法会立即执行", function(done) {
var testString = "foo";
var promise = new MyPromise(function(resolve) {
setTimeout(function() {
resolve(testString);
}, 20);
});
setTimeout(function() {
promise.then(function(value) {
expect(value).toBe(testString);
done();
});
}, 200);
},500);
it("7. 二次调用 resolve 不会产生影响。", function(done) {
var testString = "foo";
var testString2 = "bar";
var promise = new MyPromise(function(resolve) {
setTimeout(function() {
resolve(testString);
resolve(testString2);
}, 20);
});
promise.then(function(value) {
expect(value).toBe(testString);
});
setTimeout(function() {
promise.then(function(value) {
expect(value).toBe(testString);
done();
});
}, 50);
},500);
实现promise
promise 是一个对象,一般是通过 new Promise ()来实例化的;所以这里我要实现 Promise 类!
promise 的 then 是可以链式调用的,所以可能会用到上面提到的,链式调用的实现。
根据逐个单元测试的要求来实现 Promise
主要实现 Promise 的构造方法和 then 方法; 后面会以链接的方式给出完整的实现。
【解题代码】
const State = {
pending: "pending",
resolved: "rejected",
rejected: "rejected"
};
const noop = () => {};
class MyPromise {
constructor(exclutor) {
exclutor(this._resolve.bind(this), this._reject);
}
_state = State.pending;
_value;
_resolve(val) {
if (this._state === State.pending) {
this._value = val;
this._state = State.resolved;
this._runResolveArray();
}
}
_reject() {}
_runResolveArray() {
//执行 then 传入进来的 onRes
this._resArray.forEach(item => {
// const item
const result = item.handle(this._value);
const nextPromise = item.promise;
if (result instanceof MyPromise) {
result.then(val => item.promise._resolve(val));
} else {
item.promise._resolve(result);
}
});
}
_resArray = [];
then(onRes, onRej = noop) {
// if (this._state === State.pending) {
const newPromise = new MyPromise(() => {});
const item = { promise: newPromise, handle: onRes };
this._resArray.push(item);
// }
if (this._state === State.resolved) {
this._runResolveArray();
}
return newPromise;
}
}
export default MyPromise;
【问题延伸】
几种Promise 的实现
https://github.com/ericyang89/my-promise
https://github.com/vividbytes/implementing-promises
https://github.com/iam91/zpromise/blob/master/src/zpromise.js
代理
TS
好处: 比js多了类型约束,减少bug
应用:vue和react都可使用
模块封装
网络/api 请求模块
-
接口请求一般是异步的,可以返回 promise 更加清晰。
-
网络请求url 的公共部分可以单独配置到 网络请求内部。
-
针对所有的接口可以进行统一的处理。这也是面向切面编程的一个实践。
-
可以借助第三方库 axios 快速的封装 网络请求模块。
【代码】
import axios from "axios";
import constant from "../constant";
import reactNavigationHelper from "./reactNavigationHelper";
import commonToast from "./commonToast";
//配置请求url 的公共部分及超时时间
const commonHttp = axios.create({
baseURL: constant.baseUri,
timeout: 10 * 1000
});
commonHttp.interceptors.response.use(
function(response) {
return response;
},
function(error) {
//针对所有接口统一处理登录过期的问题
if (error.response.status === 401) {
commonToast.show("登录过期");
reactNavigationHelper.navigate("Login");
}
return Promise.reject(error);
}
);
export default commonHttp;
导入
js
js导入js
import mycounter from ‘./counter’
console.log(mycounter) 可以
但是不能mycounter += 1,即 mycounter = mycounter + 1,会出现mycounter未定义
可以用,但没有定义变量
var mycounter = mycounter + 1, var变量提升,原来import的没用了,mycounter变为undefined,+1,输出NaN
vue
- vue页面的script
1)script导入js文件
import 文件名 from ‘@/utils/文件名.js’
2)script导入vue文件,组件(注册)
import 组件名 from ‘./组件名’
export default{
components:{
组件名
}
}
-
vue页面的template
导入组件<组件名 />
与uniapp些许不同,uniapp还是双标签