Leon82 发布于 07月09, 2019

WSL:在Windows下优雅地玩Linux

引子

一直以来,Windows的命令行的体验都不是特别的友好。由于Windows以图形界面交互为主,同时微软在一段时间内对命令行程序发展并不积极,以及Windows系统底层与*nix系列的不一致。造成Windows下的命令行开发与图形开发体验相去甚远:一方面工具链的不完整,一方面终端字体不甚美观,甚至默认的终端色域相对于Mac都是精简的。

阅读全文 »

Leon82 发布于 05月22, 2019

谈谈StorageEvent

纷纷红紫已成尘,布谷声中夏令新。夹路桑麻行不尽,始知身是太平人。 ——宋.陆游 《初夏绝句》

我们在开发多Tab应用时候,常常会遇到多个Tab状态同步的问题。

想象如下场景:用户主界面,显示用户购物车内待结算的商品总数。此时,用户可能打开多个Tab。当用户添加新商品到购物车的时候,需要更新购物车的数量。

此时,当前页面需要向服务器发起请求,在得到添加成功响应的时候,可以更新用户界面。为了兼顾体验和可靠性,如果确信添加成功概率比较高的时候,也可以先更新界面,当多数返回错误的时候,可以给用户界面做状态回滚。为了下次展示方便,我们还会把这个数据写到LocalStorage里面。用户再次打开时候,可以优先从localStorage中取值。

当前页面解决了,那么如果同时打开多个Tab该如何解决呢?这里使用StorageEvent可能是一种代价较小的解决方案。

StorageEvent是什么呢?

  1. 是一种Event,可以通过标准的Event监听器操作。
  2. 当storage变化时候,事件会被派发到所有同域下的其他页面。
  3. 触发变化的当前页面,没有事件派发。

这里有一个简单的示例可以展示这个API的用法。

const STORAGE_KEY = "cartlist"

const getStorage = () => {
    try {
        let rets = window.localStorage.getItem(STORAGE_KEY)

        if (rets === null) {
            return []
        }
        return JSON.parse(rets)
    }
    catch(e){
        return []
    }
}

const addCart = (value) => {
    let rets = getStorage()

    rets.push(value)

    window.localStorage.setItem(STORAGE_KEY, JSON.stringify(rets))

    return rets    
}

const minusCart = (value) => {
    let rets = getStorage()
    let idx = rets.indexOf(value)

    if (idx !== -1){
        rets.splice(idx, 1)
        window.localStorage.setItem(STORAGE_KEY, JSON.stringify(rets))
    }


    return rets    
}

const render = () => {
    let rets = getStorage()

    if (rets.length){
        $("#num").html(rets.length).show()
    }
    else {
        $("#num").hide()
    }

    $(".list li").each((i,el) => {
        if (rets.includes(i)){
            $(el).find("a:nth-child(1)").css("visibility", "hidden")
            $(el).find("a:nth-child(2)").css("visibility", "visible")

        }
        else {
            $(el).find("a:nth-child(1)").css("visibility", "visible")
            $(el).find("a:nth-child(2)").css("visibility", "hidden")

        }
    })
}

$(".list a").on("click", (e)=> {
    let opIdx = $(e.target).parent().find("a").index(e.target)
    let line = $(e.target).parent().parent()
    let idx = $(".list li").index(line)

    opIdx === 0 ? addCart(idx) : minusCart(idx)

    render()

    return false
})

window.addEventListener('storage', (e) => {
    render()
})

render()

其中,下面这行代码是实现的关键:

window.addEventListener('storage', (e) => {
    render()
})

当我们注释掉这个语句,我们的页面同步就不能运行了。

读者可以打开多个Tab并观察页面的变化https://jsbin.com/radekilosu/1/edit?html,css,js,output。

实际上,这个事件e上还带有很多信息,方便编程时,对于事件做精确的控制。

字段 含义
key 发生变化的storageKey
newValue 变换后新值
oldValue 变换前原值
storageArea 相关的变化对象
url 触发变化的URL,如果是frameset内,则是触发帧的URL

