木纸鸢

木纸鸢 查看完整档案

北京编辑辽宁科技大学  |  自动化 编辑zzz  |  fe 编辑填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

木纸鸢 提出了问题 · 2019-06-14

长单词换行, 如何加连接符

当英文单词比较长时, 折断换行的时候想加一个"-"

word-break: break-all

这个只能折断换行, 该怎么加连接符呢, 求教

关注 2 回答 1

木纸鸢 收藏了文章 · 2019-02-17

深拷贝的终极探索(99%的人都不知道)

划重点,这是一道面试必考题,我靠这道题刷掉了多少面试者✧(≖ ◡ ≖✿)嘿嘿

首先这是一道非常棒的面试题,可以考察面试者的很多方面,比如基本功,代码能力,逻辑能力,而且进可攻,退可守,针对不同级别的人可以考察不同难度,比如漂亮妹子就出1☆题,要是个帅哥那就得上5☆了,(*^__^*) 嘻嘻……

无论面试者多么优秀,漂亮的回答出问题,我总能够潇洒的再抛出一个问题,看着面试者露出惊异的眼神,默默一转身,深藏功与名

本文我将给大家破解深拷贝的谜题,由浅入深,环环相扣,总共涉及4种深拷贝方式,每种方式都有自己的特点和个性

深拷贝 VS 浅拷贝

再开始之前需要先给同学科普下什么是深拷贝,和深拷贝有关系的另个一术语是浅拷贝又是什么意思呢?如果对这部分部分内容了解的同学可以跳过

其实深拷贝和浅拷贝都是针对的引用类型,JS中的变量类型分为值类型(基本类型)和引用类型;对值类型进行复制操作会对值进行一份拷贝,而对引用类型赋值,则会进行地址的拷贝,最终两个变量指向同一份数据

// 基本类型
var a = 1;
var b = a;
a = 2;
console.log(a, b); // 2, 1 ,a b指向不同的数据

// 引用类型指向同一份数据
var a = {c: 1};
var b = a;
a.c = 2;
console.log(a.c, b.c); // 2, 2 全是2,a b指向同一份数据

对于引用类型,会导致a b指向同一份数据,此时如果对其中一个进行修改,就会影响到另外一个,有时候这可能不是我们想要的结果,如果对这种现象不清楚的话,还可能造成不必要的bug

那么如何切断a和b之间的关系呢,可以拷贝一份a的数据,根据拷贝的层级不同可以分为浅拷贝和深拷贝,浅拷贝就是只进行一层拷贝,深拷贝就是无限层级拷贝

