【常更】日常开发中的一些小技巧

926 阅读6分钟

js部分

从字符串中快速提取数字

 const str = "12.6px";
 
 const numInt = parseInt(str);
 // numInt: 12
 
 const numFloat = parseFloat(str);
 // numInt: 12.6

当数组/对象中的某一项需要根据条件来决定是否存在时

对象

断言+对象展开运算符

    let isSleep = true;
    const obj = {
      name: "logan",
      ...isSleep && {
        age: 230
      },
    };
    
    obj为{
      name: "logan",
      age: 230
    }

数组

三目+数组展开运算符(断言表达式会存在一个undefined,数组默认不会过滤掉)

    const hasSecond = true
    const arr = [
        {
            num: 1
        },
        ...hasSecond ?  [{
            num: 2
        }] : []
    ]

用length属性删除数组

    const arr = [0, 1, 2, 3, 4, 5];
    arr.length = 0; // arr变为[]
在vue2中,数据劫持对于数组的操作,是监听了数组原型中的所有能改变数组的方法(push,pop,shift,unshift,splice,sort,reverse),所以直接改变length虽然改变了数组的值,但是不会触发watcher更新视图。

所幸在vue3中,vue团队使用Proxy替代Object.defineProperty解决了该问题,静静等待vue3的正式版发布。

逗号操作符

逗号操作符 对它的每个操作数求值(从左到右),并返回最后一个操作数的值。

    const history = [];
    // 将数据处理后放入列表,同时返回数据本身
    const push = val => (history.push(memory(val)), val);

解构表达式常用到的地方

for of表达式行内解构

    const cacheList = [
        {
            id: 1,
            date: "2020-05-18",
            timeout: 2569
        },
        {
            id: 3,
            date: "2020-04-16",
            timeout: 7863
        },
        {
            id: 2,
            date: "2020-03-21",
            timeout: 6387
        }
    ]
    
    for(let {id, date, timeout} of cacheList) {
        console.log(id);
        console.log(date);
        console.log(timeout);
    }

取出对象剩余属性【扩展运算符】

vue组件向下传递props时有语法糖$attrs$listener, 但是react并没有提供这个方案,所以当props需要跨好几层传递时,为了避免让我们的组件成为props的搬运者,可以利用解构的方式来做。

代码如下

    // 父组件要把数据传递给孙组件
    function Parent() {
      const [list, setList] = useState([]);
      const produce = [
        {
          id: 0,
          name: "x23"
        }
      ];
     
      return (
        <child list={list} produce={produce} change={setList}/>
      )
    }
    
    // 子组件利用对象的解构操作向下传递
    function Child(props) {
      const {list, ...rest} = props;
      
      return (
        <Grandson {...rest} />
      )
    }
    
    // 孙组件成功接收
    function GrandSon(props) {
      return (
        <>
          <ul>
            {produce.map(({id, name}) => <Cell key={id} title={name}>)}
          </ul>
          <button onClick={change}></button>
        </>
      )
    }

同样,这种思路的书写方式在vue中也可以派上用场,比如你写了一个table组件,要加入很多位置的自定义字段,这里可以用v-bind去做。

(这里的启发来源于Layui的table源码部分)

    <th
        v-for="({title, ...rest}) in col"
        :key="title"
        v-bind="rest"
    >
        {{title}}
    </th>

参数直接解构,减少不必要的临时变量声明

    window.addEventListener("wheel", function({clientX, clientY}) {
        console.log({clientX, clientY});
    });

动态属性名和属性名表达式

    const type = "email";
    const params = {
        username: "James Rhodes",
        password: "war machine Rox with an X",
        [type]: "Lodi@stark.com",
    }
    
    const key = "password";
    console.log(params[key]); // 读取params的password属性,结果为war machine Rox with an X

比如你要在vue中动态取值(vue(2.6.0+)的动态attribute)

<component :[key]="value"></component>

取值时使用默认值

||

    function Jarvis(options) {
        this.name = options.sir || "Tony Stark";
    }

解构表达式

    function Jarvis(options) {
        const {sir = "Tony Stark"} = options;
        this.name = sir;
        // ps: 注意区分,取出的属性加别名是这样写
        // const {sir:name} = options;
        // this.name = name;
    }

