万年打野易大师

万年打野易大师 查看完整档案

北京编辑北京工商大学  |  信息工程 编辑  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

万年打野易大师 回答了问题 · 4月1日

antdesignpro 顶部 弹出式导航 怎么写

试试下拉菜单menu相关,自定义子菜单?

关注 2 回答 1

万年打野易大师 发布了文章 · 3月28日

virtualbox安装自己的gitlab,并配置静态IP

最近云服务器没钱搞了,自己搞了个简易的gitlab仓库用,github是真的慢,gitee又不能随便搞,所以自己搭个代码仓库吧

第一步,下载 virtualbox,并安装

官方下载地址

直接安装就行了,运行过程中有无法加载的问题,需要进入bios打开电脑的虚拟机模拟设置(有些电脑可能不支持)。具体的操作在百度搜一下一大堆,这儿就不再说了

第二步,下载gitlab,并安装

bitnami版本的gitlab

安装好virtualbox后,在虚拟机内直接打开这个 .ova 文件,加载好我们的gitlab

启动的时候大多数人或报错:VT-x is not available (VERR_VMX_NO_VMX).

去系统bios开启虚拟服务,并且关闭win10系统的Hyper-V:参考操作

启动成功后看到我们的仓库成功运行

image

图片中第一行黄色的字是我们访问gitlab的地址

图片中第二行黄色的字是gitlab的管理员帐号和密码

图片中第三行黄色的字是bitnami的gitlab的官方地址

红色的字体是debian中linux的登录帐号和密码

第三步,设置静态网络IP

gitlab的linux系统设置中,网络选用 桥接模式

1.查看当前gitlab被分配的ip信息

sudo ifconfig

2.进入到网络配置的文件夹

cd /etc/systemd/network

3.复制一份配置到网络目录,名字叫25-wired.network

sudo cp 99-dhcp.network 25-wired.network

4.编辑文件内容,更新相关信息

vim 25-wired.network

如果上面的命令提示该文件是只读文件,请用下面的命令修改

sudo vim /etc/systemd/network/25-wired.network

按下按键 i , 表示插入,改完设定的参数值

[Match]
Name=INTERFACE-NAME
[Network]
Address=10.1.10.9/24
Gateway=10.1.10.1
DNS=10.1.10.1

上述内容中的IP地址、网关和DNS请以自己电脑为准,修改完成后
esc 按钮,
输入:冒号(英文下)
输入:wq 或者 wq!,
再按enter键结束内容修改

最后重启我们的虚拟机,再访问我们的gitlab地址,就能打开我们自己的gitlab仓库。慢慢玩儿吧

查看原文

赞 2 收藏 1 评论 0

万年打野易大师 发布了文章 · 3月25日

react17中生命周期改变,组件挂载流程微调

react17的版本更新了,做了一些更新,最近学习了新版的生命周期,来使用对比16.8的版本

旧版本:
16.8生命周期

新版本:
image

可以看到新版的生命周期中 componentWillMount, componentWillReceiveProps, componentWillUpdate 三个生命周期被干掉了,其实还没被干掉,使用时会报警告:(componentWillReceiveProps也一样的警告)
image.png

为啥推荐加 UNSAFE_ 前缀,官方说明:新生命周期差异
目的是为了react18版本的 异步渲染更新,请移步官方去查看具体:异步渲染更新新特性探索

新生命周期里面,有两个函数 getDeriveStateFromPropsgetSnapshotBeforeUpdate

getDeriveStateFromProps

从props获取一个派生的状态,在调用 render 方法之前调用,函数必须返回一个状态对象或者 null,

// 使用方法,必须加 static !!!
static getDerivedStateFromProps(props, state)

如果在 getDerivedStateFromProps 中返回了一个状态对象(跟定义的state中一样),视图中的值会被函数返回值拦截并赋值为当前函数返回的状态对象中的值,并且对state内容操作,视图也不会更新(卸载组件还是没影响)。

使用很少

// state的值跟随props值变更而变更
// 例如判断props内值是奇数还是偶数,来确定用state还是用props
// 官方举例实现 <Transition> 组件很容易,跟随信息判断哪些组件做动画
static getDerivedStateFromProps(props, state) {
    props.xx === xx ? props : state;
}

getSnapshotBeforeUpdate