var a1 = {b: {c: {}};

var a2 = shallowClone(a1); // 浅拷贝
a2.b.c === a1.b.c // true

var a3 = clone(a3); // 深拷贝
a3.b.c === a1.b.c // false

浅拷贝的实现非常简单,而且还有多种方法,其实就是遍历对象属性的问题,这里只给出一种,如果看不懂下面的方法,或对其他方法感兴趣,可以看我的这篇文章

function shallowClone(source) {
    var target = {};
    for(var i in source) {
        if (source.hasOwnProperty(i)) {
            target[i] = source[i];
        }
    }

    return target;
}

最简单的深拷贝

深拷贝的问题其实可以分解成两个问题,浅拷贝+递归,什么意思呢?假设我们有如下数据

var a1 = {b: {c: {d: 1}};

只需稍加改动上面浅拷贝的代码即可,注意区别

function clone(source) {
    var target = {};
    for(var i in source) {
        if (source.hasOwnProperty(i)) {
            if (typeof source[i] === 'object') {
                target[i] = clone(source[i]); // 注意这里
            } else {
                target[i] = source[i];
            }
        }
    }

    return target;
}

大部分人都能写出上面的代码,但当我问上面的代码有什么问题吗?就很少有人答得上来了,聪明的你能找到问题吗?

其实上面的代码问题太多了,先来举几个例子吧

  • 没有对参数做检验
  • 判断是否对象的逻辑不够严谨
  • 没有考虑数组的兼容

(⊙o⊙),下面我们来看看各个问题的解决办法,首先我们需要抽象一个判断对象的方法,其实比较常用的判断对象的方法如下,其实下面的方法也有问题,但如果能够回答上来那就非常不错了,如果完美的解决办法感兴趣,不妨看看这里吧

function isObject(x) {
    return Object.prototype.toString.call(x) === '[object Object]';
}

函数需要校验参数,如果不是对象的话直接返回

function clone(source) {
    if (!isObject(source)) return source;

    // xxx
}

关于第三个问题,嗯,就留给大家自己思考吧,本文为了减轻大家的负担,就不考虑数组的情况了,其实ES6之后还要考虑set, map, weakset, weakmap,/(ㄒoㄒ)/~~

其实吧这三个都是小问题,其实递归方法最大的问题在于爆栈,当数据的层次很深是就会栈溢出

下面的代码可以生成指定深度和每层广度的代码,这段代码我们后面还会再次用到

function createData(deep, breadth) {
    var data = {};
    var temp = data;

    for (var i = 0; i < deep; i++) {
        temp = temp['data'] = {};
        for (var j = 0; j < breadth; j++) {
            temp[j] = j;
        }
    }

    return data;
}

createData(1, 3); // 1层深度,每层有3个数据 {data: {0: 0, 1: 1, 2: 2}}
createData(3, 0); // 3层深度,每层有0个数据 {data: {data: {data: {}}}}

当clone层级很深的话就会栈溢出,但数据的广度不会造成溢出

clone(createData(1000)); // ok
clone(createData(10000)); // Maximum call stack size exceeded

clone(createData(10, 100000)); // ok 广度不会溢出

其实大部分情况下不会出现这么深层级的数据,但这种方式还有一个致命的问题,就是循环引用,举个例子

var a = {};
a.a = a;

clone(a) // Maximum call stack size exceeded 直接死循环了有没有,/(ㄒoㄒ)/~~

关于循环引用的问题解决思路有两种,一直是循环检测,一种是暴力破解,关于循环检测大家可以自己思考下;关于暴力破解我们会在下面的内容中详细讲解

一行代码的深拷贝

有些同学可能见过用系统自带的JSON来做深拷贝的例子,下面来看下代码实现

function cloneJSON(source) {
    return JSON.parse(JSON.stringify(source));
}

其实我第一次简单这个方法的时候,由衷的表示佩服,其实利用工具,达到目的,是非常聪明的做法

下面来测试下cloneJSON有没有溢出的问题,看起来cloneJSON内部也是使用递归的方式

cloneJSON(createData(10000)); // Maximum call stack size exceeded

既然是用了递归,那循环引用呢?并没有因为死循环而导致栈溢出啊,原来是JSON.stringify内部做了循环引用的检测,正是我们上面提到破解循环引用的第一种方法:循环检测

var a = {};
a.a = a;

cloneJSON(a) // Uncaught TypeError: Converting circular structure to JSON

破解递归爆栈

其实破解递归爆栈的方法有两条路,第一种是消除尾递归,但在这个例子中貌似行不通,第二种方法就是干脆不用递归,改用循环,当我提出用循环来实现时,基本上90%的前端都是写不出来的代码的,这其实让我很震惊

举个例子,假设有如下的数据结构

var a = {
    a1: 1,
    a2: {
        b1: 1,
        b2: {
            c1: 1
        }
    }
}

这不就是一个树吗,其实只要把数据横过来看就非常明显了

    a
  /   \
 a1   a2        
 |    / \         
 1   b1 b2     
     |   |        
     1  c1
         |
         1       

用循环遍历一棵树,需要借助一个栈,当栈为空时就遍历完了,栈里面存储下一个需要拷贝的节点

首先我们往栈里放入种子数据,key用来存储放哪一个父元素的那一个子元素拷贝对象

然后遍历当前节点下的子元素,如果是对象就放到栈里,否则直接拷贝

function cloneLoop(x) {
    const root = {};

    // 栈
    const loopList = [
        {
            parent: root,
            key: undefined,
            data: x,
        }
    ];

    while(loopList.length) {
        // 深度优先
        const node = loopList.pop();
        const parent = node.parent;
        const key = node.key;
        const data = node.data;

        // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
        let res = parent;
        if (typeof key !== 'undefined') {
            res = parent[key] = {};
        }

        for(let k in data) {
            if (data.hasOwnProperty(k)) {
                if (typeof data[k] === 'object') {
                    // 下一次循环
                    loopList.push({
                        parent: res,
                        key: k,
                        data: data[k],
                    });
                } else {
                    res[k] = data[k];
                }
            }
        }
    }

    return root;
}

改用循环后,再也不会出现爆栈的问题了,但是对于循环引用依然无力应对

破解循环引用

有没有一种办法可以破解循环应用呢?别着急,我们先来看另一个问题,上面的三种方法都存在的一个问题就是引用丢失,这在某些情况下也许是不能接受的

举个例子,假如一个对象a,a下面的两个键值都引用同一个对象b,经过深拷贝后,a的两个键值会丢失引用关系,从而变成两个不同的对象,o(╯□╰)o

var b = 1;
var a = {a1: b, a2: b};

a.a1 === a.a2 // true

var c = clone(a);
c.a1 === c.a2 // false

如果我们发现个新对象就把这个对象和他的拷贝存下来,每次拷贝对象前,都先看一下这个对象是不是已经拷贝过了,如果拷贝过了,就不需要拷贝了,直接用原来的,这样我们就能够保留引用关系了,✧(≖ ◡ ≖✿)嘿嘿

但是代码怎么写呢,o(╯□╰)o,别急往下看,其实和循环的代码大体一样,不一样的地方我用// ==========标注出来了

引入一个数组uniqueList用来存储已经拷贝的数组,每次循环遍历时,先判断对象是否在uniqueList中了,如果在的话就不执行拷贝逻辑了

find是抽象的一个函数,其实就是遍历uniqueList

// 保持引用关系
function cloneForce(x) {
    // =============
    const uniqueList = []; // 用来去重
    // =============

    let root = {};

    // 循环数组
    const loopList = [
        {
            parent: root,
            key: undefined,
            data: x,
        }
    ];

    while(loopList.length) {
        // 深度优先
        const node = loopList.pop();
        const parent = node.parent;
        const key = node.key;
        const data = node.data;

        // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
        let res = parent;
        if (typeof key !== 'undefined') {
            res = parent[key] = {};
        }
        
        // =============
        // 数据已经存在
        let uniqueData = find(uniqueList, data);
        if (uniqueData) {
            parent[key] = uniqueData.target;
            break; // 中断本次循环
        }

        // 数据不存在
        // 保存源数据,在拷贝数据中对应的引用
        uniqueList.push({
            source: data,
            target: res,
        });
        // =============
    
        for(let k in data) {
            if (data.hasOwnProperty(k)) {
                if (typeof data[k] === 'object') {
                    // 下一次循环
                    loopList.push({
                        parent: res,
                        key: k,
                        data: data[k],
                    });
                } else {
                    res[k] = data[k];
                }
            }
        }
    }

    return root;
}

function find(arr, item) {
    for(let i = 0; i < arr.length; i++) {
        if (arr[i].source === item) {
            return arr[i];
        }
    }

    return null;
}

下面来验证一下效果,amazing

var b = 1;
var a = {a1: b, a2: b};

a.a1 === a.a2 // true

var c = cloneForce(a);
c.a1 === c.a2 // true

接下来再说一下如何破解循环引用,等一下,上面的代码好像可以破解循环引用啊,赶紧验证一下

惊不惊喜,(*^__^*) 嘻嘻……

var a = {};
a.a = a;

cloneForce(a)

看起来完美的cloneForce是不是就没问题呢?cloneForce有两个问题

第一个问题,所谓成也萧何,败也萧何,如果保持引用不是你想要的,那就不能用cloneForce了;

第二个问题,cloneForce在对象数量很多时会出现很大的问题,如果数据量很大不适合使用cloneForce

性能对比

上边的内容还是有点难度,下面我们来点更有难度的,对比一下不同方法的性能

我们先来做实验,看数据,影响性能的原因有两个,一个是深度,一个是每层的广度,我们采用固定一个变量,只让一个变量变化的方式来测试性能

测试的方法是在指定的时间内,深拷贝执行的次数,次数越多,证明性能越好

下面的runTime是测试代码的核心片段,下面的例子中,我们可以测试在2秒内运行clone(createData(500, 1)的次数

function runTime(fn, time) {
    var stime = Date.now();
    var count = 0;
    while(Date.now() - stime < time) {
        fn();
        count++;
    }

    return count;
}

runTime(function () { clone(createData(500, 1)) }, 2000);

下面来做第一个测试,将广度固定在100,深度由小到大变化,记录1秒内执行的次数

深度clonecloneJSONcloneLoopcloneForce
500351212338372
1000174104175143
15001166711282
200092508869

将上面的数据做成表格可以发现,一些规律

  • 随着深度变小,相互之间的差异在变小
  • clone和cloneLoop的差别并不大
  • cloneLoop > cloneForce > cloneJSON

我们先来分析下各个方法的时间复杂度问题,各个方法要做的相同事情,这里就不计算,比如循环对象,判断是否为对象

  • clone时间 = 创建递归函数 + 每个对象处理时间
  • cloneJSON时间 = 循环检测 + 每个对象处理时间 * 2 (递归转字符串 + 递归解析)
  • cloneLoop时间 = 每个对象处理时间
  • cloneForce时间 = 判断对象是否缓存中 + 每个对象处理时间

cloneJSON的速度只有clone的50%,很容易理解,因为其会多进行一次递归时间

cloneForce由于要判断对象是否在缓存中,而导致速度变慢,我们来计算下判断逻辑的时间复杂度,假设对象的个数是n,则其时间复杂度为O(n2),对象的个数越多,cloneForce的速度会越慢

1 + 2 + 3 ... + n = n^2/2 - 1

关于clone和cloneLoop这里有一点问题,看起来实验结果和推理结果不一致,其中必有蹊跷

接下来做第二个测试,将深度固定在10000,广度固定为0,记录2秒内执行的次数

宽度clonecloneJSONcloneLoopcloneForce
013400327214292989

排除宽度的干扰,来看看深度对各个方法的影响

  • 随着对象的增多,cloneForce的性能低下凸显
  • cloneJSON的性能也大打折扣,这是因为循环检测占用了很多时间
  • cloneLoop的性能高于clone,可以看出递归新建函数的时间和循环对象比起来可以忽略不计

下面我们来测试一下cloneForce的性能极限,这次我们测试运行指定次数需要的时间

var data1 = createData(2000, 0);
var data2 = createData(4000, 0);
var data3 = createData(6000, 0);
var data4 = createData(8000, 0);
var data5 = createData(10000, 0);

cloneForce(data1)
cloneForce(data2)
cloneForce(data3)
cloneForce(data4)
cloneForce(data5)

通过测试发现,其时间成指数级增长,当对象个数大于万级别,就会有300ms以上的延迟

总结

尺有所短寸有所长,无关乎好坏优劣,其实每种方法都有自己的优缺点,和适用场景,人尽其才,物尽其用,方是真理

下面对各种方法进行对比,希望给大家提供一些帮助

clonecloneJSONcloneLoopcloneForce
难度☆☆☆☆☆☆☆☆☆
兼容性ie6ie8ie6ie6
循环引用一层不支持一层支持
栈溢出不会不会
保持引用
适合场景一般数据拷贝一般数据拷贝层级很多保持引用关系

本文的灵感都来自于@jsmini/clone,如果大家想使用文中的4种深拷贝方式,可以直接使用@jsmini/clone这个库

// npm install --save @jsmini/clone
import { clone, cloneJSON, cloneLoop, cloneForce } from '@jsmini/clone';

本文为了简单和易读,示例代码中忽略了一些边界情况,如果想学习生产中的代码,请阅读@jsmini/clone的源码

@jsmini/clone孵化于jsmini,jsmini致力于为大家提供一组小而美,无依赖的高质量库

jsmini的诞生离不开jslib-base,感谢jslib-base为jsmini提供了底层技术

感谢你阅读了本文,相信现在你能够驾驭任何深拷贝的问题了,如果有什么疑问,欢迎和我讨论

最后推荐下我的新书《React状态管理与同构实战》,深入解读前沿同构技术,感谢大家支持

京东:https://item.jd.com/12403508.html

当当:http://product.dangdang.com/25308679.html

最后最后招聘前端,后端,客户端啦!地点:北京+上海+成都,感兴趣的同学,可以把简历发到我的邮箱: yanhaijing@yeah.net

原文网址:http://yanhaijing.com/javascr...

查看原文

木纸鸢 收藏了文章 · 2018-06-25

Flow - JS静态类型检查工具

Flow工具用法简图

本章的目标是提供一些Flow工具的介绍与使用建议。Flow本质上也只是个检查工具,它并不会自动修正代码中的错误,也不会强制说你没按照它的警告消息修正,就不会让你运行程序。当然,并没有要求什么时候一定要用这类的工具,只是这种作法可以让你的代码更具强健性与提高阅读性,也可以直接避去很多不必要的数据类型使用上的问题,这种开发方式目前在许多框架与函数库项目,或是以JavaScript应用为主的开发团队中都已经都是必用工具。

注: 本文内容大部份参考自Flow官网,是之前我个人博客文章 - "Flow静态数据类型的检查工具,10分钟快捷入门"的增修版本。

注: 本文内容字数过万,去除代码也有数千字,笔误在所难免,有错再回馈留言吧。

注意

"奇异博士"说过「使用警语应该要加注在书的最前面」。所以我把注意项目先加在这里。

  • 由于Flow还是个年轻的项目,问题仍然很多,功能也没你想像中完整,用起来有时候会卡顿是正常的,效能仍须改善。以后用户愈来愈多就会愈作愈好。

  • Windows平台的支持也是几个月前(2016.8)时的事,Flow只支持64位元的作业系统,32位元就不能用了。

  • 如果你是要学或用React或Vue.js等等,Flow是必学的。不管你要用不用,库源码里面都用了。

Flow介绍

Flow是个JavaScript的静态类型检查工具,由Facebook出品的开源码项目,问世只有一年多,是个相当年轻的项目。简单来说,它是对比TypeScript语言的解决方式。

会有这类解决方案,起因是JavaScript是一种弱(动态)数据类型的语言,弱(动态)数据类型代表在代码中,变量或常量会自动依照赋值变更数据类型,而且类型种类也很少,这是直译式脚本语言的常见特性,但有可能是优点也是很大的缺点。优点是容易学习与使用,缺点是像开发者经常会因为赋值或传值的类型错误,造成不如预期的结果。有些时候在使用框架或函数库时,如果没有仔细看文件,亦或是文件写得不清不楚,也容易造成误用的情况。

这个缺点在应用规模化时,会显得更加严重。我们在开发团队的协同时,一般都是用详尽的文字说明,来降低这个问题的发生,但JS语言本身无法有效阻止这些问题。而且说明文件也需要花时间额外编写,其他的开发者阅读也需要花时间。在现今预先编译器流行的年代,像TypeScript这样的强(静态)类的JavaScript超集语言就开始流行,用严格的角度,以JavaScript语言为基底,来重新打造另一套具有强(静态)类型特性的语言,就如同Java或C#这些语言一样,这也是为什么TypeScript称自己是企业级的开发JavaScript解决方案。

注: 强(静态)类型语言,意思是可以让变量或常量在声明(定义)时,就限制好只能使用哪种类型,之后在使用时如果发生类型不相符时,就会发出错误警告而不能编译。但不只这些,语言本身也会拓展了更多的类型与语法。

TypeScript自然有它的市场,但它有一些明显的问题,首先是JavaScript开发者需要再进一步学习,内容不少,也有一定陡峭的学习曲线,不过这还算小事情。重大的事情是需要把已经在使用的应用代码,都要整个改用TypeScript代码语法,才能发挥完整的功用。这对很多已经有内部代码库的大型应用开发团队而言,将会是个重大的决定,因为如果不往全面重构的路走,将无法发挥强(静态)类型语言的最大效用。

所以许多现行的开源码函数库或框架,并不会直接使用TypeScript作为代码的语言,另一方面当然因为是TypeScript并非普及到一定程度的语言,社群上有热爱的粉丝也有不是那么支持的反对者。当然,TypeScript也有它的优势,自从TypeScript提出了DefinitelyTyped的解决方式之后,让现有的函数库能额外再定义出里面使用的类型,这也是另一个可以与现有框架与库相整合的方案,这让许多函数库与框架都提交定义档案,提供了另一种选择。另一个优势是,TypeScript也是个活跃的开源码项目,发展到现在也有一段时间,算是逐渐成熟的项目。它的背后有微软公司的支持,在最近发布的知名的、全新打造过的Angular2框架中(由Google主导),也采用了TypeScript作为基础的开发语言。

现在,Flow提供了另一个新的选项,它是一种强(静态)类型的辅助检查工具。Flow的功能是让现有的JavaScript语法可以事先作类型的声明(定义),在开发过程中进行自动检查,当然在最后编译时,一样可以用babel工具来移除这些标记。

相较于TypeScript是另外重新制定一套语言,最后再经过编译为JavaScript代码来运行。Flow走的则是非强制与非侵入性的路线。Flow的优点是易学易用,它的学习曲线没有TypeScript来得高,虽然内容也很多,但大概一天之内学个大概,就可以渐进式地开始使用。而且因为Flow从头到尾只是个检查工具,并不是新的程序语言或超集语言,所以它可以与各种现有的JavaScript代码兼容,如果你哪天不想用了,就去除掉标记就是回到原来的代码,没什么负担。当然,Flow的功用可能无法像TypeScript这么全面性,也不可能改变要作某些事情的语法结构。

总结来说,这两种方式的目的是有些相似的,各自有优点也有不足之处,青菜萝卜各有所爱,要选择哪一种方式就看你的选择。

从一个小例子演示

这种类型不符的情况在代码中非常容易发生,例如以下的例子:

function foo(x) {
  return x + 10
}

foo('Hello!')

x这个传参,我们在函数声明时希望它是个数字类型,但最后使用调用函数时则用了字符串类型。最后的结果会是什么吗? "Hello!10",这是因为加号(+)在JavaScript语言中,除了作为数字的加运算外,也可以当作字符串的连接运算。想当然这并不是我们想要的结果。

聪明如你应该会想要用类型来当传参的识别名,容易一眼看出传参要的是什么类型,像下面这样:

function foo(number) {
  return number + 10
}

但如果在复合类型的情况,例如这个传参的类型可以是数字类型也可以是布尔类型,你又要如何写得清楚?更不用说如果是个复杂的对象类型时,结构又该如何先确定好?另外还有函数的返回类型又该如何来写?

利用Flow类型的定义方式,来解决这个小案例的问题,可以改写为像下面的代码:

// @flow

function foo(x: number): number {
  return x + 10
}

foo('hi')

你有看到在函数的传参,以及函数的圆括号(())后面的两个地方,加了: number标记,这代表这个传参会限定为数字类型,而返回值也只允许是数字类型。

当使用非数字类型的值作为传入值时,就会出现由Flow工具发出的警告消息,像下面这样:

message: '[flow] string (This type is incompatible with number See also: function call)'

这消息是说,你这函数的传参是string(字符串)类型,与你声明的number(数字)不相符合。

如果是要允许多种类型也是很容易可以加标记的,假使这个函数可以使用布尔与数字类型,但返回可以是数字或字符串,就像下面这样修改过:

// @flow

function foo(x: number | boolean): number | string {
  if (typeof x === 'number') {
    return x + 10
  }
  return 'x is boolean'
}

foo(1)
foo(true)
foo(null)  // 这一行有类型错误消息

由上面这个小例子你可以想见,如果在多人协同开发某个有规模的JavaScript应用时,这种类型的输出输入问题就会很常遇见。如果利用Flow工具的检查,可以避免掉许多不必要的类型问题。

真实案例

可能你会认为Flow工具只能运用在小型代码中,但实际上Facebook会创造出Flow工具,有很大的原因是为了React与React Native。

举一个我最近正在研究的的函数库代码中NavigationExperimental(这网址位置有可能会变,因为是直接连到源码里),这里面就预先声明了所有的对象结构,像下面这样的代码:

export type NavigationGestureDirection = 'horizontal' | 'vertical';

export type NavigationRoute = {
  key: string,
  title?: string
};

export type NavigationState = {
  index: number,
  routes: Array<NavigationRoute>,
};

// ...

Flow具备有像TypeScript语言中,预先定义对象类型的作用。上面代码的都是这个组件中预先定义的类型,这些类型可以再套用到不同的代码文档之中。

export type NavigationGestureDirection = 'horizontal' | 'vertical';

上面这行类似于列举(enum)的类型,意思是说要不就是'horizontal'(水平的),要不然就'vertical'(垂直的),就这两种字符串值可使用。

export type NavigationRoute = {
  key: string,
  title?: string
};

这行里面用了一个问号(?)定义在title属性的后面,这代表这属性是可选的(Optional),不过你可能会有点搞混,因为问号(?)可以放在两个位置,见下面的例子:

export type Test = {
  titleOne?: string,
  titleTwo: ?string
}

titleOne代表的是属性为可自定义的(可有可无),但一定是字符串类型。titleTwo代表的是类型可自定义,也就是值的部份除了定义的类型,也可以是null或undefined,不过这属性是需要的,而且你一定要给它一个值。好的,这有些太细部了,如果有用到再查手册文档就可以。

export type NavigationState = {
  index: number,
  routes: Array<NavigationRoute>,
};

上面的代码可以看到,只要是声明过的类型(type),同样可以拿来拿在其他类型中套用,像这里的Array<NavigationRoute>,就是使用了上面已声明的NavigationRoute类型。它是一个数组,里面放的成员是NavigationRoute类型,是个对象的结构。

刚已经有说过Flow工具有很大的原因是为了React与React Native所设计,因为Flow本身就内建对PropTypes的检查功能,也可以正确检查JSX语法,在这篇官方文档中有说明,而这在之后介绍React的文档的例子中就可以看到。

安装与使用

Flow目前可以支持macOS、Linux(64位元)、Windows(64位元),你可以从以下的四种安装方式选择其中一种:

  • 直接从Flow的发布页面下载可运行档案,加到计算机中的PATH(路径),让flow指令可以在命令列窗口访问即可。

  • 透过npm安装即可,可以安装在全局(global)或是各别项目中。下面为安装在项目中的指令:

npm install --save-dev flow-bin
  • macOS中可以使用homebrew安装:

brew update
brew install flow
  • 透过OCaml OPAM套装管理程序打包与安装,请见Flow的Github页面

Flow简单使用三步骤

第1步: 初始化项目

在你的项目根目录的用命令列工具输入下面的指令,这将会创建一个.flowconfig文档,如果这文档已经存在就不需要再进行初始化,这个设置档一样是可以加入自定义的设置值,请参考Advanced Configuration这里的说明,目前有很多项目里面都已经内附这个设置档,例如一些React的项目:

flow init

第2步: 在代码文档中加入要作类型检查的注释

一般都在代码档案的最上面一行加入,没加Flow工具是不会进行检查的,有两种格式都可以:

// @flow

/* @flow */

第3步: 进行检查

目前支持Flow工具插件的代码编辑工具很多,常见的Atom, Visual Studio Code(VSC), Sublime与WebStorm都有,当有安装搭配代码编辑工具的插件时,编辑工具会辅助显示检查的讯息。不过有时候会有点卡顿的要等一下,因为检查速度还不是那么快。

或是直接用下面的命令列指令来进行检查:

flow check

在Visual Studio Code中因为它内建TypeScript与JavaScript的检查功能,如果要使用Flow工具来作类型检查,需要在用户设置中,加上下面这行设置值以免冲突:

"javascript.validate.enable": false

转换(编译)有Flow标记的代码

注: 有些脚手架就已经装好与设置好这个babel拓展插件,你不用再多安装了。

在开发的最后阶段要将原本有使用Flow标记,或是有类型注释的代码,进行清除或转换。转换的工作要使用babel编译器,这也是目前较推荐的方式。

使用babel编译器如果以命令列工具为主,可以使用下面的指令来安装在全局中:

npm install -g babel-cli

再来加装额外移除Flow标记的npm套件babel-plugin-transform-flow-strip-types在你的项目中:

npm install --save-dev babel-plugin-transform-flow-strip-types

然后创建一个.babelrc设置档案,档案内容如下:

{
  "plugins": [
    "transform-flow-strip-types"
  ]
}

完成设置后,之后babel在编译时就会一并转换Flow标记。

下面的指令则是直接把src目录的档案编译到dist目录中:

babel src -d dist

当然,babel的使用方式不是只有上面说的这种命令列指令,你可以视项目的使用情况来进行设置。

Flow支持的数据类型

Flow用起来是的确是简单,但里面的内容很多,主要原因是是要看实际不同的使用情况作搭配。JavaScript里面的原始数据类型都有支持,而在函数、对象与一些新的ES6中的类,在搭配使用时就会比较复杂,详细的情况就请到官网文档中观看,以下只能提供一些简单的介绍说明。

原始数据类型

Flow支持原始数据类型,如下面的列表:

  • boolean

  • number

  • string

  • null

  • void

其中的void类型,它就是JS中的undefined类型。

这里可能要注意的是,在JS中undefinednull的值会相等但类型不同,意思是作值相等比较时,像(undefined == null)时会为true,有时候在一些运行期间的检查时,可能会用值相等比较而不是严格的相等比较,来检查这两个类型的值。

所有的类型都可以使用垂直线符号(|)作为联合使用(也就是 OR 的意思),例如string | number指的是两种类型其中一种都可使用,这是一种联合的类型,称为"联合(Union)类型"。

最特别的是可选的(Optional)类型的设计,可选类型代表这个变量或常量的值有可能不存在,也就是允许它除了是某个类型的值外,也可以是nullundefined值。要使用可选类型,就是在类型名称定义前加上问号(?),例如?string这样,下面是一个简单的例子:

let bar: ?string = null

字面文字(literal)类型

字面文字类型指的是以真实值作为数据类型,可用的值有三种,即数字、字符串或布尔值。字面文字类型搭配联合的类型可以作为列举(enums)来使用,例如以下的一个扑克牌的类型例子:

type Suit =
  | "Diamonds"
  | "Clubs"
  | "Hearts"
  | "Spades";

type Rank =
  | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
  | "Jack"
  | "Queen"
  | "King"
  | "Ace";

type Card = {
  suit: Suit,
  rank: Rank,
}

注: type是Flow中定义类型别名(Type Alias)的关键字,是一种预先声明的类型,这些声明的标记一样只会在开发阶段中使用,最后编译去除。

类型别名

类型别名(Type Alias)提供了可以预先定义与集中代码中所需要的类型,一个简单的例子如下:

type T = Array<string>
var x: T = []
x["Hi"] = 2 //有Flow警告

类型别名(Type Alias)也可以用于复杂的应用情况,详见Flow官网提供的Type Aliases内容。

任何的数据类型

在某一些情况可能不需要定义的太过于严格,或是还在开发中正在调试时,有一种作为渐进的改善代码的类型。

Flow提供了两种特殊的类型可以作为松散的数据类型定义:

  • any: 相当于不检查。既是所有类型的超集(supertype),也是所有类型的子集(subtype)

  • mixed: 类似于any是所有类型的超集(supertype),但不同于any的是,它不是所有类型的子集(subtype)

mixed是一个特别的类型,中文是混合的意思,mixed算是any的"啰嗦"进化类型。mixed用在函数的输入(传参)与输出(返回)时,会有不一样的状态,例如以下的例子会出现警告:

function foo(x: mixed): string {
  return x + '10'
}

foo('Hello!')
foo(1)

会出现警告消息如下:

[flow] mixed (Cannot be added to string)

这原因是虽然输入时可以用mixed,但Flow会认为函数中x的值不见得可以与string类型作相加,所以会请求你要在函数中的代码,要加入检查对传入类型在运行期间的类型检查代码,例如像下面修改过才能过关:

function foo(x: mixed): string {
  if (typeof x === 'number' || typeof x === 'string') {
    return x + '10'
  }
  throw new Error('Invalid x type')
}

foo('Hello!')
foo(1)

mixed虽然"啰嗦",但它是用来渐进替换any使用的,有时候往往开发者健忘或偷懒没作传入值在运行期间的类型检查,结果后面要花更多的时间才能找出错误点,这个类型的设计大概是为了提早预防这样的情况。

注: 从上面的例子可以看到Flow除了对类型会作检查外,它也会请求对某些类型需要有动态的检查。在官方的文件可以参考Dynamic Type Tests这个章节。

复合式的数据类型

数组(Array)

数组类型使用的是Array<T>,例如Array<number>,会限定数组中的值只能使用数字的数据类型。当然你也可以加入埀直线(|)来定义允许多种类型,例如Array<number|string>

对象(Object)

对象类型会比较麻烦,主要原因是在JavaScript中所有的数据类型大概都可以算是对象,就算是基础数据类型也有对应的包装对象,再加上有个异常的null类型的typeof返回值也是对象。

对象类型在Flow中的使用,基本上要分作两大部份来说明。

第一种是单指Object这个类型,Flow会判断所有的基础数据类不是属于这个类型的,以下的例子全部都会有警告:

// 以下都有Flow警告

(0: Object);
("": Object);
(true: Object);
(null: Object);
(undefined: Object);

其他的复合式数据类型,除了数组之外,都会认为是对象类型。如下面的例子:

({foo: "foo"}: Object);
(function() {}: Object);
(class {}: Object);
([]: Object); // Flow不认为数组是属于对象

注意: 上面有两个特例,typeof nulltypeof []都是返回'object'。也就是说在JS的标准定义中,null数组用typeof检测都会返回对象类型。所以,Flow工具的检查会与JS预设并不相同,这一点要注意。

注: typeof在Flow中有一些另外的用途,详见Typeof的说明。

第二种方式是要定义出完整的对象的字面文字结构,像{ x1: T1; x2: T2; x3: T3;}的语法,用这个结构来检查,以下为例子:

let object: {foo: string, bar: number} = {foo: "foo", bar: 0};

object.foo = 111; //Flow警告
object.bar = '111'; //Flow警告

函数(Function)

上面已经有看到,函数也属于对象(Object)类型,当然也有自己的Function类型,函数的类型也可以从两大部份来看。

第一是单指Function这个类型,可以用来定义变量或常量的类型。如下面的代码例子:

var anyFunction: Function = () => {};

第二指的是函数中的用法,上面已经有看到函数的输出(返回值)与输入(传参)的用法例子。例如以下的例子:

function foo(x: number): number {
  return x + 10;
}

因为函数有很多种不同的使用情况,实际上可能会复杂很多,Flow工具可以支持目前最新的arrow functions、async functions与generator functions,详见官方的这篇Functions的说明。

类(Class)

类是ES6(ES2015)中新式的特性,类目前仍然只是原型的语法糖,类本身也属于一种对象(Object)类型。类的使用情况也可能会复杂,尤其是涉及多型与实例的情况,详见Flow网站提供的Classes内容。

Flow的现在与未来的发展

Flow在最近的博客中说明引入了flow-typed的函数库定义档("libdefs"),在这个Github存储库中将统一存放所有来自社群提供的函数库定义档案。这是一种可以让现有的函数库与框架,预先写出里面使用的类型定义。让项目里面有使用Flow工具与这些函数库,就可以直接使用这些定义档,以此结合现有的函数库与框架来使用。这个作法是参考TypeScript的DefinitelyTyped方式。因为这还是很新的消息(2016.10),目前加入的函数库还没有太多,不过React周边的一些函数库或组件都已经开始加入,其他常用的像underscore、backbone或lodash也已经有人在提交或维护。

Flow另一个发展会是在开发工具的自动完成功能的改进,因为如果已经能在撰写代码时,就知道变量或常量的类型(静态类型),那么在自动完成功能中就可以更准确地给出可用的属性或方法。这一个功能在Facebook自家的Nuclide开发工具的Flow说明页中就有看到。Nuclide是基于Atom开发工具之上的工具,计算机硬件如果不够力是跑不动的,而且它稳定性与运行速度都还需要再努力。这大概是未来可见到的一些新趋向。

结论

本文简单的说明了Flow工具的功能介绍,以及其中的一些简要的内容等等。相信看过后你已经对这个Flow工具有一些认识,以我个人学过TypeScript的经验,相较于TypeScript的学习曲线,Flow大概是等于不用学。Flow虽然是一个很新的工具,但相当的有用,建议每个JavaScript开发者都可以试试,一开始不用学太多,大概这篇文档看完就可以开始用了。复杂的地方就再查找官方的文件即可。

对于每个正在使用JS开发稍具规模化的应用,或是开发开源码的函数库或框架的团队来说,让JS具有静态类型特性,是一个很重要而且必要的决定。以我的观察,在网络上一直有很多的超集语言(例如TypeScript)的爱好者,会提出要全面改用TypeScript(或其他超集语言)的声音,例如Vue.js在很早之前就有讨论是不是要全面采用TypeScript的声音。后来Vue.js只有提交TypeScript的DefinitelyTyped文档,但在2.0中则采行了Flow工具。在这篇Vue作者于知乎上发表的: Vue 2.0 为什么选用 Flow 进行静态代码检查而不是直接使用 TypeScript?的内容中,你可以看到为何选择Flow的理由,这可能也是整个开发团队所认同的最后结果。作者回答的文中可以总结下面这句话:

全部换 TS(TypeScript) 成本过高,短期内并不现实。 相比之下 Flow 对于已有的 ES2015 代码的迁入/迁出成本都非常低 … 万一哪天不想用 Flow 了,转一下,就得到符合规范的 ES。

总之,Flow提供了另一个选择,要用什么工具就看聪明的你如何选择了。

查看原文

木纸鸢 关注了标签 · 2018-04-08

关注 0

木纸鸢 关注了标签 · 2018-04-08

golang

Go语言是谷歌2009发布的第二款开源编程语言。Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。
Go语言是谷歌推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。谷歌首席软件工程师罗布派克(Rob Pike)说:我们之所以开发Go,是因为过去10多年间软件开发的难度令人沮丧。Go是谷歌2009发布的第二款编程语言。

七牛云存储CEO许式伟出版《Go语言编程
go语言翻译项目 http://code.google.com/p/gola...
《go编程导读》 http://code.google.com/p/ac-m...
golang的官方文档 http://golang.org/doc/docs.html
golang windows上安装 http://code.google.com/p/gomi...

关注 26113

木纸鸢 提出了问题 · 2018-03-24

解决go发http请求 除了net/http 还有其他好用的包么

go发http请求调其它服务,新手求教

关注 3 回答 2

木纸鸢 回答了问题 · 2018-02-11

使用手淘flexible的rem方案适配移动端h5后,怎么处理富文本编辑器的适配?

h5展示页引入编辑后富文本所需的css class样式, 编辑后的富文本html标签里有class不用style,

关注 4 回答 4

木纸鸢 回答了问题 · 2018-02-11

解决v-for动态生成router-link的 :to传参怎么写

关注 2 回答 1

木纸鸢 关注了用户 · 2018-02-04

咚子 @zi_597d64ce14187

一个前端

关注 12542

木纸鸢 关注了用户 · 2018-02-04

文强 @wenqiang_59c918cbdc608

关注 210

认证与成就

  • 获得 9 次点赞
  • 获得 8 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 8 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-11-15
个人主页被 267 人浏览