如何写好JS | 青训营笔记

96 阅读9分钟

这是我参与「第四届青训营 」笔记创作活动的第2天

一、写好JavaScript的三个原则

  • 各司其责

    让HTML、CSS、JavaScript职能分离

  • 组件封装

    好的UI组件具备正确性、扩展性、复用性

  • 过程抽象

    应用函数式编程思想

1. 各司其职

HTML负责结构

CSS负责样式表现

JavaScript负责行为

image-20220726095121522.png 要保证结构、表现、行为分离。

场景:模式切换

点击按钮,切换场景的白天/黑夜模式

版本一代码:

const btn = document.getElementById('modeBtn');
  btn.addEventListener('click', (e) => {
    const body = document.body;
    if(e.target.innerHTML === '🌞') {
      body.style.backgroundColor = 'black';
      body.style.color = 'white';
      e.target.innerHTML = '🌜';
    } else {
      body.style.backgroundColor = 'white';
      body.style.color = 'black';
      e.target.innerHTML = '🌞';
    }
  });

版本一直接在js中控制了style的具体内容,js和css没有分离开。

对于以上这段代码,主要存在以下几点问题:

  • 样式和行为没有分离,即没有做到职责分离
  • 全局污染
  • 复用性不强
  • 封装性不好

版本二

 const btn = document.getElementById('modeBtn');
  btn.addEventListener('click', (e) => {
    const body = document.body;
    if(body.className !== 'night') {
      body.className = 'night';
    } else {
      body.className = '';
    }
  });

相比版本一,通过class切换比通过style切换分离程度更高。将黑夜模式和白天模式通过class写好,然后通过label的点击事件控制className的切换。

版本三

 <input id="modeCheckBox" type="checkbox">
  <div class="content">
    <header>
      <label id="modeBtn" for="modeCheckBox"></label>
      <h1>深夜食堂</h1>
    </header>
    <main>
      <div class="pic">
        <img src="https://p2.ssl.qhimg.com/t0120cc20854dc91c1e.jpg">
      </div>
      <div class="description">
        <p>
            这是一间营业时间从午夜十二点到早上七点的特殊食堂。这里的老板,不太爱说话,却总叫人吃得热泪盈眶。在这里,自卑的舞蹈演员偶遇隐退多年舞界前辈,前辈不惜讲述自己不堪回首的经历不断鼓舞年轻人,最终令其重拾自信;轻言绝交的闺蜜因为吃到共同喜爱的美食,回忆起从前的友谊,重归于好;乐观的绝症患者遇到同命相连的女孩,两人相爱并相互给予力量,陪伴彼此完美地走过了最后一程;一味追求事业成功的白领,在这里结交了真正暖心的朋友,发现真情比成功更有意义。食物、故事、真情,汇聚了整部剧的主题,教会人们坦然面对得失,对生活充满期许和热情。每一个故事背后都饱含深情,情节跌宕起伏,令人流连忘返 [6]  。
        </p>
      </div>
    </main>
  </div>
  #modeCheckBox {
    display: none;
  }

  #modeCheckBox:checked + .content {
    background-color: black;
    color: white;
    transition: all 1s;
  }

利用了CSS的高级功能,通过伪类选择器来匹配元素的状态。

  1. 首先在内容板块前面添加一个checkbox,用于控制模式;
  2. 通过label的for属性,将label与checkbox联动起来;
  3. 通过checkbox的伪类选择器来匹配元素状态,并修改样式;
  4. 最后通过display:none把checkbox隐藏起来即可。

版本三通过css直接完成了样式的切换,完全与js分离。

总结

  • HTML、CSS、JavaScript各司其职
  • 应当避免不必要的由JavaScript直接操作样式
  • 可以用样式的class来表示状态
  • 纯展示类交互寻求零JS方案

2. 组件封装

组件是指Web页面上抽出来一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元。好的组件具备封装性正确性扩展性复用性

场景:图片轮播

  • HTML结构:列表结构,可以通过无序列表ul将图片加载进来;

  • CSS表现:

    • 使用 CSS 绝对定位将图片重叠在同一个位置
    • 轮播图切换的状态使用修饰符(modifier)
    • 轮播图的切换动画使用 CSS transition
  • JavaScript行为:

    • 定义Slider类,再封装接口;

      • getSelectedItem ()
      • getSelectedItemIndex ()
      • slideTo ( id )
      • slideNext ()
      • slidePrevious ()
    • 控制流:自定义事件来绑定状态,进行解耦。

总结:写组件的基本方法

  1. HTML结构设计

  2. CSS展现效果

  3. JavaScript行为设计

  • API功能
  • Event控制流

重构

解耦:

  • 插件化

    • 将控制元素抽取成插件
    • 插件与组件之间通过依赖注入方式进行联系。(即通过插件实现功能,然后将其注册到组件中,实现组件和插件的联系)
  • 模板化

    • 将HTML结构和CSS样式进行模板化(动态地修改组件)
    • 更易于扩展
  • 抽象化——组件框架

    • 将组件通用模型抽象出来
    • 可以进一步考虑父子组件,将子组件作为父组件的插件来使用。

3. 过程抽象

  • 用来处理局部细节控制的一些方法

  • 过程抽象,是一种思维方法,也是一种编程范式

  • 函数式编程思想的基础应用。

    比如:为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来,写为Once函数。这个过程我们称为过程抽象

高阶函数

如果一个函数return另外一个函数,则它为高阶函数。

HOF:以函数作为参数、以函数作为返回值,常用于作为函数装饰器 image-20220726161307135.png