(可忽略)ES11提供的??运算

    function Jarvis(options) {
        this.name = options.sir ?? "Tony Stark";
    }

cssText替代dom.style.xxx

    const dom = document.querySelector(".wrapper");
    dom.style.cssText = "width:20px;height:20px;border:solid 1px gray;";

使用Array的工厂函数创建特定长度的数组

Array.from(arrayLike[, mapFn[, thisArg]])

  • arrayLike 想要转换成数组的伪数组对象或可迭代对象。
  • mapFn 可选 如果指定了该参数,新数组中的每个元素会执行该回调函数。
  • thisArg 可选 可选参数,执行回调函数 mapFn 时 this 对象。
    Array.from({ length: 10 }, (v, i) => i + 1);
    // 输出 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

目前笔者常用该特性来mock数据

    app.get('/getProduces', function (req, res) {
        res.send({
            code: 200,
            data: Array.from({length: 6}, (v, i) => ({
                name: `produce - ${i}`,
                url: `/view/produce/${i}.html`,
                type: Math.round(Math.random())
                    ? "new"
                    : "default",
                num: Math.round(Math.random() * 10000),
            }))
        });
    });

向数组头部添加数据

有时候或许会用数组做一个栈结构

    const arr = ["Ferrie", "Natasha"];
    arr.unshift("Tony");
    // arr变成["Tony", "Ferrie", "Natasha"]

减少作用域查找

例如以下运行在全局作用域的代码:

    <script>
        var header = document.querySelector("#header");
        header.style.cssText = `position: sticky;top: ${offsetTop}px;`;
    </script>

一个 script 标签中,代码的上下文都在全局作用域,由于全局作用域比较复杂,编译器在处理时会比较慢。 就像上面的 header 变量,第二行在使用的时候,需要在全局作用域查找一下这个变量, 假设 header 是在一个循环里面使用,那可能就会有效率问题了。 所以尽量避免全局作用域,选择局部作用域:

举个🌰,闭包作用域

    <script>
      !function(){
        var header = document.querySelector("#header");
        header.style.cssText = `position: sticky;top: ${offsetTop}px;`;
      }()
    </script>

避免滥用闭包

纵使闭包很好用也别滥用,闭包中变量的作用域是独立的。 编译器在查找变量时,会从当前作用域向父级作用域依次查找。 所以闭包嵌套的深度会影响变量的查找时间。

再举个🌰,我们有一个flow函数,函数内部定义了一个animate函数, animate的作用域内,存在一个常量location,而函数内还需要用到targetrate,这两个变量的作用域在其父级作用域内,查找时间比location的时间要久,

    function flow(target, rate = 0) {
      rate += 0.1;
      function animate() {
        const location = getLocation(target.location, 0.5);
        return Tween.get(target).to({rate});
      }
      return animate();
    }

这里可以通过参数的形式,把targetrate传给animate。 这样一来,他们的作用域变成同级,读取时间也只是编译器处理常量与变量的差异了。

    function flow(target, rate = 0) {
      rate += 0.1;
      function animate(target, rate) {
        return Tween.get(target).to({rate});
      }
      return animate(target, rate);
    }

这个就启示我们如果某个全局变量需要频繁地被使用的时候,可以用一个局部变量缓存一下,(🌰:我自己来了)

    function call() {
        if(window.location.origin !== "xxxxxx") return;
        
        const hash = getHash(window.location.href);
        
        const url = `/api/radio/${window.location.search}/${hash}`;
    }

多处使用window.location,我们可以在函数作用域内缓存一下,

    function call() {
        const {location} = window;
        
        if(location.origin !== "xxxxxx") return;
        
        const hash = getHash(location.origin, location.hash);
        
        const url = `/api/radio/${location.search}/${hash}`;
    }

=== 替代 ==

两个原因

  • 1.js是一个弱类型语言,==会忽略数据类型,为了更好的可读性选择更精确的===运算
  • 2.更好的编译速度 编译器在处理==时会先将两个变量转换成同类型,然后再比较值是否相等; 而处理===时会先判断类型是否相同,然后再决定要不要比较值相等。

字符串模板