上述各值都是只读的。

还有一点没有解决掉,就是触发storage变化的本页面,不能接收这个值,这个一般情况下是没问题。当然,为了一致性,我们可以自行new一个事件,在发生时候主动触发它。

此时我们可以包装一个新的Storage对象:

var Storage = {
    setItem : function(k,v){
      localStorage.setItem(k,v);
    },
    removeItem : function(k){
      localStorage.removeItem(k);
    },
    clear: function (){},
    getItem: function(k){}
}

此时,我们再包装一个函数:

function dispatchMe(key, oldval, newval, url, storage){
    var se = document.createEvent("StorageEvent");
    se.initStorageEvent('storage', false, false, key, oldval, newval, url, storage);
    window.dispatchEvent(se);
}

此时,我们只需要在setItem、removeItem、clear中获取对应的值,并手动调用一下dispatchMe,同时把和localStorage打交道的地方改为调用我们包装的Storage即可。

参考资料

  1. https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event
  2. https://www.cnblogs.com/cczw/p/3196195.html

阅读全文 »

Leon82 发布于 05月21, 2019

从谷歌华为暂停合作说起

引子

2019年5月20日,科技界最火爆的消息莫过于谷歌暂停支持华为部分业务。关于这个事件将产生的影响,大家可以从相关的科技媒体上找到详细的分析。比较一致的看法是:谷歌的这个做法,对于国内的华为用户,影响不大;对于海外的华为用户,尤其是在上游应用生态环境上,会有一定的影响。

这是由于,谷歌暂停合作的服务,都是在用户层面的服务GMS。用户层与安卓底层内核遵从不同的协议,因此不必开源,进而导致替换成本高企。基于现有生态,在海外市场构建应用生态环境难度非常大。

这篇文章将尝试从这则新闻涉及的开源协议来分析一下,这个做法背后相关的逻辑,并籍此,跟各位读者聊一聊主流的开源协议。

阅读全文 »

Leon82 发布于 04月21, 2019

Puppeteer PK 滑动验证码

天街小雨润如酥,草色遥看近却无。最是一年春好处,绝胜烟柳满皇都。 —— 唐.韩愈 《早春呈水部张十八员外二首》

引子

几个月之前,chunpu小编曾经在《震惊! 滑动验证码竟然能这样破解》一文中给大家展示了使用支持WebDriver标准的Firefox破解滑动验证码的示例,脑洞之大,构思之巧,笔法幽默,叹为观止,也极大的燃起了笔者对此的兴趣。

这篇文章就带大家使用目前较为流行的Puppeteer完成这件事情。

阅读全文 »

Leon82 发布于 04月03, 2019

NodeJS和命令行程序

造物无言却有情,每于寒尽觉春生。千红万紫安排著,只待新雷第一声。 —— 清.张维屏 《新雷》

源起

植根于Unix系统环境下的程序,很多都把贯彻Unix系统设计的哲学作为一种追求。Unix系统管道机制的发明者Douglas McIlroy把Unix哲学总结为三点:

  1. 专注做一件事,并做到极致。
  2. 程序协同工作。
  3. 面向通用接口,如文本数据流。

随着Unix/Linux系统在服务器上影响力越发强大,以及各种跨平台解决方案的发展,这种哲学也被带到了各种平台上。若干年前,笔者第一次接触NodeJS和其包管理解决方案NPM时候,就感觉到其官方倡导的风格,和Unix系统哲学非常契合。近年来,随着NodeJS在服务端以及前端构建领域上的不断开拓,NodeJS的这种思想也正快速的渗透到这些领域。

其实,NodeJS的本身,也是开发命令行程序的一个重要利器。本文就将介绍几个常用的NodeJS相关命令行程序,之后介绍几个开发命令行中常用的组件,最后介绍发布npm包以及带scope的包的发布方法。

阅读全文 »

Leon82 发布于 03月12, 2019