获取更新前的快照,从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给 componentDidUpdate()。应返回 snapshot 的值(或 null

什么是 snapshot ??

答案是什么都可以!!!灵活的js,哈哈哈

结合上述,我们的 componentDidUpdate() 方法中的参数就应该是 prevProps, prevState

来个例子,滚动位置定位

class NewsList extends React.Component{
    state = {newsArr:[]}
    componentDidMount(){
        setInterval(() => {
            //获取原状态
            const {newsArr} = this.state
            //模拟一条新闻
            const news = '新闻'+ (newsArr.length+1)
            //更新状态
            this.setState({newsArr:[news,...newsArr]})
        }, 1000);
    }
    getSnapshotBeforeUpdate(){
        // 返回每一条新闻的高度,就是我们说的,
        return this.refs.list.scrollHeight
    }
    componentDidUpdate(prevProps, prevState, height){
        // 计算滚动到当前想显示的条目位置并且保持不动
        // height就是我们说的 snapshot 的值!!!!
        this.refs.list.scrollTop += this.refs.list.scrollHeight - height
    }
    render(){
        return(
            <div className="list" ref="list">
                {
                    this.state.newsArr.map((n,index)=>{
                        return <div key={index} className="news">{n}</div>
                    })
                }
            </div>
        )
    }
}
ReactDOM.render(<NewsList/>,document.getElementById('test'))
查看原文

赞 4 收藏 3 评论 0

万年打野易大师 发布了文章 · 3月24日

vue3常用的API实用型

vue3.x已经发布了这么久,相关的生态也慢慢起来了,包括vite这个新的打包工具,在vue3.0学习过程中有一些实用性的api对比,希望能在开发中给大家做个示范,准确的使用对应的api去完成我们的项目开发

生命周期的变更

要特别说明一下的就是,setup 函数代替了 beforeCreatecreated 两个生命周期函数,因此我们可以认为它的执行时间在beforeCreatecreated 之间

Vue2Vue3
beforeCreatesetup
createdsetup
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestoryonBeforeUnmount
destoryedonUnmounted

了解过vue3的小伙伴儿都知道,现在使用都会用到setup函数,关于在setup函数操作数据,我们用例子说明会好一点

reactive

reactive 方法是用来创建一个响应式的数据对象,该API也很好地解决了Vue2通过 defineProperty 实现数据响应式的缺陷

用法很简单,只需将数据作为参数传入即可

<template>
  <div id="app">
   <!-- 4. 访问响应式数据对象中的 count  -->
   {{ state.count }}
  </div>
</template>

<script>
// 1. 从 vue 中导入 reactive 
import {reactive} from 'vue'
export default {
  name: 'App',
  setup() {
    // 2. 创建响应式的数据对象
    const state = reactive({count: 3})

    // 3. 将响应式数据对象state return 出去,供template使用
    return {state}
  }
}
</script>

ref

在介绍 setup 函数时,我们使用了 ref 函数包装了一个响应式的数据对象,这里表面上看上去跟 reactive 好像功能一模一样啊,确实差不多,因为 ref 就是通过 reactive 包装了一个对象 ,然后是将值传给该对象中的 value 属性,这也就解释了为什么每次访问时我们都需要加上 .value

我们可以简单地把 ref(obj) 理解为这个样子 reactive({value: obj})

<script>
import {ref, reactive} from 'vue'
export default {
  name: 'App',
  setup() {
   const obj = {count: 3}
   const state1 = ref(obj)
   const state2 = reactive(obj)

    console.log(state1)
    console.log(state2)
  }
  
}
</script>

image

注意: 这里指的 .value 是在 setup 函数中访问 ref 包装后的对象时才需要加的,在 template 模板中访问时是不需要的,因为在编译时,会自动识别其是否为 ref 包装过的

那么我们到底该如何选择 refreactive 呢?

建议:

  1. 基本类型值(StringNmuberBoolean 等)或单值对象(类似像 {count: 3} 这样只有一个属性值的对象)使用 ref
  2. 引用类型值(ObjectArray)使用 reactive

我们在vue2.x中获取元素标签是用 ref ,vue3.x我们要获取元素标签怎么办呢?

<template>
  <div>
    <div ref="el">div元素</div>
  </div>
</template>

<script>
import { ref, onMounted } from 'vue'
export default {
  setup() {
    // 创建一个DOM引用,名称必须与元素的ref属性名相同
    const el = ref(null)

    // 在挂载后才能通过 el 获取到目标元素
    onMounted(() => {
      el.value.innerHTML = '内容被修改'
    })

    // 把创建的引用 return 出去
    return {el}
  }
}
</script>

获取元素的操作一共分为以下几个步骤:

  1. 先给目标元素的 ref 属性设置一个值,假设为 el
  2. 然后在 setup 函数中调用 ref 函数,值为 null,并赋值给变量 el,这里要注意,该变量名必须与我们给元素设置的 ref 属性名相同
  3. 把对元素的引用变量 el 返回(return)出去
补充:设置的元素引用变量只有在组件挂载后才能访问到,因此在挂载前对元素进行操作都是无效的

当然如果我们引用的是一个组件元素,那么获得的将是该组件的实例对象

toRef

toRef 是将某个对象中的某个值转化为响应式数据,其接收两个参数,第一个参数为 obj 对象;第二个参数为对象中的属性名

<script>
// 1. 导入 toRef
import {toRef} from 'vue'
export default {
    setup() {
        const obj = {count: 3}
        // 2. 将 obj 对象中属性count的值转化为响应式数据
        const state = toRef(obj, 'count')
  
        // 3. 将toRef包装过的数据对象返回供template使用
        return {state}
    }
}
</script>

上面又有个ref,又有个toRef,不是冲突了吗?两个有不一样的功效:

<template>
    <p>{{ state1 }}</p>
    <button @click="add1">增加</button>

 <p>{{ state2 }}</p>
    <button @click="add2">增加</button>
</template>

<script>
import {ref, toRef} from 'vue'
export default {
    setup() {
        const obj = {count: 3}
        const state1 = ref(obj.count)
        const state2 = toRef(obj, 'count')

        function add1() {
            state1.value ++
            console.log('原始值:', obj);
            console.log('响应式数据对象:', state1);
        }

        function add2() {
            state2.value ++
            console.log('原始值:', obj);
            console.log('响应式数据对象:', state2);
        }

        return {state1, state2, add1, add2}
    }
}
</script>

ref 是对原数据的一个拷贝,不会影响到原始值,同时响应式数据对象值改变后会同步更新视图
toRef 是对原数据的一个引用,会影响到原始值,但是响应式数据对象值改变后会不会更新视图

toRefs

将传入的对象里所有的属性的值都转化为响应式数据对象,该函数支持一个参数,即 obj 对象

<script>
// 1. 导入 toRefs
import {toRefs} from 'vue'
export default {
    setup() {
        const obj = {
          name: '前端印象',
          age: 22,
          gender: 0
        }
        // 2. 将 obj 对象中属性count的值转化为响应式数据
        const state = toRefs(obj)
  
        // 3. 打印查看一下
        console.log(state)
    }
}
</script>

返回的是一个对象,对象里包含了每一个包装过后的响应式数据对象

shallowReactive

听这个API的名称就知道,这是一个浅层的 reactive,难道意思就是原本的 reactive 是深层的呗,没错,这是一个用于性能优化的API

<script>
<template>
 <p>{{ state.a }}</p>
 <p>{{ state.first.b }}</p>
 <p>{{ state.first.second.c }}</p>
 <button @click="change1">改变1</button>
 <button @click="change2">改变2</button>
</template>
<script>
import {shallowReactive} from 'vue'
export default {
    setup() {
        const obj = {
          a: 1,
          first: {
            b: 2,
            second: {
              c: 3
            }
          }
        }
        
        const state = shallowReactive(obj)
  
        function change1() {
          state.a = 7
        }

        function change2() {
          state.first.b = 8
          state.first.second.c = 9
          console.log(state);
        }

        return {state}
    }
}
</script>

首先我们点击了第二个按钮,改变了第二层的 b 和第三层的 c,虽然值发生了改变,但是视图却没有进行更新;

当我们点击了第一个按钮,改变了第一层的 a 时,整个视图进行了更新;

由此可说明,shallowReactive 监听了第一层属性的值,一旦发生改变,则更新视图

shallowRef

这是一个浅层的 ref,与 shallowReactive 一样是拿来做性能优化的,配合triggerRef ,调用它就可以立马更新视图,其接收一个参数 state ,即需要更新的 ref 对象

shallowReactive 是监听对象第一层的数据变化用于驱动视图更新,那么 shallowRef 则是监听 .value 的值的变化来更新视图的

<template>
 <p>{{ state.a }}</p>
 <p>{{ state.first.b }}</p>
 <p>{{ state.first.second.c }}</p>
 <button @click="change">改变</button>
</template>

<script>
import {shallowRef, triggerRef} from 'vue'
export default {
    setup() {
        const obj = {
          a: 1,
          first: {
            b: 2,
            second: {
              c: 3
            }
          }
        }
        
        const state = shallowRef(obj)
        console.log(state);

        function change() {
          state.value.first.b = 8
          state.value.first.second.c = 9
          // 修改值后立即驱动视图更新
          triggerRef(state)
          console.log(state);
        }

        return {state, change}
    }
}
</script>

toRaw

toRaw 方法是用于获取 refreactive 对象的原始数据的

<script>
import {reactive, toRaw} from 'vue'
export default {
    setup() {
        const obj = {
          name: '前端印象',
          age: 22
        }

        const state = reactive(obj) 
        const raw = toRaw(state)

        console.log(obj === raw)   // true
    }
}
</script>

上述代码就证明了 toRaw 方法从 reactive 对象中获取到的是原始数据,因此我们就可以很方便的通过修改原始数据的值而不更新视图来做一些性能优化了

注意: 补充一句,当 toRaw 方法接收的参数是 ref 对象时,需要加上 .value 才能获取到原始数据对象

markRaw

markRaw 方法可以将原始数据标记为非响应式的,即使用 refreactive 将其包装,仍无法实现数据响应式,其接收一个参数,即原始数据,并返回被标记后的数据。即使我们修改了值也不会更新视图了,即没有实现数据响应式

<template>
 <p>{{ state.name }}</p>
 <p>{{ state.age }}</p>
 <button @click="change">改变</button>
</template>

<script>
import {reactive, markRaw} from 'vue'
export default {
    setup() {
        const obj = {
          name: '前端印象',
          age: 22
        }
        // 通过markRaw标记原始数据obj, 使其数据更新不再被追踪
        const raw = markRaw(obj)   
        // 试图用reactive包装raw, 使其变成响应式数据
        const state = reactive(raw) 

        function change() {
          state.age = 90
          console.log(state);
        }

        return {state, change}
    }
}
</script>

watchEffect

watchEffect 它与 watch 的区别主要有以下几点:

  1. 不需要手动传入依赖
  2. 每次初始化时会执行一次回调函数来自动获取依赖
  3. 无法获取到原值,只能得到变化后的值
<script>
import {reactive, watchEffect} from 'vue'
export default {
    setup() { 
          const state = reactive({ count: 0, name: 'zs' })

          watchEffect(() => {
          console.log(state.count)
          console.log(state.name)
          /*  初始化时打印:
                  0
                  zs

            1秒后打印:
                  1
                  ls
          */
          })

          setTimeout(() => {
            state.count ++
            state.name = 'ls'
          }, 1000)
    }
}
</script>

没有像 watch 方法一样先给其传入一个依赖,而是直接指定了一个回调函数

当组件初始化时,将该回调函数执行一次,自动获取到需要检测的数据是 state.countstate.name

根据以上特征,我们可以自行选择使用哪一个监听器

getCurrentInstance

我们都知道在Vue2的任何一个组件中想要获取当前组件的实例可以通过 this 来得到,而在Vue3中我们大量的代码都在 setup 函数中运行,并且在该函数中 this 指向的是undefined,那么该如何获取到当前组件的实例呢?这时可以用到另一个方法,即 getCurrentInstance

评论中反馈:在生产环境就没有用,无法得到ctx上下文

<template>
 <p>{{ num }}</p>
</template>
<script>
import {ref, getCurrentInstance} from 'vue'
export default {
    setup() { 
        const num = ref(3)
        const instance = getCurrentInstance()
        console.log(instance)

        return {num}
    }
}
</script>

instance 中重点关注 ctxproxy 属性,这两个才是我们想要的 this。可以看到 ctxproxy 的内容十分类似,只是后者相对于前者外部包装了一层 proxy,由此可说明 proxy 是响应式的

useStore

在Vue2中使用 Vuex,我们都是通过 this.$store 来与获取到Vuex实例,但上一部分说了原本Vue2中的 this 的获取方式不一样了,并且我们在Vue3的 getCurrentInstance().ctx 中也没有发现 $store 这个属性,那么如何获取到Vuex实例呢?这就要通过 vuex 中的一个方法了,即 useStore

// store 文件夹下的 index.js
import Vuex from 'vuex'

const store = Vuex.createStore({
    state: {
      name: '前端印象',
      age: 22
    },
    mutations: {
      ……
    },
    ……
})

// example.vue
<script>
// 从 vuex 中导入 useStore 方法
import {useStore} from 'vuex'
export default {
    setup() { 
        // 获取 vuex 实例
        const store = useStore()

        console.log(store)
    }
}
</script>

然后接下来就可以像之前一样正常使用 vuex

参考:vue3常用api使用

查看原文

赞 16 收藏 12 评论 2

万年打野易大师 发布了文章 · 3月18日

reduce方法高级使用

前端小伙伴儿应该都听过reduce这个数组的方法,总结一下我在开发过程中遇到的reduce的一些好玩儿的用法

老规矩,上MDN:reduce-MDN

简单介绍一下一些重要的点

定义:reduce() 方法对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

示例:

const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue;

// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer));
// expected output: 10

// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5));
// expected output: 15

reducer 函数接收4个参数:

Accumulator (acc) (累计器)
CurrentValue (cur) (当前值)
CurrentIndex (idx) (当前索引)
SourceArray (src) (源数组)

您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值

arr.reduce(callback(accumulator, currentValue, index, array), initialValue)

callback执行数组中每个值 (如果没有提供 initialValue则第一个值除外)的函数,包含四个参数:

`accumulator`:累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或`initialValue`。

`currentValue`:数组中正在处理的元素。

`index`(可选):数组中正在处理的当前元素的索引。 如果提供了`initialValue`,则起始索引号为0,否则从索引1起始。

`array`(可选):调用`reduce()`的数组

reduce为数组中的每一个元素依次执行callback函数,不包括数组中被删除或从未被赋值的元素,接受四个参数:

  • accumulator 累计器
  • currentValue 当前值
  • currentIndex 当前索引
  • array 数组

回调函数第一次执行时,accumulatorcurrentValue的取值有两种情况:
1、如果调用reduce()时提供了initialValueaccumulator取值为initialValuecurrentValue取数组中的第一个值;
2、如果没有提供 initialValue,那么accumulator取数组中的第一个值,currentValue取数组中的第二个值。

注意:官方推荐在reduce使用时提供 initialValue,为了避免错误,更多的详细理解请查阅MDN,下面就用实际的例子来玩儿一下

1、基础的累加累乘

var  arr = [1, 2, 3, 4];
var sum = arr.reduce((prev, cur) => prev + cur, 0)
var mul = arr.reduce((prev, cur) => prev * cur, 1)
console.log(sum); //    10
console.log(mul); //    24

2、对象内的操作

var result = [
    {
        subject: 'math',
        score: 10
    },
    {
        subject: 'chinese',
        score: 20
    },
    {
        subject: 'english',
        score: 30
    }
];

var sum = result.reduce((prev, cur) => {
    return cur.score + prev;
}, 0);
console.log(sum) //60

3、统计数组中元素出现次数

let names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'];

let nameNum = names.reduce((prev, cur)=>{
  if(cur in prev){
    prev[cur]++
  }else{
    prev[cur] = 1 
  }
  return pre
}, {})
console.log(nameNum); //{Alice: 2, Bob: 1, Tiff: 1, Bruce: 1}

4、数组去重

let arr = [1,2,3,4,4,1]
let newArr = arr.reduce((prev, cur)=>{
    if(!prev.includes(cur)){
      return prev.concat(cur)
    }else{
      return prev
    }
}, [])
console.log(newArr);// [1, 2, 3, 4]

// 使用特定的属性值判断去重
arrayDeduplicate<T>(arr: T[], key: string): T[] {
  let result: T[] = [];
  arr.reduce((prev, cur) => {
    if (prev[key] !== cur[key]) {
      result.push(cur);
    }
    return cur;
  }, {});
  return result;
}

5、多维数组降一维数组

let arr = [[0, 1], [2, 3], [4,[5,6,7]]]
const newArr = function(arr){
   return arr.reduce((prev, cur) => prev.concat(Array.isArray(cur) ? newArr(cur) : cur), [])
}
console.log(newArr(arr)); //[0, 1, 2, 3, 4, 5, 6, 7]

6、数组和对象深克隆

const deepClone = param => {
    if (typeof param !== 'object') return
    if (Array.isArray(param)) {
        param.reduce((prev, cur) => (cur instanceof Array ? [...prev, deepClone(cur)] : [...prev, cur]), [])
    } else {
        Object.entries(param).reduce(
            (prev, [key, value]) => (typeof value === 'object' ? { ...prev, [key]: deepClone(value) } : { ...prev, [key]: value }),
            {}
        )
    }
    return param
}

7、封装一个同步顺序执行函数,并返回结果

let fn1 = () => {
    return {
        name: 'lsd',
        age: 18
    }
}
let fn2 = () => {
    return {
        name: 'lbb',
        age: 19
    }
}
let fn3 = () => {
    return {
        name: 'whh',
        age: 20
    }
}

let fnlist = [fn1, fn2, fn3]

let res = fnlist.reduce((prev, cur) => {
    let t = cur()
    if (t) {
        prev.push(t)
    }
    return prev
}, [])
console.log(res)

8、基于7封装异步请求顺序执行,并处理请求结果

let fn1 = () => {
    return new Promise((resolve, reject) => resolve(1))
}
let fn2 = () => {
    return new Promise((resolve, reject) => reject())
}
let fn3 = () => {
    return new Promise((resolve, reject) => resolve(2))
}
let fnlist = [fn1, fn2, fn3]
 let res = fnlist.reduce((prev, cur) => {
    cur().then(
        data => {
            if (data) {
                prev.push(data)
            }
        },
        reason => {
            prev.push('失败')
        }
    )
    return prev
}, [])
console.log(res)
// 应该在then函数中定义onResolve和onRejct函数,如果使用catch捕获错误,会进入下一次事件循环,不是同步执行;此处如果需要异步执行,请自行修改

9、模拟koa洋葱模型

// 每个中间件都能接收到core
function receiveMiddleware(middlewareList) {
    //将中间件队列改造为函数层层嵌套形式
    //[a,b,c,d] => a(b(c(d(core)))) By reduce
    let tiggerPipe = middlewareList.reduce((a, b) => core => a(b(core)))

    let tiggerPipeWitchCore = tiggerPipe(() => {
        console.log('我是核心操作')
    })

    return tiggerPipeWitchCore
}
const VerfiyCsrfToekn = next => lastMDarg => {
    console.log('验证csrf Token')

    next(lastMDarg)

    console.log('验证csrf Token end')
}

const VerfiyAuth = next => lastMDarg => {
    console.log('验证是否登录')

    next(lastMDarg)

    console.log('验证是否登录 end')
}

const VerfiyRoutes = next => lastMDarg => {
    console.log('验证路由匹配')

    next(lastMDarg)

    console.log('验证路由匹配 end')
}
let dispatch = receiveMiddleware([VerfiyCsrfToekn, VerfiyAuth, VerfiyRoutes])
dispatch()

10、带异步控制的中间件

const store = {
    status: { name: '固态空气' },
    getState: () => {
        return this.status
    },
    dispatch: arg => {
        console.log(`我是核心操作,参数=${arg}`)
    }
}

function receiveMiddleware(middlewareList) {
    //拿到中间件队列
    let dispatch = store.dispatch
    let middlewareAPI = {
        dispatch: arg => {
            dispatch(arg)
        },
        getState: store.getState
    }

    //判断中间件数量
    if (middlewareList.length === 0) {
        return dispatch
    }
    //将核心操作当作参数赋予每个中间件
    middlewareList = middlewareList.map(middleware => middleware(middlewareAPI))
    //将中间件队列改造为函数层层嵌套形式
    //[a,b,c,d] => a(b(c(d(core)))) By reduce
    let tiggerPipe = middlewareList.reduce((prev, cur) => reallyDispatch => prev(cur(reallyDispatch)))

    //重写dispatch
    dispatch = tiggerPipe(store.dispatch)
    return dispatch
}

const VerfiyCsrfToekn = middlewareAPI => next => lastMDarg => {
    console.log('验证csrf Token')
    next(lastMDarg)
    console.log('验证csrf Token end')
}

const VerfiyAuth = middlewareAPI => next => lastMDarg => {
    console.log('验证是否登录')
    next(lastMDarg)
    console.log('验证是否登录 end')
}

const VerfiyRoutes = middlewareAPI => next => lastMDarg => {
    console.log('验证路由匹配')
    next(lastMDarg)
    console.log('验证路由匹配 end')
}

const asyncMiddleware = middlewareAPI => next => lastMDarg => {
    console.log('异步中间件-start')
    if (typeof lastMDarg === 'function') {
        lastMDarg(middlewareAPI)
    } else {
        next(lastMDarg)
        console.log('异步中间件-end')
    }
}

let dispatch = receiveMiddleware([VerfiyCsrfToekn, VerfiyAuth, VerfiyRoutes, asyncMiddleware])

let asyncFun = middlewareAPI => {
    setTimeout(() => {
        let test = '我是固态空气'
        middlewareAPI.dispatch(test)
        console.log(middlewareAPI.getState())
    }, 3000)
}

dispatch(asyncFun)
查看原文

赞 22 收藏 16 评论 2

万年打野易大师 发布了文章 · 3月15日

函数复杂度检测和优化-学习

代码质量管控 -- 复杂度检测

背景

代码的复杂度是评估一个项目的重要标准之一。较低的复杂度既能减少项目的维护成本,又能避免一些不可控问题的出现。然而在日常的开发中却没有一个明确的标准去衡量代码结构的复杂程度,大家只能凭着经验去评估代码结构的复杂程度,比如,代码的程度、结构分支的多寡等等。当前代码的复杂度到底是个什么水平?什么时候就需要我们去优化代码结构、降低复杂度?这些问题我们不得而知。
因此,我们需要一个明确的标准去衡量代码的复杂度。

衡量标准

Litmus 是我们团队建设的一个代码质量检测系统,目前包括代码的风格检查、重复率检查以及复杂度检查。litmus 采用代码的 Maintainability(可维护性)来衡量一个代码的复杂度,并且通过以下三个方面来定义一段代码的 Maintainability 的值:

  • Halstead Volume(代码容量)
  • Cyclomatic Complexity(圈复杂度)
  • Lines of Code(代码行数)

根据这三个参数计算出 Maintainability,也就是代码的可维护性,公式如下:

Maintainability Index = MAX(0,(171 - 5.2 * ln(Halstead Volume) - 0.23 * (Cyclomatic Complexity) - 16.2 * ln(Lines of Code))*100 / 171)复制代码

代码行数不做赘述,下面我们具体介绍代码容量、圈复杂的含义以及它们的计算原理

Halstead Volume(代码容量)

代码的容量关注的是代码的词汇数,有以下几个基本概念

参数含义
n1Number of unique operators,不同的操作元(运算子)的数量
n2Number of unique operands,不同的操作数(算子)的数量
N1Number of total occurrence of operators,为所有操作元(运算子)合计出现的次数
N2Number of total occurrence of operands,为所有操作数(算子)合计出现的次数
Vocabularyn1 + n2,词汇数
lengthN1 + N2,长度
Volumelength * Log2 Vocabulary,容量
一个例子
function tFunc(opt) {
    let result = opt + 1;
    return result;
}
// n1:function,let,=,+,return
// n2:tFunc,opt,result,1
// N1: function,let,=,+,return
// N2:tFunc,opt,result,opt,1,result
// Vocabulary = n1 + n2 = 9
// length = N1 + N2 = 11
// Volume =  length * Log2 Vocabulary = 34.869复制代码

Cyclomatic Complexity(圈复杂度)

概念

圈复杂度(Cyclomatic complexity,简写CC)也称为条件复杂度,是一种代码复杂度的衡量标准。由托马斯·J·麦凯布(Thomas J. McCabe, Sr.)于1976年提出,用来表示程序的复杂度,其符号为VG或是M。它可以用来衡量一个模块判定结构的复杂程度,数量上表现为独立现行路径条数,也可理解为覆盖所有的可能情况最少使用的测试用例数。圈复杂度大说明程序代码的判断逻辑复杂,可能质量低且难于测试和 维护。程序的可能错误和高的圈复杂度有着很大关系。

如何计算

如果在控制流图中增加了一条从终点到起点的路径,整个流图形成了一个闭环。圈复杂度其实就是在这个闭环中线性独立回路的个数。

如图,线性独立回路有:

  • e1→ e2 → e
  • e1 → e3 → e

所以复杂度为2
对于简单的图,我们还可以数一数,但是对于复杂的图,这种方法就不是明智的选择了。

计算公式

V(G) = e – n + 2 * p
  • e:控制流图中边的数量(对应代码中顺序结构的部分)
  • n:代表在控制流图中的判定节点数量,包括起点和终点(对应代码中的分支语句)

    • ps:所有终点只计算一次,即使有多个 return 或者 throw
  • p:独立组件的个数

几种常见的语句控制流图

一个例子
function test(index, string) {
       let returnString;
       if (index == 1) {
           if (string.length < 2) {
              return '分支1';
           }
           returnString = "returnString1";
       } else if (index == 2) {
           if (string.length < 5) {
              return '分支2';
           }
           returnString = "returnString2";
       } else {
          return  '分支3'
       }
       return returnString;
}
flow-chart

flow-chart

flow-graph

flow-graph

计算
e(边):9
n(判定节点):6
p:1
V = e - n + 2 * p = 5复制代码

如何优化

主要针对圈复杂度

大方向:减少判断分支和循环的使用

(下面某些例子可能举的不太恰当,仅用以说明这么一种方法)

提炼函数

// 优化前,圈复杂度4
function a (type) {
    if (type === 'name') {
        return `name:${type}`;
    } else if (type === 'age') {
        return `age:${type}`;
    } else if (type === 'sex') {
        return `sex:${type}`;
    }
}

// 优化后,圈复杂度1
function getName () {
    return `name:${type}`;
}
function getAge () {
    return `age:${type}`;
}
function getSex () {
    return `sex:${type}`;
}

表驱动

// 优化前,圈复杂度4
function a (type) {
    if (type === 'name') {
        return 'Ann';
    } else if (type === 'age') {
        return 11;
    } else if (type === 'sex') {
        return 'female';
    }
}

// 优化后,圈复杂度1
function a (type) {
    let obj = {
        'name': 'Ann',
        'age': 11,
        'sex': 'female'
    };
    return obj[type];
}

简化条件表达式

// 优化前,圈复杂度4
function a (num) {
    if (num === 0) {
        return 0;
    } else if (num === 1) {
        return 1;
    } else if (num === 2) {
        return 2;
    } else {
        return 3;
    }
}

// 优化后,圈复杂度2
function a (num) {
    if ([0,1,2].indexOf(num) > -1) {
        return num;
    } else {
        return 3;
    }
}

简化函数

// 优化前,圈复杂度4
function a () {
    let str = '';
    for (let i = 0; i < 10; i++) {
        str += 'a' + i;
    }
    return str
}
function b () {
    let str = '';
    for (let i = 0; i < 10; i++) {
        str += 'b' + i;
    }
    return str
}
function c () {
    let str = '';
    for (let i = 0; i < 10; i++) {
        str += 'c' + i;
    }
    return str
}

// 优化后,圈复杂度2
function a (type) {
    let str = '';
    for (let i = 0; i < 10; i++) {
        str += type + i;
    }
    return str
}

检测工具

本地检测:es6-plato

npm install --save es6-plato
es6-plato -r -d report ./

参考文章:函数复杂度检测,美团

查看原文

赞 1 收藏 1 评论 0

万年打野易大师 发布了文章 · 3月15日

学习vuecli背后的过程

vue create my-vue-project 执行后发生了什么,顺序执行

  • 【系统】系统定位到bin/vue.js文件,通过node bin/vue.js create my-project来执行该文件;
  • 【vue.js】bin/vue.js利用commander来定义命令选项create,将create命令匹配到create方法(lib/create.js),执行该方法;
  • 【create.js】lib/create.js使用Inquirer.js来询问用户,进行项目配置;
  • 【Creator.js】根据用户配置生成package.json文件(基础信息,从项目配置中注入对应的开发依赖devDependencies);
  • 【Creator.js】执行npm i来安装依赖;(PS: 这里封装了常用的npm操作,可以直接拷贝到自己项目中使用)
  • 【Creator.js】加载vue-cli插件(@vue/cli-service是第一个被执行的插件);
  • 【Generator.js】执行所有插件(执行cli-service插件会生成项目文件结构);
  • 【Creator.js】生成README.md文件;

上面就是create命令的基本执行过程,如果我们想扩展create方法,例如按照我们的定义的模板生成目录结构,可以新建一个插件(generator,可以参考cli-service),在插件里生成自定义的目录结构即可。

一,命令执行过程

当你在控制台敲下npm -v并回车的时候,到底发生了什么?

我们先来看看npm这个命令在哪。通过执行which npm,(不了解which命令的同学可以参考这里:linux命令之which),可以看到,npm命令的可执行文件在/usr/local/bin/npm,打开文件夹一看,是个替身,右键“显示原身”(Mac用户的方法,其他用户请搜索“软链接”),就能定位到该命令在哪:/usr/local/lib/node_modules/npm/bin/npm-cli.js,看到是js代码相信大家已经松了一口气,我们先不急着看npm-cli.js里面的源,我们先思考一个问题:

Q1:系统是怎么找到npm这个这个命令的可执行文件的?

A1:了解which命令的同学都知道,which命令会按照PATH变量中的路径顺序来查找可执行文件。执行echo $PATH可以打印出该变量内容:/usr/local/bin:/usr/bin:/bin(例如这是我的部分内容,目录间用‘:’分隔),所以系统会先在/usr/local/bin下面找npm执行文件,/usr/local/bin/npm链接到/usr/local/lib/node_modules/npm/bin/npm-cli.js。所以调用npm命令相当于执行npm-cli.js

总的来说:在控制台执行命令时,系统会先去环境路径(PATH)中找到可执行文件,然后执行该文件

那么,有同学好奇:

Q2:系统又是怎么执行这些文件(例如上面的npm-cli)的呢?

A2:打开npm-cli.js文件,我们能看到的第一行代码就是:#!/usr/bin/env node,这行代码到底有什么用呢?具体可参考:stackoverflow - #!/usr/bin/env到底有什么用?,大致意思是告知系统用什么解释程序来执行该文件,例如#!/usr/bin/env node就是告知系统,npm-cli.js要用node来执行。因此npm -v相当于node /usr/local/lib/node_modules/npm/bin/npm-cli.js -v

$ node /usr/local/lib/node_modules/npm/bin/npm-cli.js -v
6.4.1

二,npm包打造cli的原理

了解完命令执行过程之后,我们就可以打造自己的cli命令了。

①先编写my-cli.js文件:

#!/usr/bin/env node
console.log('Hello cli!');

②在/usr/local/bin下(或者PATH里的任意路径下)创建软链接:

ln -s my-cli.js my-cli

③给my-cli命令添加可执行权限:(若不添加权限,会报错bash: /usr/local/bin/my-cli: Permission denied)

chmod 777 my-cli

④验证效果:

$ my-cli
Hello cli!

在上面的基础上,我们虽然能打造自己的命令,但是这个命令要想给团队使用,就需要每个人都拷贝my-cli.js文件,创建软链接,添加可执行权限,非常繁琐。怎么将自己的命令分发出去给别人使用呢?

我们再往前探索一步,一起打造一个基于npm分发的命令。

我们在下载使用一个npm模块命令的时候,我们会这样:

npm install -g @vue/cli
vue create my-project

全局安装vue-cli这个npm模块之后,我们全局新增了vue命令,这背后到底发生了什么?是npm install帮我们把上面提到的②③步自动执行了(如有错误,欢迎指出)。既然npm已经帮我们完成这些简单但是繁琐的脚本操作,那我们只需要按照npm的规范来配置一下代码即可。流程比较简单,请参考:通过npm包来制作命令行工具的原理

总结一下开发过程:

  1. npm init新建npm模块目录;
  2. 开发命令(例如上面的my-cli.js);
  3. package.json中添加bin字段(bin: { "my-cli": "./my-cli.js" });
  4. 发布npm;
  5. 全局安装即可使用my-cli;

三,VUE-CLI中核心库

vue-cli源码 了解怎么优雅地实现一个cli。

介绍几个vue-cli里面用到的几个库:

  1. commander - 命令行参数解析库;
  2. Inquirer.js - 命令行常用交互形式集合(问答,选择...);
  3. chalk - 在命令行样式美化;
  4. ora - 命令行loader;

commander几乎是开发cli必不可少的工具,原理(参考commander源码),基本使用方法如下:

#!/usr/bin/env node
var program = require('commander');

program
  .version('0.1.0')
  .option('-p, --peppers', 'Add peppers')
  .option('-P, --pineapple', 'Add pineapple')
  .option('-b, --bbq-sauce', 'Add bbq sauce')
  .option('-c, --cheese [type]', 'Add the specified type of cheese [marble]', 'marble')
  .parse(process.argv);

核心流程如下:

1. 通过option定义收集命令的功能选项;

2. parse解析命令参数(有process获得);

3. 由命令参数去匹配前面收集到的功能选项,执行前面的方法(将参数传入);

/**
 * Parse `argv`, settings options and invoking commands when defined.
 *
 * @param {Array} argv
 * @return {Command} for chaining
 * @api public
 */

Command.prototype.parse = function(argv) {
  // implicit help
  if (this.executables) this.addImplicitHelpCommand();

  // store raw args
  this.rawArgs = argv;

  // guess name
  this._name = this._name || basename(argv[1], '.js');

  // github-style sub-commands with no sub-command
  if (this.executables && argv.length < 3 && !this.defaultExecutable) {
    // this user needs help
    argv.push('--help');
  }

  // process argv
  var parsed = this.parseOptions(this.normalize(argv.slice(2)));
  var args = this.args = parsed.args;

  var result = this.parseArgs(this.args, parsed.unknown);

  // executable sub-commands
  var name = result.args[0];

  var aliasCommand = null;
  // check alias of sub commands
  if (name) {
    aliasCommand = this.commands.filter(function(command) {
      return command.alias() === name;
    })[0];
  }

  if (this._execs[name] && typeof this._execs[name] !== 'function') {
    return this.executeSubCommand(argv, args, parsed.unknown);
  } else if (aliasCommand) {
    // is alias of a subCommand
    args[0] = aliasCommand._name;
    return this.executeSubCommand(argv, args, parsed.unknown);
  } else if (this.defaultExecutable) {
    // use the default subcommand
    args.unshift(this.defaultExecutable);
    return this.executeSubCommand(argv, args, parsed.unknown);
  }

  return result;
};

原文参考: cli原理解析

查看原文

赞 1 收藏 1 评论 0

万年打野易大师 发布了文章 · 3月13日

封装一个performance监控类

谷歌浏览器内嵌了性能测试工具Lighthouse,F12打开能看到,使用参考: Lighthouse工具

前端性能监控,设置监控超时的任务,回传到服务器

完整代码如下

// 在src/utils/PerformanceMonitor.js
export default class PerformanceMonitor {
    constructor() {
        // 设置基础毫秒
        this.SEC = 1000
        // 设置超时时差
        this.TIMEOUT = 10 * this.SEC
        // 基础配置,上传数据
        this.config = {}
    }
    init(option) {
        // 向前兼容
        if (!window.performance) return false
        const { url, timeoutUrl, method = 'POST', timeout = 10000 } = option
        this.config = {
            url,
            timeoutUrl,
            method,
            timeout
        }
    }
    // 上报两项核心数据
    logPackage() {
        const { url, timeoutUrl, method } = this.config
        const domComplete = this.getLoadTime()
        const timeoutRes = this.getTimeoutRes(this.config.timeout)
        // 上报页面加载时间
        this.log(url, { domeComplete }, method)
        // 上报超时加载的资源列表
        if (timeoutRes.length) {
            this.log(timeoutUrl, { timeoutRes }, method)
        }
    }
    // 上报数据
    log(url, data = {}, type = 'POST') {
        const method = type.toLowerCase()
        const urlToUse = method === 'get' ? `${url}?${this.makeItStr(data)}` : url
        const body = method === 'get' ? {} : { body: this.convert2FormData(data) }
        // 接口,可用自己项目封装的axios
        const init = {
            method,
            ...body
        }
        // 请求接口上报服务器
        fetch(urlToUse, init).catch(e => console.log(e))
    }
    getTimeoutRes(limit = this.TIMEOUT) {
        const isTimeout = this.setTime(limit)
        // 获取资源加载时间
        const resourceTimes = performance.getEntriesByType('resource')
        // 生成超时资源列表
        return resourceTimes.filter(item => isTimeout(this.getDomLoadTime(item))).map(getName)
    }
    getDomLoadTime() {
        // 获取页面加载时间
        const [{ domComplete }] = performance.getEntriesByType('navigation')
        return domComplete
    }
    setTime(limit = this.TIMEOUT) {
        time => time >= limit
    }
    getLoadTime({ startTime, responseEnd }) {
        return responseEnd - startTime
    }
    getName({ name }) {
        return name
    }
    // 生成表单数据
    convert2FormData(data = {}) {
        Object.entries(data).reduce((last, [key, value]) => {
            if (Array.isArray(value)) {
                return value.reduce((lastResult, item) => {
                    lastResult.append(`${key}[]`, item)
                    return lastResult
                }, last)
            }
            last.append(key, value)
            return last
        }, new FormData())
    }

    // 拼接 GET 时的url
    makeItStr(data = {}) {
        Object.entries(data)
            .map(([k, v]) => `${k}=${v}`)
            .join('&')
    }
}

为了监测工具不占用主线程的 JavaScript 解析时间。因此,最好在页面触发 onload 事件后,采用异步加载的方式:

// 在项目的入口文件的底部,js按流程解析
const log = async () => {
  const PM = await import('/src/utils/PerformanceMonitor.js')
  PM.init({ url: 'xxx', timeoutUrl: 'xxxx' })
  PM.logPackage()
}
const oldOnload = window.onload
window.onload = e => { if (oldOnload && typeof oldOnload === 'string') {
    oldOnload(e)
  } // 尽量不影响页面主线程
  if (window.requestIdleCallback) {
    window.requestIdleCallback(log)
  } else {
    setTimeout(log)
  }
}
查看原文

赞 2 收藏 1 评论 0

万年打野易大师 发布了文章 · 3月4日

鼠标移入放大图片预览效果实现

商城项目中,有鼠标移入图片放大的功能,研究一下实现

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Image zoom</title>
    </head>
    <style>
        body {
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
        }

        #image {
            width: 300px;
            height: 300px;
            background-color: #000;
            background-image: url(https://placekitten.com/900/900);
            background-size: 300px 300px;
            background-repeat: no-repeat;
        }

        #image[zoomed] {
            background-size: 900px 900px;
            background-position: calc(var(--x) * 100%) calc(var(--y) * 100%);
        }
    </style>
    <body>
        <div id="image"></div>
    </body>
    <script>
        let el = document.querySelector('#image')

        // PC端操作
        el.addEventListener('mouseenter', enterHandler)
        el.addEventListener('mousemove', moveHandler)
        el.addEventListener('mouseleave', leaveHandler)

        // 移动端操作
        el.addEventListener('touchstart', enterHandler)
        el.addEventListener('touchmove', moveHandler)
        el.addEventListener('touchend', leaveHandler)

        function enterHandler(e) {
            e.target.setAttribute('zoomed', 1)
            moveHandler(e)
        }

        function moveHandler(e) {
            // getBoundingClientRect用于获取元素相对于视窗的位置集合。集合中有top, right, bottom, left等属性
            let rect = e.target.getBoundingClientRect()
            let offsetX, offsetY
            let isH5 = ['touchstart', 'touchmove', 'touchend'].includes(e.type)
            // 是移动端,并且touches事件存在
            if (isH5 && e.touches[0]) {
                offsetX = e.touches[0].pageX - rect.left
                offsetY = e.touches[0].pageY - rect.top

                e.preventDefault()
            } else {
                // PC端
                offsetX = e.offsetX
                offsetY = e.offsetY
            }
            // 元素的位置信息
            let x = offsetX / rect.width
            let y = offsetY / rect.height
            // 设置元素属性,用于计算background-position的位置
            e.target.style.setProperty('--x', x)
            e.target.style.setProperty('--y', y)
        }

        function leaveHandler(e) {
            e.target.removeAttribute('zoomed')
            moveHandler(e)
        }
    </script>