为了更好的可读性,放弃字符串拼接或数组.join(""),选择字符串模板

  const str = `原价:${price},优惠减:${discount},最终价:${(price - discount).toFixed(2)}`;

变量修饰符的优先级:const > let > var

why?

var

  1. 可以重新声明
  var starLord = "fool";
  starLord = "very fool";
  var starLord = "foolish";
  1. 可以变量提升
    function () {
      console.log(name); // 输出undefined,不会报错
      
      var name = "Tony"
    }
  1. 跨越块作用域
  • 块作用域:{ },条件、循环语句的{ }
    {
            var Tony = "Iron Man";
    }
    console.log(Tony); // 输出Iron Man
    
    if(true) {
      var Steve  = "caption";
    }
    console.log(Steve ); // caption
    
    let i = 0;
    while(i++ < 2) {
      if(i === 1) {
        var thor = "Fatty";
      }
    }
    console.log(thor); // Fatty

let

只能在当前块级作用域使用

const

  1. const更语义化,提醒开发者该变量不应该被改变;
  2. 编译器会对const进行优化,读取常量的速度要快于变量,其本质区别在于编译器内部处理的方式不同。

正确姿势使用Promise.all

在ES11的Promise.allSettled出现之前,Promise.all收集promise时,任意一个出现错误都无法得到处理结果,这时候可以对all收集的promise对象进行catch操作手动处理err

  const p1 = new Promise(...)
  const p2 = new Promise(...)
  const p3 = new Promise(...)
  Promise.all([p1,p2,p3]).then(...).catch(err => err)

多个 loading 的冲突问题

引入一个用于计数的变量loadingCount

loading 时先判断数量是否为 0,满足条件则+1,然后执行,

close 时先-1,若数量为 0,则执行

let loadingCount = 0
function open() {
    if(loadingCount > 0) return

    loadingCount++;
    loading.open();
}
function close() {
    loadingCount--
    if(loadingCount === 0) {
        loading.close()
    }
}

vue中 触发父组件的事件后,重新拿到props

    props: {
        msgList: ...
    }
   this.$emit("sendMsg", id);
   // 在下一个更新队列完成后判断父组件传递过来的数据是否有值
   this.$nextTick(() => {
       this.showDetail = this.msgList && this.msgList.length > 0;
   });

canvas画线和清除功能的冲突问题

清除时记得调用context.beginPath(),否则之前的线路还存在

css部分

调试UI小技巧

试着给调试区域(#app)加入这个样式

    #app {
        outline: 1px #000 solid;
    }

可自动适应宽/高的背景图片拼接方案

dom的背景是一张图片,其宽高还要由内容决定,
这时怎么做到图片自适应高度呢?
(闪烁是因为我们改变宽高引起了浏览器的重绘与重排)

    <style>
    .box {
      width: 100px;
      height: auto; // 高度随意更改
      background: url(/static/img/box-bg-top.png) top no-repeat,
                  url(/static/img/box-bg-bottom.png) bottom no-repeat;
    }
    .box:before {
      content: '';
      position: absolute;
      z-index: -1;
      top: 80px;
      right: 0;
      bottom: 80px;
      left: 0;
      background: url(/static/img/box-bg-center.png) repeat;
    }
    </style>
    
    <body>
      <div class="box">
      </div>
    </body>

实现思路:dom本身通过background-position和background-repeat实现图片的头和尾,然后用伪元素实现中间的自适应部分

合理运用pointer-events

在一个超级大的dom中,只有一个小小的子dom需要鼠标事件,我们可以这样做

    .parent {
        width: 600px;
        height: 500px;
        pointer-events: none;
    }
    .close {
        float: right;
        width: 10px;
        height: 10px;
        pointer-events: visible;
    }

css选择器

第x个往后(不包含第x个)

    li:nth-of-type(n+x) {
        ...
    }

第x个及之前(包含第x个)

    li:nth-of-type(-n+x) {
        ...
    }

not选择器(排除某一个)

    li:not(:last-child) {
        background: #08627f;
    }

字体间距及居中

大多数人只设置letter-spacing,这样会导致文字右边距变大从而不居中,这时候需要首行缩进(text-indent)同样的值来保证居中效果

 .text {
    letter-spacing: 12px;
    text-indent: 12px;
 }