function HOF0(fn){
    return function(...args){
        return fn.apply(this,args);
    }
}

常用的高阶函数:

Once函数:可以防止函数不必要的重复执行,只让其执行一次。

function once(fn) {
    return function(...args) {
      if(fn) {
        const ret = fn.apply(this, args);
        fn = null;
        return ret;
      }
    }
  }

Throttle节流函数:节流的意思就是函数在一段时间内的多次调用,仅第一次有效。

Debounce防抖函数:函数在一段时间内的多次调用,仅使得最后一次调用有效。

  • 节流---王者荣耀释放技能
  • 防抖---王者荣耀回城

Consumer/2

Iterative函数:迭代方法

为什么要用高阶函数?

image-20220726182342168.png 事实上,在一个系统中,纯函数越多时,这个系统的可维护性是越好的。

纯函数: 如果输入的值是唯一的,且输出的值也是唯一的,那它就是纯函数。比如:

function add(x, y) {
    return x + y;
}
add(1, 2); // 3

非纯函数:就是输入和输出的值不是唯一的,那它就不是纯函数。比如:

let x = 10;
function foo() {
    return x++;
}
function bar() {
    return x * 2;
}
​
foo();
foo();
bar();
bar();

假设我们稍微调整了函数的顺序,那么它的执行结果都是不一样的,这种就称为了非纯函数

之所以要使用纯函数的原因在于,它能使得我们代码的可维护性和可扩展性变得更好。

编程范式

  • 命令式编程语言

    • 面向过程(fortran、C)
    • 面向对象(C++、Java)
  • 声明式编程语言

    • 逻辑式
    • 函数式编程

js既可以命令式,也可以函数式。对于命令式编程来说,它指的是命令机器如何去做事情 (how) ,这样不管你想要的是什么 (waht) ,它都会按照你的命令实现。

而对于声明式编程来说,它旨在告诉机器你想要的是什么 (what) ,让机器想出如何去做 (how) ,这种方式的可扩展性会更高。

二、如何评价代码的好坏?

参考:【青训营】- 紧跟月影大佬的步伐,一起来学习如何写好JS(下) - 掘金 (juejin.cn)

首先我们先来看一段代码。具体代码如下:

//判断一个mat2d矩阵是否是单位矩阵
function isUnitMatrix2d(m) {
    return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
}

我们先来思考一个问题:上面的代码写的好不好?为什么?

其实,上面这段代码是一段真实的代码,其来源于一个开源库。具体地址:spritejs

get layerTransformInvert() {
    if(this[_layerTransformInvert]) return this[_layerTransformInvert];
    const m = this.transformMatrix;
    if(m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0) {
        return nul l;
    }
    this[_layerTransformInvert] = mat2d.invert(m);
    return this[_layerTransformInvert];
}

现在,我们来分析一下上面这段代码写的好不好。具体分析结果如下:

单从代码优雅性的角度来看的话,这段代码确实不够优雅。

但是呢,上面这个库是一个图形库,且这段代码负责的是在渲染之前,计算我们图层的 transform 矩阵的逻辑代码。

也就是说,我们在计算每一帧的时候,都需要进行一个计算。因此呢,在这样的一个场景下,我们首先要去关注的是如何达到性能最优化

而与其他任何类型的写法来比,以上这种写法能够达到性能最大化,所以,以上这段代码在这样的一个场景下是没有任何问题滴。

同时呢,如果是对于其他场景来说,如果堆性能优化没有这么敏感的话,可以不用这么写。

所以,一般来说,代码的好坏要结合它的使用场景来分析

三、写代码最应该关注什么?

参考:【青训营】- 紧跟月影大佬的步伐,一起来学习如何写好JS(下) - 掘金 (juejin.cn)

写代码我们应该要注重风格效率约定使用场景(算法)设计等方面;

  • 风格:选择什么风格都没有错,关键是风格要统一(分号、行尾花括号等等);
  • 效率:在写代码时要考虑什么样的代码写起来效率是最高的,能写高效率的代码就不要写低效率的代码;当然,也要追求一个平衡就是,要结合它的场景来使用;
  • 约定:在开发前,团队要约定好代码规范和风格,比如 eslintairbnb 等等;

四、当年的 Left-pad 事件

参考:【青训营】- 紧跟月影大佬的步伐,一起来学习如何写好JS(下) - 掘金 (juejin.cn)

我们来了解下当年 githubLeft-pad 事件。先来这个事件中的一段代码,具体如下

function leftpad (str, len, ch) {
	str = String(str);
	
	var i = -1;
	
	if (!ch && ch !== 0) ch = '';
	
	len = len - str.length;
	
	while(++i < len) {
		str = ch - str;
	}
	
	return str;
}

这个作者想要实现的功能就是,比如说我现在有一段字符串,然后呢,我想要把这段字符串拼接成同样长度的字符串。

这个使用场景通常会放在一些展示类的地方,比如排序。当时这个模块一开始被用于很多的 npm 包中,但是后面被作者下线了,所以引起了很大的风波,因为很多人在用的库突然被下线了,试想,开发者岂不是要哭辽。

那这个事件本身的槽点呢,主要有以下三点:

  • npm模块粒度
  • 代码风格
  • 代码质量和代码效率

如果要考虑效率的话,那么我们可以对代码进行改进。比如:

function leftpad(str, len, ch = '') {
    str = "" + str;
    // 判断要补充的代码长度
    const padLen = len - str.length;
    
    if(padLen <= 0) {
        return str;
    }else {
        return ("" + ch).repeat(padLen) + str;
    }
}

通过这样的改进,使得代码更简洁,同时也极大的提升了运行效率