NodeJS与模块系统

孤山寺北贾亭西,水面初平云脚低。几处早莺争暖树,谁家新燕啄春泥。乱花渐欲迷人眼,浅草才能没马蹄。最爱湖东行不足,绿杨阴里白沙堤。 ———— 唐.白居易《钱塘湖春行》

自从 Node8.5 以后, Node 开始支持引入 ES 模块。在新开的项目中,笔者尝试使用了这种方式。由于目前 NodeJS 对于 ES 模块尚属试验性支持,因此需要在启动时候加入参数:--experimental-modules,完整的命令如:node --experimental-modules index.mjs。这样程序就愉快地运行起来了。不过当笔者尝试加入一些新的依赖之后问题出现了:

阅读全文 »

Leon82 发布于 01月18, 2019

GraphQL初探(一)

白玉堂前一树梅,为谁零落为谁开。唯有春风最相惜,一年一度一归来。 宋.王安石《梅花》

引子

仿佛一瞬间,2019 年的一月份就快过去了,然而迎接元旦的时刻似乎还历历在目。时间总是这么安静的流走,以它自己的节奏,不疾不徐。猛的发现,21 世纪也过去了将近 1/5 的时间了。

每一年,都会诞生各种各样的技术。有的经过实践的检验和逐步的完善,慢慢成为业界的标准或是最佳实践;有的则慢慢淡化直至完成使命,或者演化为其他技术。

在技术发展的上升阶段,总会有一些热点的技术,在一段时间被反复提起。它们诞生的初衷往往是一个很具象的痛点,然后在解决、完善之后,外延不断扩大,并最终成长为一个成熟的技术或者技术栈。

今天要说的 GraphQL 可能就是这样的技术。

为什么会有 GraphQL

随着前后端分离的开发模式不断的深入人心,越来越多的项目采用这种方式开发和部署。前端程序或是 App,多采用数据驱动的方式完成。数据可以采用数据接口的方式从服务端请求获得。主流的方式一般采用 REST API。

在 REST 风格的 API 中,接口数据被认为是一种“资源”,服务端提供 RESTful API,客户端通过 GET/POST/DELETE/PUT 等动作,通过 URI 对“资源”进行操作。

比如:GET /books/1 即是通过 GET 请求,访问 URI /books/1,也即服务端提供的/books/:id的 RESTful API,“获取”资源,得到如下返回:

{
    "bookId": 1,
    "title": "Black Hole Blues",
    "author": {
        "firstName": "Janna",
        "lastName": "Levin"
    },
    "page": 260,
    "press": "..."
}

这样的情景适应于现在的主流开发场景。不过也由一些痛点,下面列出几种:

  1. 后一个请求依赖前一个请求。在依赖比较多情况下,一般会造成多次请求。对于每次请求也都需要做失败情况的处理。

举个例子,我们需要查询某本书作者的详细信息。

现有接口:GET /books/:bookId返回形如下列格式:

{
    "bookId": 1,
    "title": "Black Hole Blues",
    "authors": [
        "authorId": 100
    ],
    "page": 260,
    "press": "..."
}

另有一个接口,可以获得作者信息:GET /authors/:userID返回形如下列格式:

