这是我参与「第四届青训营 」笔记创作活动的的第7天
如何评价一段代码的好坏
大家好,这里是这几天效率不高的Vic,今天给大家带来月影老师JS课程的下午部分。下午的课程其实相对于上午的课程轻松了不少,其主要分为两个部分,第一个部分为如何评价一段代码的好坏,第二个部分为JS算法的相关。今天给大家讲解第一部分。
引言
首先,我们来看一段代码:
// 判断一个mat2d矩阵是不是单位矩阵
function isUnit(m) {
return m[0] === 1 && m[1] === 0 && m[2] === 0
&& m[3] === 1 && m[4] === 0 && m[5] === 0;
}
当看到这段代码的第一眼,相信大家都是一脸懵逼的状态。这写的是啥?我是谁?我在哪儿?
从可读性方面而言,这段代码绝对不是一段好的代码,之前我们在“组件封装”的部分有讲过,一个好的组件需要做到兼顾四个方面,分别为封装性、正确性、扩展性、复用性。很明显,这段代码的扩展性与复用性并不好。
然而这是月影老师在spritejs中所写的一段真实代码,其地址如下:对应地址
看到这里,我们不禁思考这样一个问题,写代码最应该关注什么,这里有五个选项,分别为:风格、效率、约定、使用场景、设计。
在这里使用Leftpad事件的例子作为讲解。
Leftpad事件
我们首先来看一段代码:
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模块粒度;
- 代码风格;
- 代码质量/效率;
在这里,月影老师认为这段代码的问题并不大,本身来说,这段代码具有很好的可读性。针对上述三个问题,月影老师是如下分析的:
针对第一个问题,他觉得这是由于当年的模块化并不完善,tree shaking功能不太行,因此不得不将模块粒度弄得这么小。虽然现在看来,这个模块的粒度太细了,但在当年来说是无可厚非的。
针对第二个问题,月影老师认为这段代码的可读性非常好,认为好的代码本身就是注解。
最后的代码效率问题,这里的时间复杂度为O(N),但是结合实际使用情况来说,并不会经常使用前面拼接很多字符的这种情况,因此问题也不是很大。
那么如何改进这段代码呢?
代码改进
我们的第一个改进方法是不再使用while循环进行拼接,而是使用repeat函数进行拼接工作,其代码如下:
function leftpad(str, len, ch) {
str = "" + str;
const padLen = len - str.length;
if(padLen <= 0) {
return str;
}
return (""+ch).repeat(padLen)+str;
}
为什么使用repeat函数能够提升性能呢?我们来看一下repeat函数的代码:
/*! https://mths.be/repeat v1.0.0 by @mathias */
'use strict';
var RequireObjectCoercible = require('es-abstract/2019/RequireObjectCoercible');
var ToString = require('es-abstract/2019/ToString');
var ToInteger = require('es-abstract/2019/ToInteger');
module.exports = function repeat(count) {
var O = RequireObjectCoercible(this);
var string = ToString(O);
var n = ToInteger(count);
// Account for out-of-bounds indices
if (n < 0 || n == Infinity) {
throw RangeError('String.prototype.repeat argument must be greater than or equal to 0 and not be Infinity');
}
var result = '';
while (n) {
if (n % 2 == 1) {
result += string;
}
if (n > 1) {
string += string;
}
n >>= 1;
}
return result;
};
这段代码乍一看可能使人比较眼花,但当我们把其中的关键部分提取出来就比较简单了:
var result = '';
while (n) {
if (n % 2 == 1) {
result += string;
}
if (n > 1) {
string += string;
}
n >>= 1;
}
return result;
可以看到,在这段代码中,采用的是将重复次数n转化为二进制数字形式进行运算的方式。当n存在的时候开始循环,每次循环首先判断最后一位是否为1,若为1则拼接一次此时的string至结果中。每次移位操作之前,由于是二次幂,因此需要对字符串string进行一次重复拼接。
通过这种方法,我们就将时间复杂度从O(N)转化为了O(logN)。
当然,还存在着更深一步的优化:
/**
* String.prototype.repeat() polyfill
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat#Polyfill
*/
if (!String.prototype.repeat) {
String.prototype.repeat = function(count) {
'use strict';
if (this == null)
throw new TypeError('can't convert ' + this + ' to object');
var str = '' + this;
// To convert string to integer.
count = +count;
// Check NaN
if (count != count)
count = 0;
if (count < 0)
throw new RangeError('repeat count must be non-negative');
if (count == Infinity)
throw new RangeError('repeat count must be less than infinity');
count = Math.floor(count);
if (str.length == 0 || count == 0)
return '';
// Ensuring count is a 31-bit integer allows us to heavily optimize the
// main part. But anyway, most current (August 2014) browsers can't handle
// strings 1 << 28 chars or longer, so:
if (str.length * count >= 1 << 28)
throw new RangeError('repeat count must not overflow maximum string size');
var maxCount = str.length * count;
count = Math.floor(Math.log(count) / Math.log(2));
while (count) {
str += str;
count--;
}
str += str.substring(0, maxCount - str.length);
return str;
}
}
将其中的核心代码提取出来:
var maxCount = str.length * count;
count = Math.floor(Math.log(count) / Math.log(2));
while (count) {
str += str;
count--;
}
str += str.substring(0, maxCount - str.length);
return str;
这段方法的思想其实和上一个方法类似,不同的是,这里采用的是二次幂的数学运算方法。
接下来,我们再来看一个例子,这个例子是一个交通灯的案例。
交通灯案例
我们首先用菜鸟思路来写一个信号灯切换的函数,其代码如下:
const traffic = document.getElementById('traffic');
(function reset(){
traffic.className = 's1';
setTimeout(function(){
traffic.className = 's2';
setTimeout(function(){
traffic.className = 's3';
setTimeout(function(){
traffic.className = 's4';
setTimeout(function(){
traffic.className = 's5';
setTimeout(reset, 1000)
}, 1000)
}, 1000)
}, 1000)
}, 1000);
})();
这段代码的问题是很大的,其内部嵌套了太多的函数,因此陷入了回调地狱的问题,代码的可读性非常差。因此我们需要对其进行改进。
首先我们将这段代码中的数据抽象出来。代码如下:
const traffic = document.getElementById('traffic');
const stateList = [
{state: 'wait', last: 1000},
{state: 'stop', last: 3000},
{state: 'pass', last: 3000},
];
function start(traffic, stateList){
function applyState(stateIdx) {
const {state, last} = stateList[stateIdx];
traffic.className = state;
setTimeout(() => {
applyState((stateIdx + 1) % stateList.length);
}, last)
}
applyState(0);
}
start(traffic, stateList);
在这段代码中,我们将数据抽象出来定义在一个状态列表stateList中,定义了一个开始函数start,并在这个函数中定义定义了一个用于切换状态的函数applyState。
之前的我们已经学过,在JS中写一个好的组件还需要进行过程抽象,因此接下来我们使用过程抽象对代码进行重构。
首先对过程进行分析,在这个交通灯的组件中,我们有如下几个过程:1. 设置交通灯,2. 等待时间,3. 状态切换。
首先将等待时间抽象出来:
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
将状态切换抽象出来:
function poll(...fnList){
let stateIndex = 0;
return async function(...args){
let fn = fnList[stateIndex++ % fnList.length];
return await fn.apply(this, args);
}
}
将设置交通灯抽象出来:
async function setState(state, ms){
traffic.className = state;
await wait(ms);
}
完整代码如下:
const traffic = document.getElementById('traffic');
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function poll(...fnList){
let stateIndex = 0;
return async function(...args){
let fn = fnList[stateIndex++ % fnList.length];
return await fn.apply(this, args);
}
}
async function setState(state, ms){
traffic.className = state;
await wait(ms);
}
let trafficStatePoll = poll(setState.bind(null, 'wait', 1000),
setState.bind(null, 'stop', 3000),
setState.bind(null, 'pass', 3000));
(async function() {
// noprotect
while(1) {
await trafficStatePoll();
}
}());
当然,我们也可以使用异步加函数式编程的方法,其代码如下:
const traffic = document.getElementById('traffic');
function wait(time){
return new Promise(resolve => setTimeout(resolve, time));
}
function setState(state){
traffic.className = state;
}
async function start(){
//noprotect
while(1){
setState('wait');
await wait(1000);
setState('stop');
await wait(3000);
setState('pass');
await wait(3000);
}
}
start();
回到最初的例子
下面回到我们最初的例子,我们看到之前那段代码:
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 null;
}
this[_layerTransformInvert] = mat2d.invert(m);
return this[_layerTransformInvert];
}
这段代码尽管看起来可读性较差,但在我们需要的场景中,这是效率最高的方式。因此我们在实际开发中评价一段代码的好坏需要结合实际场景来进行判断。若在效率需求并不高的情况下,我们应以代码的可读性作为第一关注点。