- 正确使用JavaScript数组
首先,我们可以简单地认为缩进就是代码复杂性的指标(尽管很粗略)。因为缩进越多代表我们的嵌套越多,因此代码就越复杂。今天就拿数组来做具体的例子,来展示以下如何抛弃循环,减少缩进,正确地使用JavaScript数组。 “…a loop is an imperative control structure that’s hard to reuse and difficult to plug in to other operations. In addition, it implies code that’s constantly changing or mutating in response to new iterations.” -Luis Atencio 循环 我们都知道,循环结构就是会无形地提高代码的复杂性。那我们现在看看在JavaScript上的循环是如何工作的。 在JavaScript上至少有四五种循环的方式,其中最基础的就是[代码]while[代码]循环了。讲例子前,先设定一个函数和数组: [代码]// oodlify :: String -> String function oodlify(s) { return s.replace(/[aeiou]/g, 'oodle'); } const input = [ 'John', 'Paul', 'George', 'Ringo', ]; [代码] 那么,如果我们现在要使用[代码]oodlify[代码]函数操作一下数组里每个元素的话,如果我们使用[代码]while[代码]循环的话,是这样子的: [代码]let i = 0; const len = input.length; let output = []; while (i < len) { let item = input[i]; let newItem = oodlify(item); output.push(newItem); i = i + 1; } [代码] 这里就有许多无谓的,但是又不得不做的工作。比如用[代码]i[代码]这个计数器来记住当前循环的位置,而且需要把[代码]i[代码]初始化成0,每次循环还要加一;比如要拿[代码]i[代码]和数组的长度[代码]len[代码]对比,这样才知道循环到什么时候停止。 这时为了让清晰一点,我们可以使用JavaScript为我们提供的[代码]for[代码]循环: [代码]const len = input.length; let output = []; for (let i = 0; i < len; i = i + 1) { let item = input[i]; let newItem = oodlify(item); output.push(newItem); } [代码] [代码]for[代码]循环的好处就是把与业务代码无关的计数逻辑放在了括号里面了。 对比起[代码]while[代码]循环虽有一定改进,但是也会发生类似忘记给计数器[代码]i[代码]加一而导致死循环的情况。 现在回想一下我们的最初目的:就只是给数组的每一个元素执行一下[代码]oodlify[代码]函数而已。其实我们真的不想关什么计数器。 因此,[代码]ES2015[代码]就为我们提供了一个全新的可以让我们忽略计数器的循环结构- [代码]for...of[代码]循环 : [代码]let output = []; for (let item of input) { let newItem = oodlify(item); output.push(newItem); } [代码] 这个方式是不是简单多了!我们可以注意到,计数器和对比语句都没了。 如果我们这就满足的话,我们的目标也算完成了,代码的确是简洁了不少。 但是其实,我们可以对JavaScript的数组再深入挖掘一下,更上一层楼。 Mapping [代码]for...of[代码]循环的确比[代码]for[代码]循环简洁不少,但是我们仍然写了一些不必要的初始化代码,比如[代码]output[代码]数组,以及把每个操作过后的值push进去。 其实我们有办法写得更简单明了一点的。不过,现在我们来放大一下这个问题先: 如果我们有两个数组需要使用[代码]oodlify[代码]函数操作的话呢? [代码]const fellowship = [ 'frodo', 'sam', 'gandalf', 'aragorn', 'boromir', 'legolas', 'gimli', ]; const band = [ 'John', 'Paul', 'George', 'Ringo', ]; [代码] 很明显,我们就要这样循环两个数组: [代码]let bandoodle = []; for (let item of band) { let newItem = oodlify(item); bandoodle.push(newItem); } let floodleship = []; for (let item of fellowship) { let newItem = oodlify(item); floodleship.push(newItem); } [代码] 这的确可以完成我们的目标,但是这样写得有点累赘。我们可以重构一下以减少重复的代码。因此我们可以创建一个函数: [代码]function oodlifyArray(input) { let output = []; for (let item of input) { let newItem = oodlify(item); output.push(newItem); } return output; } let bandoodle = oodlifyArray(band); let floodleship = oodlifyArray(fellowship); [代码] 这样是不是好看多了。但是问题来了,如果我们要使用其他函数来操作这个数组的话呢? [代码]function izzlify(s) { return s.replace(/[aeiou]+/g, 'izzle'); } [代码] 这时,我们前面创建的[代码]oodlifyArray[代码]函数帮不了我们了。不过如果我们这时创建[代码]izzlifyArray[代码]函数的话,代码不就又有许多重复的部分了吗? [代码]function oodlifyArray(input) { let output = []; for (let item of input) { let newItem = oodlify(item); output.push(newItem); } return output; } function izzlifyArray(input) { let output = []; for (let item of input) { let newItem = izzlify(item); output.push(newItem); } return output; } [代码] 这两个函数是不是及其相似呢。 如果此时我们将其抽象成一个模式的话呢:我们希望传入一个数组和一个函数,然后映射每个数组元素,最后输出一个数组。这个模式就称为[代码]mapping[代码]: [代码]function map(f, a) { let output = []; for (let item of a) { output.push(f(item)); } return output; } [代码] 其实我们并不需要自己手动写[代码]mapping[代码]函数,因为JavaScript提供了内置的[代码]map[代码]函数给我们使用,此时我们的代码是这样的: [代码]let bandoodle = band.map(oodlify); let floodleship = fellowship.map(oodlify); let bandizzle = band.map(izzlify); let fellowshizzle = fellowship.map(izzlify); [代码] Reducing 此时[代码]map[代码]是很方便了,但是并不能覆盖我们所有的循环需要。 如果此时我们需要累计数组中的所有数组呢。我们假设有一个这样的数组: [代码]const heroes = [ {name: 'Hulk', strength: 90000}, {name: 'Spider-Man', strength: 25000}, {name: 'Hawk Eye', strength: 136}, {name: 'Thor', strength: 100000}, {name: 'Black Widow', strength: 136}, {name: 'Vision', strength: 5000}, {name: 'Scarlet Witch', strength: 60}, {name: 'Mystique', strength: 120}, {name: 'Namora', strength: 75000}, ]; [代码] 如果我们要找到[代码]strength[代码]最大的那个的元素的话,使用[代码]for...of[代码]循环是这样的: [代码]let strongest = {strength: 0}; for (hero of heroes) { if (hero.strength > strongest.strength) { strongest = hero; } } [代码] 如果此时我们想累计一下所有的[代码]strength[代码]的话,循环里面就是这样的了: [代码]let combinedStrength = 0; for (hero of heroes) { combinedStrength += hero.strength; } [代码] 这两个例子我们都需要初始化一个变量来配合我们的操作。合并两个例子的话就是这样的: [代码]function greaterStrength(champion, contender) { return (contender.strength > champion.strength) ? contender : champion; } function addStrength(tally, hero) { return tally + hero.strength; } // 例子 1 const initialStrongest = {strength: 0}; let working = initialStrongest; for (hero of heroes) { working = greaterStrength(working, hero); } const strongest = working; // 例子 2 const initialCombinedStrength = 0; working = initialCombinedStrength; for (hero of heroes) { working = addStrength(working, hero); } const combinedStrength = working; [代码] 此时我们可以抽象成这样一个函数: [代码]function reduce(f, initialVal, a) { let working = initialVal; for (item of a) { working = f(working, item); } return working; } [代码] 其实这个方法JavaScript也提供了内置函数,就是[代码]reduce[代码]函数。这时代码是这样的: [代码]const strongestHero = heroes.reduce(greaterStrength, {strength: 0}); const combinedStrength = heroes.reduce(addStrength, 0); [代码] Filtering 前面的[代码]map[代码]函数是将数组的全部元素执行同个操作之后输出一个同样大小的数组; [代码]reduce[代码]则是将数组的全部值执行操作之后,最终输出一个值。 如果此时我们只是需要提取几个元素到一个数组内呢?为了更好得解释,我们来扩充一下之前的例子: [代码]const heroes = [ {name: 'Hulk', strength: 90000, sex: 'm'}, {name: 'Spider-Man', strength: 25000, sex: 'm'}, {name: 'Hawk Eye', strength: 136, sex: 'm'}, {name: 'Thor', strength: 100000, sex: 'm'}, {name: 'Black Widow', strength: 136, sex: 'f'}, {name: 'Vision', strength: 5000, sex: 'm'}, {name: 'Scarlet Witch', strength: 60, sex: 'f'}, {name: 'Mystique', strength: 120, sex: 'f'}, {name: 'Namora', strength: 75000, sex: 'f'}, ]; [代码] 现在假设我们要做的两件事: 找到[代码]sex = f[代码]的元素 找到[代码]strength > 500[代码]的元素 如果使用[代码]for...of[代码]循环的话,是这样的: [代码]let femaleHeroes = []; for (let hero of heroes) { if (hero.sex === 'f') { femaleHeroes.push(hero); } } let superhumans = []; for (let hero of heroes) { if (hero.strength >= 500) { superhumans.push(hero); } } [代码] 由于有重复的地方,那么我们就把不同的地方抽取出来: [代码]function isFemaleHero(hero) { return (hero.sex === 'f'); } function isSuperhuman(hero) { return (hero.strength >= 500); } let femaleHeroes = []; for (let hero of heroes) { if (isFemaleHero(hero)) { femaleHeroes.push(hero); } } let superhumans = []; for (let hero of heroes) { if (isSuperhuman(hero)) { superhumans.push(hero); } } [代码] 此时就可以抽象成JavaScript内置的[代码]filter[代码]函数: [代码]function filter(predicate, arr) { let working = []; for (let item of arr) { if (predicate(item)) { working = working.concat(item); } } } const femaleHeroes = filter(isFemaleHero, heroes); const superhumans = filter(isSuperhuman, heroes); [代码] Finding [代码]filter[代码]搞定了,那么如果我们只要找到一个元素呢。 的确,我们同样可以使用[代码]filter[代码]函数完成这个目标,比如: [代码]function isBlackWidow(hero) { return (hero.name === 'Black Widow'); } const blackWidow = heroes.filter(isBlackWidow)[0]; [代码] 当然我们也同样会发现,这样的效率并不高。因为[代码]filter[代码]函数会过滤所有的元素,尽管在前面已经找到了应该要找到的元素。因此我们可以写一个这样的查找函数: [代码]function find(predicate, arr) { for (let item of arr) { if (predicate(item)) { return item; } } } const blackWidow = find(isBlackWidow, heroes); [代码] 正如大家所预期那样,JavaScript也同样提供了内置方法[代码]find[代码]给我们,因此我们最终的代码是这样的: [代码]const blackWidow = heroes.find(isBlackWidow); [代码] 总结 这些JavaScript内置的数组函数就是很好的例子,让我们学会了如何去抽象提取共同部分,以创造一个可以复用的函数。 现在我们可以用内置函数完成几乎所有的数组操作。分析一下,我们可以看出每个函数都有以下特点: 摒弃了循环的控制结构,使代码更容易阅读。 通过使用适当的方法名称描述我们正在使用的方法。 减少了处理整个数组的问题,只需要关注我们的业务代码。 在每种情况下,JavaScript的内置函数都已经将问题分解为使用小的纯函数的解决方案。通过学习这几种内置函数能让我们消除几乎所有的循环结构,这是因为我们写的几乎所有循环都是在处理数组或者构建数组或者两者都有。因此使用内置函数不仅让我们在消除循环的同时,也为我们的代码增加了不少地可维护性。 本文翻译自:JavaScript Without Loops
2020-03-11 - 总结JavaScript的检测方式
我们都知道,JavaScript是有5种原始类型的: number string boolean undefined null 检测原始类型 检测原始类型的最佳选择是使用[代码]typeof[代码] [代码]typeof 'abc' // string typeof 123 // number typeof true // boolean typeof undefined // undefined [代码] [代码]typeof[代码]有一个好处就是:未声明的变量也不会报错 [代码]typeof someVariable //此时someVariable是未定义,返回undefined [代码] 检测复合类型 复合类型内置有(不只以下几种,只是举例说明): Object Array Date Error 当我们使用[代码]typeof[代码]检测的时候,就会看到都是返回object [代码]typeof {} //object typeof [] //object typeof new Date() //object typeof new Error() //object [代码] 此时的最佳选择是使用[代码]instanceof[代码] [代码]var today = new Date() today instanceof Date // true [代码] 到这里好像全部的检测类型都搞定了。 但是,检测类型并不能这么简单地分为原始类型和复合类型,因为复合类型会涉及到构造函数的问题。 检测函数 当我们的页面内嵌了其他的frame时,问题就来了。因为不同的frame的构造函数是独立的,即会发生以下问题: [代码]// 在frame A定义的函数test function test(){} // 在frame B检测 test instanceof Function //false // 而使用typeof则可以正确返回 typeof test // function [代码] 故检测函数的时候,最佳选择是使用[代码]typeof[代码] 检测数组 数组的问题和函数是一样的,因为不同的构造函数。而此时[代码]typeof[代码]也不灵了,因为只返回object。 Douglas Crockford则提供了一种叫duck typing(鸭式辩型)的方式: [代码]function isArray(value){ return typeof value.sort === 'function'; } [代码] 其实,这种方式是默认的认为只有数组才有sort方法。其实传入任何有sort方法的对象也是返回true的。因此这个方法并不完美。 最终的解决方案也是ECMAScript 5的实现方案,就是来自Kangax大神的方法: [代码]function isArray(value){ return Object.prototype.toString.call(value) === '[object Array]' ; } [代码] 这个方法能完美地辨别是否为数组。 在ECMAScript 5则可以使用Array的内置方法: [代码]Array.isArray([]) // true [代码] 检测属性 我发现,在检测一个对象的属性是否存在的时候,常常是这样的: [代码]if(object.someProps){ //一些逻辑 } [代码] 或者是这样的: [代码]if(object.someProps != null){ //一些逻辑 } [代码] 或者是这样的: [代码]if(object.someProps != undefined){ //一些逻辑 } [代码] 其实以上都是有问题的!因为以上方式都忽略了object可能存在假值的情况(即是属性存在,但是等于null或者undefined或者0或者false或者空字符串等等)。因此最佳的方式是使用[代码]in[代码]运算符: [代码]if(someProps in object){ //一些逻辑 } [代码] 以上检测数据类型的所有方式。 参考: 《Maintainable JavaScript》
2020-03-11 - 论函数复用的几大姿势
开发过小程序的朋友们应该都遇到这样的情况,可能很多个页面有相同的函数,例如[代码]onShareAppMessage[代码],有什么最佳实践吗,应该如何处理呢? 本次开发技巧,我从以下几种解决办法剖析: 将它复制粘贴到每个地方(最烂的做法) 抽象成一个公共函数,每个[代码]Page[代码]都手动引用 提取一个behavior,每个页面手动注入 通过[代码]Page[代码]封装一个新的[代码]newPage[代码],以后每个页面都通过[代码]newPage[代码]注册 劫持Page函数,注入预设方法,页面仍可使用[代码]Page[代码]注册 复制粘贴大法 这是最直观,也是初学者最常用到的办法。也是作为工程师最不应该采取的办法。这有一个致命的问题,如果某一天,需要改动这个函数,岂不是要将所有的地方都翻出来改,所以这个办法直接否决。 抽象公共函数 这种方式,解决了复制粘贴大法的致命问题,不需要改动很多地方,只需要改动这个抽象出来的函数即可。但是其实,这个方式不便捷,每次新增页面都需要手动引入这个函数。 以下都通过[代码]onShareAppMessage[代码]方法举例。 假设在[代码]app.js[代码]通过[代码]global[代码]注册了[代码]onShareAppMessage[代码]方法: [代码]// app.js global.onShareAppMessage = function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } } [代码] 那么此时每次新增的Page都需要这样引入: [代码]// page.js Page({ ...global.onShareAppMessage, data: {} }) [代码] 这样的缺点也是非常明显的: 创建新页面时,容易遗忘 如果多个相同的函数,则需要每个独立引入,不方便 提取Behavior 将多个函数集成到一个对象中,每个页面只需要引入这个对象即可注入多个相同的函数。这种方式可以解决 抽象公共函数 提到的 缺点2。 大致的实现方式如下: 同样在[代码]app.js[代码]通过[代码]global[代码]注册一个[代码]behavior[代码]对象: [代码]// app.js global.commonPage = { onShareAppMessage: function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } }, onHide: function() { // do something } } [代码] 在新增的页面注入: [代码]// page.js Page({ data: {}, ...global.commonPage, }}) [代码] 缺点仍然是,新增页面时容易遗忘 封装新Page 封装新的[代码]Page[代码],然后每个页面都通过这个新的[代码]Page[代码]注册,而不是采用原有的[代码]Page[代码]。 同理,在[代码]app.js[代码]先封装一个新的[代码]Page[代码]到全局变量[代码]global[代码]: [代码]// app.js global.newPage = function(obj) { let defaultSet = { onShareAppMessage: function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } }, onShow() { // do something } } return Page({...defaultSet, ...obj}) } [代码] 往后在每个页面都使用新的[代码]newPage[代码]注册: [代码]// page.js global.newPage({ data: {} }) [代码] 好处即是全新封装了[代码]Page[代码],后续只需关注是否使用了新的[代码]Page[代码]即可;此外大家也很清晰知道这个是采用了新的封装,避免了覆盖原有的[代码]Page[代码]方法。 我倒是觉得没什么明显缺点,要是非要鸡蛋里挑骨头的话,就是要显式调用新的函数注册页面。 劫持Page 劫持函数其实是挺危险的做法,因为开发人员可能会在定位问题时,忽略了这个被劫持的地方。 劫持[代码]Page[代码]的做法,简单的说就是,覆盖[代码]Page[代码]这个函数,重新实现[代码]Page[代码],但这个新的[代码]Page[代码]内部仍会调用原有的[代码]Page[代码]。说起来可能有点拗口,通过代码看就一目了然: [代码]// app.js let originalPage = Page Page = function(obj) { let defaultSet = { onShareAppMessage: function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } }, onShow() { // do something } } return originalPage({ ...defaultSet, ...obj}) } [代码] 通过这种方式,不改变页面的注册方式,但可能会让不了解底层封装的开发者感到困惑:明明没注册的方法,怎么就自动注入了呢? 这种方式的缺点已经说了,优点也很明显,不改变任何原有的页面注册方式。 其实这个是一个挺好的思路,在一些特定的场景下,会有事半功倍的效果。
2020-03-23