{
    "userId": 100,
    "name":{
        "firstName": "Janna",
        "lastName": "Levin"
    },
    "age": ***,
    "gender": "***",
    "photos":[2, 3, 5, 7, 11, 13, 17, 19, 23]

如果只有这两个个最简单的接口,咱们的需求还是可以完成的:首先,先通过GET /books/1取得作者 ID,然后再通过GET /authors/100就取到了作者的信息。

这里,会有两次相继的请求。后面的请求严格依赖前面的请求。这样可能会造成以下几个问题:

(1)在业务上,对于多个作者的情况,可能需要请求的次数更多(可以提供多 ID 查询接口变相解决)

(2)请求时间长。即便启用了Keep-Alive也仅仅减少了重复建立链接的时间。

(3)如果第一次请求失败,需要对错误进行处理或重试。一方面会增大复杂度,另一方面极端的网络条件下,频繁的重试,对于服务端的压力是几何级的增长。

这样的情况,有的团队可能的做法是另外提供一个面对业务的整合接口,如: GET books/1/author/,可以间接地解决问题。

这样做的问题在于,一个前端界面上的问题,直接透给了服务端。业务的变化,会造成这种接口的爆炸增长抑或成批的废弃,给维护造成负担。

  1. 依然是上面的例子。现在对于多端的开发,需要的数据不同。如:手机上仅需获取作者,而 PC 上需要通过photo字段所示的照片集取得第一张照片显示......。为了达到这样的效果,一般的团队可能会为每种端开发一种特定 API,或者是统一把最冗余的数据接口提供出去。这样显然都不是最理想的。

这些痛点至少代表大家在开发中经常遇到的情况。GraphQL 也许可以成为解决这些问题的一个不错的选择。

什么是 GraphQL

那么,什么是 GraphQL 呢?

GraphQL 是一套关于数据查询和操作的语法标准,它极为适合用于 API 交互。它由 Facebook 在 2012 年开发并在内部使用。2015 年对外发布。2018 年 11 月 7 日,GraphQL 由 Facebook 转交由新成立的 GraphQL 基金会管理。

简单的说来,GraphQL 提供了一种机制,允许客户端定义返回的数据结构。当数据返回时候,精确的根据定义返回所需的数据结构。这种机制避免了大量冗余数据的返回,也使得合并多次请求成为可能。

这里是 GraphQL 官方网站。国内还有一份中文的镜像网站

GraphQL 确切讲是一种语言标准,是与实现语言无关的。实际上,Facebook 和开源届,已经有了针对 GraphQL 的各种语言的实现

感受 GraphQL

为了对 GraphQL 有一个感性的认识,读者可以参考官方示例进行操作,完成一个 GraphQL 查询。

GraphQL 引入了类型系统、参数、查询与变更类型、标量类型、枚举类型、接口、联合类型、输入类型等概念。读者可以在这里学习相关概念的描述。这些概念至关重要。

接下来我们打交道的各种程序,无论是前端还是服务端,始终都回避不了一个名字:Apollo。Apollo 是一个通过社区力量帮助你在应用中使用 GraphQL 的一套工具,由 Meteor Development Group开发。本文选型的前端和服务端都最终依赖 Apollo 这个利器。下图是 Apollo 涵盖的领域:

在 Vue 项目中整合 GraphQL

我们以使用 Vue 的项目为例,看一下在前端如何使用 GraphQL。整合 Vue 与 GraphQL 依赖库:vue-apollo。vue-cli 3.x 已经支持工具化引入这个库。

我们以 vue-cli 3.x 为例。

  1. 建立工程,以graphql-test为例
vue create graphql-test
  1. 添加 apollo 插件
cd graphql-test
vue add apollo

此后将安装vue-cli-plugin-apollo,用于配置请求。

为了简单地展示原理,我们暂时不安装样例代码。

此时,目录结构如下图:

默认地,vue-cli-plugin-apollo已经在入口的main.js为我们引入好了配置文件,并全局地,引入到 vue 组件中:

OK,一起来修改一下HelloWorld.vue,如下:

<template>
    <div>
        <ApolloQuery :query="require('../graphql/hello.gql')">
            <template slot-scope="{ result: { loading, error, data } }">
                <div v-if="data">{{ data.hello }}</div>
                <div class="book">
                    <div class="title">Book</div>
                    <ul v-if="data">
                        <li v-for="item in data.books">
                            <span>title: {{item.title}}</span> <span>author: {{item.author}}</span>
                        </li>
                    </ul>
                </div>
            </template>
        </ApolloQuery>
    </div>
</template>

<style scoped>
    .title {
        font-size: 14px;
        font-weight: bold;
    }

    .book ul {
        margin: 0;
        padding: 0;
        margin-left: 50%;
        transform: translateX(-50%);
    }

    .book li {
        margin: 0;
        padding: 0;
        list-style: none;
        text-align: left;
        display: flex;
        align-content: space-between;
    }
</style>

<script>
    export default {
        apollo: {},
    };
</script>

hello.gql 的内容如下:

query Hello {
    hello
    books {
        title
        author
    }
}

此时运行:npm run serve。打开浏览器访问:http://localhost:8080

大家会看到,目前还没有网络返回。为了实现和服务器的交互,我们还需要把服务端建立起来。

在服务器端响应请求

这里需要说明一点,无论是前端还是服务端的响应代码不一定是 Javascript 的。前文提到的各种库列出了主流语言的实现。本文中,以服务端 Javascript 为例来说明。

特别地,如果使用上文提到的 vue-cli 3.0 生成服务端代码也是可以的。这里为了清晰,还是以手动建立为例。

  1. 建立服务端工程,以graphql-server-test为例
vue create graphql-server-test
  1. 安装依赖
cd graphql-server-test
npm init -y
npm install --save apollo-server graphql
  1. 建立响应文件server.js

此时,刷新浏览器,可以看到响应。

优势与劣势分析

对于 GraghQL,优势在于可以有效地降低请求数和请求载荷,提升效率。同时可以灵活地应对各种客户端需求以及多变的业务需求。当然也客观地增加了请求实现的复杂度,同时由于请求对象的个性化,不能充分利用 HTTP 的缓存,也是一个值得重视的问题。

这里Nate Barbettini在 From Iterate Conference 2018 上的演讲以及问答视频,比较客观的比较了 RPC、REST 和 GraghQL 三种方式的优劣,有兴趣的同学可以了解。

Next

本系列下一篇,将以一个完整的前后端示例,带读者亲自体验在实际项目中的 GraphQL。

阅读全文 »

Leon82 发布于 12月31, 2018

形神兼备——谈谈CSS Shapes

东风夜放花千树。更吹落、星如雨。宝马雕车香满路。凤箫声动,玉壶光转,一夜鱼龙舞。

蛾儿雪柳黄金缕。笑语盈盈暗香去。众里寻他千百度。蓦然回首,那人却在,灯火阑珊处。 ——宋.辛弃疾《青玉案.元夕》

引子

马上就要进入 9102 年的今天,仅仅借助于 CSS,Web 开发者就已经可以绘制出各种丰富多彩的图形了,而且方式还是多种多样的。比如下面的鹰嘴形,就可以用多种方式实现。

<span class="icon"></span> <span class="icon icon--blue"></span>
.icon{
    display: inline-block;
    width: 60px;
    height: 100px;
    margin: 100px 30px;

    border-top: 30px solid #000;
    border-top-left-radius: 55px 60px;
}
.icon--blue{
    border-top-left-radius: 60px 70px;
    border-color: blue;
}
.icon{
    display: inline-block;
    width: 60px;
    height: 100px;
    margin: 100px 30px;

    border-bottom: 60px solid transparent;
    border-left: 80px solid #000;
    border-top-left-radius: 100%;

    transform: rotateX(180deg) rotateZ(-90deg);
}
.icon--blue{
    border-left-color: blue;
}

看上去很完美。不过这些我们创建的图形,还不能影响图形内部和周围的内容排布。举个例子来讲,我们可以简单地在页面上创建一个三角形,但是也许无法控制周围的内容围绕这个三角形。

自从 7102 年,iPhoneX 引入“刘海屏”之后,异形屏的全屏适配交互也成为了前端的又一个课题。随之而来的各种适配交互方案,也是层出不穷。下面是一种很讨巧的方案。

这种在排布内容的问题可以使用 CSS 标准的 CSS Shapes 解决。特别地,针对上文提到的“刘海屏”的适配交互方案,张鑫旭老师的这篇文章给出了解决方案的详细解析。

阅读全文 »

Leon82 发布于 12月02, 2018

给你点颜色看看

万事有不平,尔何空自苦;长将一寸身,衔木到终古?我愿平东海,身沉心不改;大海无平期,我心无绝时。呜呼!君不见,西山衔木众鸟多,鹊来燕去自成窠。 —— 清.顾炎武 《精卫》

引子

五颜六色的世界,是大自然对人类的一种馈赠。在计算机领域,显示器是颜色显示的主要介质。笔者接触过的最早的单色显示器到目前的广色域显示器,不得不说是技术的一个又一个飞跃。

在 Web 领域,我们不可避免的要和颜色打交道。其实除了我们最常见的十六进制表示方法,W3C 色彩标准还支持许多不同的颜色表示方法。它们各有所长。本文就带读者了解这些表示方法的原理、标准写法、浏览器支持程度以及相互转换方法。

阅读全文 »

Leon82 发布于 11月01, 2018

从一道面试题谈起

人生无根蒂,飘如陌上尘。分散逐风转,此已非常身。落地为兄弟,何必骨肉亲!得欢当作乐,斗酒聚比邻。盛年不重来,一日难再晨。及时当勉励,岁月不待人。 晋.陶渊明《杂诗》

笔者做面试官时候,除了考察常规的前端技能、框架的特性外,经常会出一些很简单的算法题目。这种经典题目的好处是言简意赅,在很短的时间就可以完成。但却能考察出工程师的基本计算机功力,以及知识储备。特别对于Javascript而言,还可以避免技术栈不同对于工程师能力的误判。

下面就是一道笔者经常考核的题目:

创建一个函数来判断给定的表达式中的大括号是否闭合,返回True/False,对于空字串,返回True:

var expression = "{{}}{}{}"
var expressionFalse = "{}{{}";

function isBalanced(exp) {}

题目本身比较简单。不同的同学拿到题目会有不同的第一反应。

有的同学仅仅查找各种括号的个数,这种是不可以的,这种括号是不匹配的:}{

有很多同学试图使用正则表达式解决问题。其思路是找到所有邻接的匹配的括号对,然后将其替换为空,直到不能替换为止。此时如果最后得到的字符串为空即为匹配,否则不匹配,代码很简洁,如下:

function isBalanced(exp) {
  var reg =  /\{\}/g, len;
  do{
      len  = exp.length;
      exp = exp.replace(reg, "")
  } while (len != exp.length)
  return exp.length === 0
}

好像也是可以的。但是问题点至少有二:

第一、此算法的最坏的时间复杂度是O(n^2)级别的,对于长篇大论是不友好的。

第二、此算法的正则表达式普适性较差,对于表达式含有其他干扰字符时候需要频繁修改正则表达式。当正则表达式过于复杂时候,反过来又会影响到检索效率。

其实,有时候最淳朴的做法,可能会更有效。

大家在数据结构课程中曾经学习过“栈”这种数据结构。“栈”是一种满足后进先出的抽象数据结构。这个结构在这道题目中可以帮助到我们。

思路如下:

  1. 巡检字符串,将括号分类,一类是左括号、一类是右括号。
  2. 左括号看作是入栈信号,右括号是出栈信号。
  3. 当出栈时,如果栈定没有与之匹配的元素,则宣告不匹配。
  4. 当巡检完毕,如果得到空栈,则匹配,否则不匹配。

写成代码大致就是这样,时间复杂度是O(n):

function isBalanced(exp){
    let info = exp.split("")
    let stack = []

    for(let i = 0; i < info.length; ++i){
                let el = info[i]
        if (el == "{"){
            stack.push("{")
        }
        else if (el == "}"){
            if(stack.length === 0){
                return false;
            }
            stack.pop()
        }
    }

    return stack.length === 0
}

在Javascript里,Array可以很方便的模拟栈的行为。读者有兴趣也可以自己实现一个简单的栈,用以丰富自己的代码工具箱。

另外,由于题目中的括号情形比较简单,很多同学用标志位来解决,即当为左括号时候,置标志位,为右括号时候,当标志位存在,取消标志位,否则判定不匹配。最后没有标志位则为匹配。这种解法,对于题目中的状况是可以的。

我们还可以把题目再向前面推进一步,看一看更一般的括号和字符串的情形:

实现函数isBalanced,用true或false表示给定的字符串的括号是否平衡(一一对应)。注意了是要支持三种类型的括号{},[],和()。带有交错括号的字符串应该返回false。

isBalanced('(foo { bar (baz) [boo] })') // true
isBalanced('foo { bar { baz }')         // false
isBalanced('foo { (bar [baz] } )')      // false 

基本思路和上面的解法是类似的。只是这里面需要注意两点:

  1. 过滤掉非括号的干扰字符。
  2. 每一种右括号有一种唯一的左括号与之对应。出现右括号时候,栈顶的左括号必须是和它匹配的。

对于第二种,大家应该很自然的联想到用JSON对象控制,这个是可以的。其实在ES6中,有Map这样的数据结构作为更专业的键值对存储结构可以使用。同时,一些好玩的语法和特性如扩展语法,箭头函数可以让我们把程序写得更加一气呵成。下面是一个可行的代码:

const isBalanced = (str) => {
    const map = new Map([
        ["{", "}"],
        ["[", "]"],
        ["(", ")"]
    ])

    let stack = [];

    for(let i = 0 ; i < str.length; ++i){
        let node = str[i]

        if (map.has(node)){
            stack.push(node)    
        }
        else if ([...map.values()].includes(node)){
            if (stack[stack.length - 1] !== 
                                [...map.entries()].filter(el=>el[1]===node).pop().shift()
                         ){
                return false
            }
            stack.splice(stack.length-1, 1)
        } 
    }    

    return stack.length === 0
}

很喜欢ES6的这些扩充,和这些新的数据结构。使得撰写Javascript有一种特别愉悦的体验。

让我们再次扩充一下这道题目。要求严格限制括号的顺序,即中括号外围只能是大括号,内部只能是小括号。也即:括号只能以大括号、中括号、小括号的顺序只能前面的包含后面的,不能后面的包含前面的,用代码来表示一下:

isStrictBalanced('foo { bar (baz) [boo] }')  // true
isStrictBalanced('(foo { bar (baz) [boo] })')  // false

这样的需求怎么解决呢?

聪明的你可能已经想出了答案,其实就是在入栈时候判断一下当前的优先级就好了。这里可能需要判断一下当前的字符和栈顶元素的关系。这里,我们如果用Array的pop API,则会破坏stack的结构。上例中,我们用length来取得,这里我们用...语法来实现数组的复制,同时在上例基础上,进行一些重构,得到下列代码:

const isStrictBalanced = (str) => {
    const map = new Map([
        ["{", "}"],
        ["[", "]"],
        ["(", ")"]
    ])
    let stack = [], keys = [...map.keys()], values = [...map.values()];
    for(let i = 0 ; i < str.length; ++i){
        let node = str[i]
        if (map.has(node)){
            if (stack.length){
                let arr = [node, [...stack].pop()]
                    .map(el => keys.indexOf(el))

                if (arr[0] < arr[1]){
                    return false
                }
            }
            stack.push(node)    
        }
        else if (values.includes(node)){
            let needKey = [...map.entries()].filter(el=>el[1]===node).pop().shift()
            if ([...stack].pop() !== needKey){
                return false
            }
            stack.pop()
        } 
    }    
    return stack.length === 0
}

从括号匹配的最简单情形,我们已经扩展到了比较复杂的括号匹配情形。至此,在逐步迭代的需求中给大家展示了栈的实际应用以及ES6中Map和扩展语法的使用。在面试的实际,即时反应很重要,也许并不拘泥于一种解法,从一点开始,逐步深入,慢慢延展,方能达到融会贯通,活学活用,这恐怕是面试官比较看中的。

阅读全文 »