</html>

具体效果复制下去打开看看

查看原文

赞 2 收藏 1 评论 0

万年打野易大师 发布了文章 · 2月25日

JS高级运算符

在代码精简优化过程中,我们总会想着要去简练我们的代码,尽量做到用最少的代码完成最好的功能

下面介绍4个JS开发优化的高级运算符使用

1、(param ? res1 : res2)三元运算符

三元运算符,又叫条件运算符

接受三个运算数:条件 ? 条件为真时要执行的表达式 : 条件为假时要执行的表达式

基本示例:

function isChecked(checked) {
    return checked ? '是' : '否' 
}
console.log(isChecked(true)) // => 是
console.log(isChecked(false)) // => 否

三元运算符用于变量赋值

let time = 0
let have = (time > 23) ? '睡觉' : '工作' 
console.log(have) // => '工作'

三元运算符用于空赋值的行为

let x = 1
let x = (x !== null || x !== undefined) ? x : 2
console.log(x) // => 1

用在函数中

function getValue(x, y) {
    return (x == null || x == undefined) ? y : x 
}
getValue(null, 8) // => 8
getValue(4, 8) // => 4

2、?? 非空运算符

如果第一个参数不是 null/undefined(译者注:这里只有两个假值,但是 JS 中假值包含:未定义 undefined、空对象 null、数值 0、空数字 NaN、布尔 false,空字符串''),将返回第一个参数,否则返回第二个参数。