transform

替代left/top,可有效避免重绘

重绘重排会引起闪屏,效果在上面案例可以看到

    .dot {
        transform: translate3d(10px, 16px, 0);
    }

使用translateZ强制开启GPU渲染

    .dot {
        transform: translateZ(0);
    }

但开启GPU加速可能会出现浏览器频繁闪烁或抖动的情况,
可这样解决:

    .dot {
        -webkit-backface-visibility:hidden;
        -webkit-perspective:1000;
    }

绝对定位,宽高不确定情况下的 居中问题

    .box {
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
    }

给thead加border

    table {
        border-collapse:collapse;
    }
    thead {
        border: 1px #0af solid;
    }

图片不失真

    img {
        object-fit: cover;
    }

精灵图适应屏幕

    @screen_width: 1920;
    img {
        background-image: url("...");
        background-position: 15px 30px;
        background-size: calc(100vw / @screen_width * 160);
    }

input去边框

    border: none;
    outline: none;

用户调整dom大小

兼容性略差

    .resize {
        resize:both;
    }

css平滑滚动

    scroll-container {
        scroll-behavior: smooth;
    }
    <nav>
        <a href="#page-1">1</a>
        <a href="#page-2">2</a>
        <a href="#page-3">3</a>
    </nav>
    <scroll-container>
        <scroll-page id="page-1">1</scroll-page>
        <scroll-page id="page-2">2</scroll-page>
        <scroll-page id="page-3">3</scroll-page>
    </scroll-container>

高度是百分比时的文字垂直居中问题

借助table-cell和vertical-align

    <style>
        .parent {
            display: table;
            width: 600px;
            height: 50%
        }
        .parent a {
            display: table-cell;
            vertical-align: middle;
        }
    </style>
    <div class="parent">
        <a href="#page-1">1</a>
        <a href="#page-2">2</a>
        <a href="#page-3">3</a>
    </nav>

var取元素属性值

class,id以及keyframes都可以直接获取元素上的属性

    <template>
        <div v-for="(item, index) in list"
             :style="{'--y': `${index*60}deg`}"
             class="around-item u_3d">
            <div class="content">{{item}}</div>
        </div>
    </template>
    <style>
        @keyframes around-self {
            0% {
                transform: rotateY(calc(var(--y) * -1));
            }
            100% {
                transform: rotateY(calc(var(--y) * -1 - 360deg));
            }
        }
        .content {
            animation: around-self 10s linear 0s infinite;
        }
    </style>

其他

在webpack配置loader时,通过test、exclude、include缩小搜索范围

    module: {
        rules: [
          {
            test: /\.vue$/,
            loader: 'vue-loader',
            include: [resolve('src/components'), resolve('src/modules')],
            options: vueLoaderConfig
          },
          {
            test: /\.js$/,
            loader: 'babel-loader',
            include: [resolve('src'), resolve('node_modules/webpack-dev-server/client')],
            exclude: /node_modules/
          },
        ]
    }

script、style标签的修饰符

preload

资源要在dom渲染之前加载 通常是一些负责首屏渲染的逻辑

    <link rel="preload" href="index.css">

prefetch、dns-prefetch

页面加载完成,此时没有任务需要做,这时候加载 通常是一些与用户交互有关的脚本,比如第三方的弹出层

    <link rel="prefetch" href="next.css">
    <link rel="dns-prefetch" href="//example.com">

无修饰符(异步无阻塞)

暂停解析document. 请求脚本 执行脚本 继续解析document

<script src="index.js" ></script>

defer

在英文中是“延迟”的意思, 不影响document解析的同时,并行下载home.js和user.js 按照页面中的顺序,在其他的同步脚本执行后,DOMContentLoaded事件执行前,依次去执行home.js和user.js。

<script src="home.js" defer></script>
<script src="user.js" defer></script>

async

不影响document解析的同时, 并行下载home.js和user.js 当脚本下载完后立即执行,脚本的执行顺序不确定,执行阶段不确定,可能再DOMContentLoaded执行前,也可能在执行后

<script src="home.js" async></script>
<script src="user.js" async></script>

图片格式选择webp

体积小,请求速度快,但是存在兼容问题,需要手动处理