基本示例:

null ?? 5 // => 5
3 ?? 5 // => 3

开发业务场景优化:
某些时候,数值为0或者为空字符串"",不应该舍弃0和空字符串""

// 优化前
let prev = 1
let current = 0
let noAccount = null
let future = false
function test(param) {
    return param || `不存在`
}
console.log(test(prev)) // => 1
console.log(test(current)) // => 不存在
console.log(test(noAccount)) // => 不存在
console.log(test(future)) // => 不存在

// 优化后
let prev = 1
let current = 0
let noAccount = null
let future = false
function test(param) {
    return param ?? `不存在`
}
console.log(test(prev)) // => 1
console.log(test(current)) // => 0
console.log(test(noAccount)) // => 不存在
console.log(test(future)) // => false

概括地说 ?? 运算符允许我们在忽略错误值(如 0 和空字符串、false)的同时指定默认值。

3、??= 空赋值运算符

与非空运算符相关

let x = null
let y = 5
console.log(x ??= y) // => 5
console.log(x = (x ?? y)) // => 5

仅当值为 null 或 undefined 时,此赋值运算符才会赋值。
上面的例子强调了这个运算符本质上是空赋值的语法糖(译者注,类似的语法糖:a = a + b 可写成 a += b )。
接下来,让我们看看这个运算符与默认参数(译者注,默认参数是 ES6 引入的新语法,仅当函数参数为 undefined 时,给它设置一个默认值)的区别:

function settingsWithNull(options) {
    options.speed ??= 1
    options.diff ??= 'easy' 
    return options
}
function settingsWithDefaultParams(speed=1, diff='easy') {
    return {speed, diff}
}
settingsWithNullish({speed: null, diff: null}) // => {speed: 1, diff: 'easy'}
settingsWithDefaultParams(undefined, null) // => {speed: null, diff: null}

4、?. 链判断运算符

链判断运算符?. 允许开发人员读取深度嵌套在对象链中的属性值,而不必验证每个引用。当引用为空时,表达式停止计算并返回 undefined。

let book = {
    name: 'js高教4',
    content: {
        first: 'hello world',
        second: 'good work'
    }
}
console.log(book.price?.zh) // => undefined
查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 399 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-04-12
个人主页被 7.5k 人浏览