bestvist

bestvist 查看完整档案

上海编辑  |  填写毕业院校  |  填写所在公司/组织 www.bestvist.com 编辑
编辑

追求完美,接受不完美

个人动态

bestvist 赞了文章 · 5月9日

你累死累活做业务,绩效还不怎么样,我只能帮你到这了……

前言

作为一个业务前端,完成业务需求的同时,还要处理各种线上问题,加班辛苦忙碌了一年,还要被老板说“思考是不够的”、“没有业务 sence”,出去面试,被问项目,也说不出什么有亮点或者有挑战的东西,想做点牛逼的东西,也没有发现什么有价值的方向,好不容易找到一些方向,还要被老板一顿质问,业务价值是什么?ROI 怎样?最终可能就只是做了一点性能优化工作,抽离了一些可复用的组件……不禁让人感叹,业务难、前端难、做业务的前端更难!

如果你也有这样的感受和困境,我想告诉你,这真的是太正常了,在阿里内部的技术论坛就有多篇关于这个问题的思考,我根据根据自己理解和调研,同时参考了多位不同前端领域专家的总结,整理成这篇文章,希望能对大家有所帮助。

1. 业务前端的困境

1.1 业务前端“好忙”

业务前端,顾名思义,做业务的前端,直接与业务的 PD、运营接触,对产品的用户直接负责。在实际的工作中,业务前端经常忙于业务的各种会议、项目和答疑,即便一条业务线上有多个前端同学支持,面对成山的需求,可能依然感到吃力,这其中的原因可能有:

  1. 用户侧产品往往需要快速上线,大部分需求都需要倒排工期,开发时间尤其紧张
  2. 对业务不熟悉,在项目需求已确定的时候才去参加视觉评审,没有办法判断需求背后的业务逻辑跟业务大节奏是否匹配、需求本身是否能够达成业务目标、有没有更好的实现方式,只能接下需求,然后排期
  3. 维护成本高,每天还要忙于解决各种线上问题,比如这里样式有点问题,那里怎么没有显示……各种琐碎问题让你过的非常“充实”
  4. 需求响应速度较慢,比如业务的技术栈较老,或者定制逻辑过多,边写代码还要边查文档,查不到可能还要查源码,效率大幅降低。又或者跟别的业务技术体系不同,难以复用和沉淀,如果要用,可能还要重写一遍……

1.2 业务前端是“资源”?

前端岗位的特点就是有视觉稿就可以完成工作,不需要理解业务全貌,所以在繁忙期很容易让前端忽视了业务思考,加上之前描述的各种原因,业务前端经常沦落为“资源”,当你沦落为“资源”的时候,其实就已经失去了和业务平等对话的资格,他们只会把你当成莫得感情的开发机器,跟你输入需求,让你吐出页面,而你在这样的关系中,本来写着还算工整的代码,为了快速实现业务需求,也开始写起乱糟糟的代码,对于你所创造的产品也没有话语权,久而久之也失去了激情和耐心。

失去激情,写的不开心也就算了,因为你没有做出什么特别的东西,老板也不会特别认可你的辛苦,还会觉得你思考不够、没有业务 sence,对业务没有助力,没有让业务因为你的存在而有所不同……

1.3 业务前端想突破

好吧,那我决定做点什么改变一下,于是跟老板提出了一系列想法:

  1. 这里技术体系太老了,为了进一步提升开发效率,我们想要搞技术重构
  2. 前后端联调有点费劲,我们想搞个联调数据中台,提升联调效率
  3. 那里展现速度太慢了,我们要搞性能优化
  4. ……

老板往往会来一系列灵魂提问:

  1. 为什么要做?(有什么业务价值?有什么技术价值?)
  2. 为什么是现在做?
  3. 为什么是你做?
  4. ROI(投入产出比)怎么样?

还没有开始,躁动的心就被老板的一系列“质疑”浇了一盆冷水。

如果没有回答好这些问题、说服老板,自然也争取不到什么资源,只能一个人搞搞,一个人搞的往往质量不行、也没有人用,久而久之自己也不维护了,只能又开始埋头在需求中。

干的不开心,也没有成长,最后只能暗淡离职,但换了一个公司就会好吗,很可能又是类似的过程……

这真的堪称是业务前端的“困境”,那么如何突破这种困境呢?首先我们就要摆正心态,从了解业务开始。

2. 了解业务

2.1 业务和需求

在了解业务之前,首先我们要知道,业务跟需求是不一样的。理解需求并不等于理解业务,需求是业务经过产品消化后的产物,可能已经经过演绎或者拆解,因此需求并不是业务本身,当然了解的需求越多,对业务的全貌也会更加了解。

那么什么是业务呢?业界对"业务"有多种定义,但是其主要思想基本不变,业务就是一系列人通过一系列活动完成某一任务的过程,因此,业务可大可小,可以无限拆分。

我们本文涉及的业务泛指商业业务,就是与该 BU 或者公司商业模式直接关联的业务或其组成部分。

2.2 前端为什么要学习业务

前端即使不学习业务,其实也不影响做需求,毕竟你只要告诉我交互是什么样的,前端就可以帮你实现,而且已经有产品经理的角色了,大家各司其职不就好了,为什么一个做技术的,要狗拿耗子、或者是越俎代庖呢?这就要说到:

  1. 只有了解业务,才能从技术的角度想到业务方不曾想到的地方;不了解业务,你可能听不懂业务方要什么,甚至连需求的业务逻辑都搞不清,这种情况的合作模式只有一种,需求下来了,你接住,然后给排期。也许,这个需求的设计不合理,你不知道;这个需求有更好的实现方案,你不知道;这个需求可以通过现成的关联产品方案解决,省时省人力,你也不知道。
  2. 只有了解到业务背后的原因,才能从全局的视角去规划技术的未来。不了解业务,会让你离用户的真实需求很远,你越难发现其中的一些痛点和挑战,没法真正提出你的思考和解决方案,去解决用户的难题。
  3. 作为一名产品研发工程师,自然是希望亲手打磨一款解决用户问题、体验友好的产品,如果产品能得到用户认可,产生影响力、自然会特别有成就感。
  4. 阿里作为一家商业科技公司,对技术人的要求就是技术与业务相结合,在满足业务需求的基础上,成为技术与业务的桥梁,主动走进业务,思考如何通过技术手段帮助业务做赢、满足市场和用户需求,先一步技术规划、人才储备、技术架构和技术预研。

2.3 你了解业务吗?

那么目前你了解你对接的业务吗?不妨尝试回答下以下问题:

  1. 业务做的是什么?产品大图有吗?
  2. 业务的核心指标是什么?KPI目标是什么,这些数字背后的含义是什么?要达成这些目标,业务策略是什么?
  3. 业务的用户是谁?流量怎么分层?占比多少?分别在业务中是怎样的定位?
  4. 业务的商业模式?靠什么吸引流量,盈利模式是怎样的?
  5. 我们做的页面是什么东西?为业务带来什么价值?要创造更多的价值,我们可以做什么?

2.4 如何学习业务?

2.4.1 业务领域知识的阅读

找到该领域相关的评分较好的书籍集中阅读,快速形成知识框架。

2.4.2 了解业务背景和规划

  1. 刚刚接手新的业务,可以邀请业务方老板或者资深的运营/产品同学,给你讲讲这块业务的过去、现在、未来、愿景、财年规划,以及对技术同学的期望;
  2. 花时间读合作方(运营、产品、研发)的周报,了解现在在发生什么,是不是离目标越来越近了;
  3. 了解业务目标、落地策略、衡量目标的数据口径,关注数据,关注目前做的项目是否为了达成目标而战,如果不是,提出你的想法和建议;
  4. 多参会,建立产品 sense。收集信息最好的方式就是参加所处业务老大的 KO 会,各种 KO 会会把战略上的拆解和背后的思考整体梳理之后宣讲传达给 BU 或部门的同学,

2.4.3 多交流

与服务端同学聊天,与 PM 聊天,与用户聊天,多角度看业务,但要注意的是,针对专业型比较强的业务,需要先做功课,至少一些英文的缩写要清楚的明白意思。

2.4.4 谨记数字

如果前面还需要花比较长的时间,那这一个可以现在就做起来,那就是把业务相关的数字记得越精细约好,越具体越好,越全面越多越好。这样做有两个好处:

  1. 所记的数字指标本身,很大程度已经涵盖了这个业务价值方向,你便知道了这个业务重点关注的是哪个维度的东西
  2. 这些数字可以作为和业务方以及产品“平等对话”的源头,否则连最基本的对话基础都没有

2.4.5 从日常需求入手

对于项目中的需求,我们要尝试分析背后的目的和价值,做了之后有什么预期的收益,为什么这么做就可以达到这个收益,跟总体目标是否契合,还要判断业务方提到的点是不是有效的方案或者说成本太大的方案,看能不能给出替代方案,用现有的方案或者小成本的方式来满足业务方。

而在项目提测上线后,还要仔细分析以及多关注上线之后的业务数据和效果,会有如下好处:

  1. 提高自己对业务的理解能力,你在关注业务数据的同时,也就会更多的从业务的角度来看到这个功能所带来的价值是否符合预期,当出现不符合预期的时候,可以和业务方一起进行数据漏斗的分析从而找到问题所在,避免我们的劳动成果成为一次性的工作。
  2. 总结的同时可以帮助自己梳理这个项目中自己哪些地方做的不足,或者相关推进中存在什么问题,以及后面怎么改进,提高了下次项目中的迭代效率和质量。比如这个项目是否存在需求理解不到位存在返工,或者沟通 & 联调低效,环境不稳定,自己设计的方案是否合理等问题,后续要怎么解决。
  3. 也可以从数据和总结中判断出什么样的需求是靠谱的 & 什么的样业务方是靠谱的,频繁争取资源上线效果又不好的业务方,下次再有需求过来则需要多增加一个心眼和思考的过程。

2.4.6 坚持

业务思考力,没有个至少半年是不会见效的

3. 助力业务

3.1 思考

尽管平时的业务很忙,但再忙,也要抽时间思考,那么思考哪些内容呢?以下举一些例子:

  1. 养成每天记工作内容的习惯,分析一下自己的时间到底耗在哪了
  2. 在业务开发中,有遇到让你特别想吐槽的点吗?想下问题背后的原因,有什么方法可以避免下次不犯,能不能提炼为更加通用的解决方案,其他同学怎么解决的,我可以怎么解决?
  3. 不断地输入、观察,业务的真实需求是什么?站在业务方的角度思考,业务遇到的痛点、挑战在哪里?

3.2 沟通

和老板、团队同学、业务方对焦,确认“我想做的”是不是“大家想要的”?

你可能会提出很多意见,但一般会遭到老板或者业务方无情的拒绝,而且问得你一脸懵逼,就比如:

  1. 当前业务背景下,为什么要做?(有什么业务价值?有什么技术价值?)
  2. 现在必须做么?
  3. 为什么是你做?
  4. 怎么做?(体系化、全链路、单点技术挑战)
  5. 有什么业务和技术结果?能否被复用?
  6. 未来规划(能否跟BU或集团的方案联动、共建)

而这往往是因为你提出要做的事情,有价值但不是必须做的,没有结合目前业务需要什么。也就是说,你想做的技术是个人和纯技术角度思考的,没有基于业务的现状和痛点去考虑技术方案,不接地气,投入产出比不高。

所以给技术产出先找好业务的阵地,看看有没有可以借力的地方,不要重复造轮子。快速验证这个方向的正确性后,再逐渐多加投入、丰满技术设计。不要自己YY、默默地做完,这样做出来的东西没有业务场景埋单。

3.3 技术规划

业务赋能其实是需要我们紧贴业务规划,制定技术规划和方案。在了解业务方今年的 KPI 重点是什么,预计的拆解和实现路径是什么后,再结合自己的和团队情况,想想自己能做哪些事情来帮助业务实现其 KPI,这里有两点需要注意下:

抓住本质从点及面,通盘考虑: 很多时候,我们收到的痛点和业务需求都是单点的,这时我们不能着眼于眼前的单点问题,而需要通盘来考虑,比如SEO的页面对性能非常敏感,经常可能会收到一些业务方来反馈,说目前我们的SEO有这个地方,那个地方需要优化下,而单点解决这些问题可能对业务带来的收益并不大,对自己的技能也没有什么成长。这时候如果通盘考虑这个命题,其实会发现做SEO页面的优化,其实目的是为了提升SEO页面的收录和排名。而提升SEO页面的收录和排名其实不仅有前端性能优化这一个路径,而是还有一些其他的路径:比如优化关键词&长尾词,采用Google的AMP技术改造SEO页面,优化爬虫爬取页面的耗时提升爬取率等等。这样就能吧点的问题转化为面的问题,才能制定更有效和全面的抓手来赋能业务。

既要解决眼前痛点,也要长远谋划: 很多时候我们不能仅满足于眼前的KPI,还需要了解业务方长远的想法和可以预见的规划。就比如试点的新业务,一层规划是保证业务项目的按时上线,考虑到未来,另一层规划可能就是如何做到技术方案的可以复制性。

3.4 站在巨人的肩膀上

当你需要制定一个产品化的方案或者工具和框架的时候,最好先放眼集团内部和行业进行一番调研,看看业界和其他同事是怎么解决这个问题的。尽量站在别人的肩膀上做出创新或者参与共建,避免小团队内造出重复和质量低的轮子

4. 技术深度

4.1 技术知识与技术能力

“技术”不能是一个笼统的词汇,我想它至少可以分为“技术知识”和“技术能力”两大部分。

什么是“技术知识”?知识就是 I KNOW

  • 《TypeScript 从入门到放弃》
  • 《React 从入门到放弃》
  • 《Webpack 从入门到放弃》
  • ......

什么是“技术能力”?能力就是 I CAN

  • 我用 TypeScript 重构了一个大型系统,代码健壮性及研发效率大幅提升。
  • 我用 React Hooks 给全栈同学进行前端培训,培训效果大幅提升。
  • 我深入研究了 Webpack,优化配置,使得系统构建速度大幅提升。
  • .....

4.2 培养技术视野

  1. 关注日常业界新技术。不一定要深入了解,但对新技术保持好奇心,大概了解它是做什么的,如果在工作中遇到匹配的落地环境,可以考虑写个 demo 看看是不是有价值
  2. 关注集团和业界的解决方案。在业务中发现问题,做解决方案的时候,我们很容易陷入自己的设计中,一脑子地想把所有东西都自己做出来,但投入会非常大,产出的价值是否一样大呢?不知道。大部分情况下,你想做的,在ATA能搜到,前人踩的坑,或者已有的成熟的解决方案,只要你去沟通去接触,就可以轻松地接进来,为什么要花大量的时间去造轮子呢?可以借力的地方,就去借力吧,把时间剩下来,做你的解决方案中更核心更有价值的事情。

4.3 技术深度

一聊到“技术深度”,可能很自然地会认为是在某项技术上挖得很深,或者解决了一个业界公认难度很高的技术难题,但这只是“技术深度”的其中一部分:

  1. 体系化 / 系统化
    体系化思维是认识事物的一种方式,在面对问题的时候,能够针对复杂的问题,列出关键的要素和解决方法,将散乱无序的问题,变得逻辑清晰,有章可循。
    在问题的定位和解决的体现,从表象到本质,拆解出造成问题背后的原因,针对性地去解决本质的原因,而非治标不治本,有解决方案有节奏地解决。
  2. 全链路
    除了前端的部分,向前向后的技术栈,还能挖多深。
  3. 单点技术挑战
    在某个技术挑战上,你的思考和解决方案是怎样的。

4.4 技术与业务共赢

真正有突破性的、带来重大价值的业务成果必然伴随着技术上的深入乃至创新,所以在做业务成果的时候,一定会有让我们增加技术深度的场景。

5. 给你更多体感

培养业务感确实是一件非常有难度的事情,他要求你以业务而非技术为第一视角,这可能违背了很多人内心的“技术坚持”,但如果一直做技术,其实是很难有非常大的突破的,在工作中,如果能实现技术与业务共赢,将会助力你到达更高的高度。

改变的确很难,但结果值得冒险。

查看原文

赞 44 收藏 18 评论 2

bestvist 发布了文章 · 2019-05-08

前端知识汇总

记录一些前端常用的基础知识点

Github地址,多多star ^_^

技能树

图片描述

BFC

BFC 定义: BFC(Block formatting context)直译为"块级格式化上下文"。它是一个独立的渲染区域,只有Block-level box参与, 它规定了内部的Block-level Box如何布局,并且与这个区域外部毫不相干。

BFC布局规则:

  • 内部的Box会在垂直方向,一个接一个地放置。
  • Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠
  • 每个元素的margin box的左边, 与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。
  • BFC的区域不会与float box重叠。
  • BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。
  • 计算BFC的高度时,浮动元素也参与计算

哪些元素会生成BFC:

  • 根元素
  • float属性不为none
  • position为absolute或fixed
  • display为inline-block, table-cell, table-caption, flex, inline-flex
  • overflow不为visible

参考

浏览器渲染页面过程

  1. 用户输入URL地址
  2. 对URL地址进行DNS域名解析
  3. 建立TCP连接(三次握手)
  4. 浏览器发送HTTP请求报文
  5. 服务器返回HTTP响应报文
  6. 关闭TCP连接(四次挥手)
  7. 浏览器解析文档资源并渲染页面

TCP

TCP三次握手

图片描述

TCP四次挥手

图片描述

JS单线程运行机制

  • 消息队列:消息队列是一个先进先出的队列,它里面存放着各种消息。
  • 事件循环:事件循环是指主线程重复从消息队列中取消息、执行的过程。

主线程只会做一件事情,就是从消息队列里面取消息、执行消息,再取消息、再执行。当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。消息就是注册异步任务时添加的回调函数。

事件循环

macroTask(宏任务): 主代码块, setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering

microTask(微任务): process.nextTick, Promise, Object.observe, MutationObserver

事件

事件流

  • 事件捕获阶段
  • 处于目标阶段
  • 事件冒泡阶段

事件委托

不在事件的发生地(直接dom)上设置监听函数,而是在其父元素上设置监听函数,通过事件冒泡,父元素可以监听到子元素上事件的触发,通过判断事件发生元素DOM的类型,来做出不同的响应。

举例:最经典的就是ul和li标签的事件监听

HTML

基础标签

<head></head>

<meta />

<link rel="stylesheet" href="" />

<title></title>

<body></body>

<center></center>

<section></section>

<article></article>

<aside></aside>

<div></div>

<ul></ul>

<li></li>

<p></p>

<h1></h1>
~
<h6></h6>

<button></button>

<input type="text" />

<a href=""></a>

<span></span>

<strong></strong>

<i></i>

CSS

CSS 样式

优先级: 行内样式 > 链接式 > 内嵌式 > @import 导入式

选择器

/* 选择所有元素 */
* {
}

/* 选择 div 元素 */
div {
}

/* 选择类名元素 */
.class {
}

/* 选择 id 元素 */
#id {
}

/* 选择 div 元素内的所有 p 元素 */
div p {
}

/* 选择 div 元素内下一层级的 p 元素 */
div > p {
}

css选择器权重: !important -> 行内样式 -> #id -> .class -> 元素和伪元素 -> * -> 继承 -> 默认

文本溢出

// 文本溢出单行显示
.single {
  overflow: hidden;
  text-overflow:ellipsis;
  white-space: nowrap;
}

// 文本溢出多行显示
.multiple {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
}

CSS3 新特性

  • transition:过渡
  • transform:旋转、缩放、移动或者倾斜
  • animation:动画
  • gradient:渐变
  • shadow:阴影
  • border-radius:圆角

Javascript

原型与原型链

  • 实例的 proto 属性(原型)等于其构造函数的 prototype 属性。
  • Object.proto === Function.prototype
  • Function.prototype.proto === Object.prototype
  • Object.prototype.proto === null

继承实现

function extend(child, parent) {
    var F = function() {}; // 空函数为中介,减少实例时占用的内存

    F.prototype = parent.prototype; // f继承parent原型

    child.prototype = new F(); // 实例化f,child继承,child、parent原型互不影响

    child.prototype.constructor = child; // child构造函数指会自身,保证继承统一

    child.super = parent.prototype; // 新增属性指向父类,保证子类继承完备
}

深拷贝

function deepCopy(s, t) {
    t = t || (Object.prototype.toString.call(t) === "[object Array]" ? [] : {});

    for (var i in s) {
        if (typeof s[i] === "object") {
            t[i] = deepCopy(s[i], t[i]);
        } else {
            t[i] = s[i];
        }
    }

    return t;
}

Ajax

var ajax = {};

ajax.get = function(url, fn) {
    var xhr = new XMLHttpRequest();

    xhr.open("GET", url, true);

    xhr.onreadystatechange = function() {
        if (
            xhr.readyState === 4 &&
            (xhr.status === 200 || xhr.status === 403)
        ) {
            fn.call(this, xhr.responseText);
        }
    };

    xhr.send();
};

ajax.post = function(url, data, fn) {
    var xhr = new XMLHttpRequest();

    xhr.open("POST", url, true);

    xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

    xhr.onreadystatechange = function() {
        if (
            xhr.readyState === 4 &&
            (xhr.status === 200 || xhr.status === 403)
        ) {
            fn.call(this, xhr.responseText);
        }
    };

    xhr.send(data);
};

格式化日期

function formatDate(date, format) {
    if (arguments.length === 0) return null;

    format = format || "{y}-{m}-{d} {h}:{i}:{s}";

    if (typeof date !== "object") {
        if ((date + "").length === 10) date = parseInt(date) * 1000;
        date = new Date(date);
    }

    const dateObj = {
        y: date.getFullYear(),
        m: date.getMonth() + 1,
        d: date.getDate(),
        h: date.getHours(),
        i: date.getMinutes(),
        s: date.getSeconds(),
        a: date.getDay()
    };

    const dayArr = ["一", "二", "三", "四", "五", "六", "日"];

    const str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (match, key) => {
        let value = dateObj[key];

        if (key === "a") return dayArr[value - 1];

        if (value < 10) {
            value = "0" + value;
        }

        return value || 0;
    });

    return str;
}

new 实现

function New(Class) {
    let obj = {};
    obj.__proto__ = Class.prototype;
    let res = Class.call(obj);
    return typeof res === 'object' ? res : obj;
}

call 实现

Function.prototype.callfb = function (ctx) {
    if (typeof this !== 'function') {
        throw new Error('Function undefined');
    }

    ctx = ctx || window;

    const fn = ctx.fn;

    ctx.fn = this;

    const args = [...arguments].slice(1);

    const res = ctx.fn(...args);

    ctx.fn = fn;

    return res;
}

apply 实现

Function.prototype.applyFb = function (ctx) {
    if (typeof this !== 'function') {
        throw new Error('Function undefined');
    }

    ctx = ctx || window;

    const fn = ctx.fn;

    ctx.fn = this;

    const arg = arguments[1];

    const res = Array.isArray(arg) ? ctx.fn(...arg) : ctx.fn();

    ctx.fn = fn;

    return res;
}

bind 实现

Function.prototype.bindFb = function (ctx) {

    const fn = this;

    const args = [...arguments].slice(1);

    const F = function () {};

    const fBind = function () {
        return fn.apply(this instanceof fBind ? this : ctx, args.concat(...arguments))
    }

    if (fn.prototype) {
        F.prototype = fn.prototype;
    }

    fBind.prototype = new F();

    return fBind;
}

instanceof 实现

function instanceofFb(left, right) {
    let proto, prototype = right.prototype;

    proto = left.__proto__;

    while (proto) {

        if (proto === prototype) {
            return true;
        }

        proto = proto.__proto__;

    }

    return false;
}

Promise 实现

function promiseFb(fn) {
    const _this = this;
    this.state = 'pending'; // 初始状态为pending
    this.value = null;
    this.resolvedCallbacks = []; // 这两个变量用于保存then中的回调,因为执行完Promise时状态可能还是pending
    this.rejectedCallbacks = []; // 此时需要吧then中的回调保存起来方便状态改变时调用

    function resolve(value) {
        if (_this.state === 'pending') {
            _this.state = 'resolved';
            _this.value = value;
            _this.resolvedCallbacks.map(cb => { cb(value) }); // 遍历数组,执行之前保存的then的回调函数
        }
    }

    function reject(value) {
        if (_this.state === 'pending') {
            _this.state = 'rejected';
            _this.value = value;
            _this.rejectedCallbacks.map(cb => { cb(value) });
        }
    }

    try {
        fn(resolve, reject);
    } catch (e) {
        reject(e);
    }
}

promiseFb.prototype.then = function (onFulfilled, onRejected) {
    // 因为then的两个参数均为可选参数,
    // 所以判断参数类型本身是否为函数,如果不是,则需要给一个默认函数如下(方便then不传参数时可以透传)
    // 类似这样: Promise.resolve(4).then().then((value) => console.log(value))
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : fn => fn;
    onRejected = typeof onRejected === 'function' ? onRejected : e => { throw e };

    switch (this.state) {
        case 'pending':
            // 若执行then时仍为pending状态时,添加函数到对应的函数数组
            this.resolvedCallbacks.push(onFulfilled);
            this.rejectedCallbacks.push(onRejected);
            break;
        case 'resolved':
            onFulfilled(this.value);
            break;
        case 'rejected':
            onRejected(this.value);
            break;
        default: break;
    }
}

debounce 防抖

function debounce(fn, wait, immediate) {
    let timer;
    return function () {
        if (immediate) {
            fn.apply(this, arguments);
        }
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, arguments);
        }, wait)
    }
}

throttle 节流

function throttle(fn, wait) {
    let prev = new Date();
    return function () {
        const now = new Date();
        if (now - prev > wait) {
            fn.apply(this, arguments);
            prev = now;
        }
    }
}

双向绑定

双向绑定:视图(View)的变化能实时让数据模型(Model)发生变化,而数据的变化也能实时更新到视图层.

图片描述

Object.defineProperty

<!DOCTYPE html>
<html lang="en">
<head>
    <title>mvvm</title>
</head>
<body>
    <p>数据值:<span id="data"></span></p>
    <p><input type="text" onkeyup="keyup()"></p>
    <script>
        var obj = {
            data: ''
        }

        function keyup(e) {
            e = e || window.event;
            obj.data = e.target.value; // 更新数据值
        }

        Object.defineProperty(obj, 'data', {
            get: function () {
                return this.data;
            },
            set: function (newVal) {
                document.getElementById('data').innerText = newVal; // 更新视图值
            }
        })
    </script>
</body>
</html>

Proxy

<!DOCTYPE html>
<html lang="en">
<head>
    <title>mvvm</title>
</head>
<body>
    <p>数据值:<span id="data"></span></p>
    <p><input type="text" onkeyup="keyup()"></p>
    <script>
        var obj = new Proxy({}, {
            get: function (target, key, receiver) {
                return Reflect.get(target, key, receiver);
            },
            set: function (target, key, value, receiver) {
                if (key === 'data') {
                    document.getElementById('data').innerText = value; // 更新视图值
                }
                return Reflect.set(target, key, value, receiver);
            }
        })

        function keyup(e) {
            e = e || window.event;
            obj.data = e.target.value; // 更新数据值
        }
    </script>
</body>
</html>

算法

冒泡排序

两两对比

function bubble(arr) {
    const len = arr.length;

    for (let i = 0; i < len; i++) {
        for (let j = 0; j < len - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                let temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
    return arr;
}

选择排序

寻找最小的数,将索引保存

function selection(arr) {
    const len = arr.length;
    let minIndex, temp;
    for (let i = 0; i < len - 1; i++) {
        minIndex = i;
        for (let j = i + 1; j < len; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        temp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
    }
    return arr;
}

Webpack

常用loader

  • file-loader: 加载文件资源,如 字体 / 图片 等,具有移动/复制/命名等功能;
  • url-loader: 通常用于加载图片,可以将小图片直接转换为 Date Url,减少请求;
  • babel-loader: 加载 js / jsx 文件, 将 ES6 / ES7 代码转换成 ES5,抹平兼容性问题;
  • ts-loader: 加载 ts / tsx 文件,编译 TypeScript;
  • style-loader: 将 css 代码以<style>标签的形式插入到 html 中;
  • css-loader: 分析@import和url(),引用 css 文件与对应的资源;
  • postcss-loader: 用于 css 的兼容性处理,具有众多功能,例如 添加前缀,单位转换 等;
  • less-loader / sass-loader: css预处理器,在 css 中新增了许多语法,提高了开发效率;

常用plugin

  • UglifyJsPlugin: 压缩、混淆代码;
  • CommonsChunkPlugin: 代码分割;
  • ProvidePlugin: 自动加载模块;
  • html-webpack-plugin: 加载 html 文件,并引入 css / js 文件;
  • extract-text-webpack-plugin / mini-css-extract-plugin: 抽离样式,生成 css 文件;
  • DefinePlugin: 定义全局变量;
  • optimize-css-assets-webpack-plugin: CSS 代码去重;
  • webpack-bundle-analyzer: 代码分析;
  • compression-webpack-plugin: 使用 gzip 压缩 js 和 css;
  • happypack: 使用多进程,加速代码构建;
  • EnvironmentPlugin: 定义环境变量;
查看原文

赞 21 收藏 17 评论 0

bestvist 评论了文章 · 2019-03-07

JS设计模式-单例模式

单例模式是一个用来划分命名空间并将一批属性和方法组织在一起的对象,如果它可以被实例化,那么它只能被实例化一次。

原文链接

单例模式优点

  • 划分命名空间,减少全局变量
  • 组织代码为一体,便于阅读维护
并非所有的对象字面量都是单例,比如模拟数据
基本结构:
let Cat = {
   name: 'Kitty',
   age: 3,
   run: ()=>{
      console.log('run');
   }
}

上面对象字面量结构是创建单例模式的方法之一,但并不是单例模式,单例模式的特点是仅被实例化一次
要实现单例模式可以使用变量来标示该类是否被实例

基本实现:
class Singleton {
    constructor(name){
        this.name = name;
        this.instance = null;
    }
    getName(){
        return this.name;
    }
}

let getInstance = (()=> {
    let instance;
    return (name)=> {
        if(!instance) {
            instance = new Singleton(name);
        }
        return instance;
    }
})()

let cat1 = getInstance('Hello');
let cat2 = getInstance('Kitty');
console.log(cat1 === cat2); //true
console.log(cat1.getName()) //'Hello'
console.log(cat2.getName()) //'Hello'

用instance变量标示实例Singleton,如果没有实例创建一个,如果有则直接返回实例,由于仅能被实例化一次,cat2得到的实例和cat1相同

实用
在创建dom元素时为避免重复创建,可以使用单例模式创建


//单例模式
let createModal = function() {
    let content = document.createElement('div');
    content.innerHTML = '弹窗内容';
    content.style.display = 'none';
    document.body.appendChild(content);
}

//代理获取实例
let getInstance = function(fn) {
    let result
    return function() {
        return result || (result = fn.apply(this,arguments));
    }
}

let createSingleModal = getInstance(createModal);
document.getElementById("id").onclick = function(){
    let modal = createSingleModal();
    modal.style.display = 'block';
};

单例模式是一种简单却非常使用的设计模式,在需要时创建实例,并且只创建唯一一个

查看原文

bestvist 评论了文章 · 2019-01-25

有哪些鲜为人知,但是很有意思的网站?

扩展阅读

网站之最

工具类

图片/视频工具

IT/AI/工具类

音乐/影视类

二次元/动漫类

文艺类

网盘/搜索类

文学/百科类

实用/行政类

趣味/无聊类

声音/太空类

怀旧类

游戏/测试类

特别推荐

原文地址

查看原文

bestvist 发布了文章 · 2018-12-18

2018博客回顾

8012年马上过去了,整理一下今年的博客内容。

canvas烟花锦集

时间:2018-01-15

内容:了解canvas知识,不同烟火效果实现方式。

链接:https://www.bestvist.com/p/49

Skeleton Screen -- 骨架屏

时间:2018-01-19

内容:用户体验一直是前端开发需要考虑的重要部分,在数据请求时常见到锁屏的loading动画,而现在越来越多的产品倾向于使用Skeleton Screen Loading(骨架屏)替代,以优化用户体验。

链接:https://www.bestvist.com/p/50

Fetch -- http请求的另一种姿势

时间:2018-02-05

内容:传统Ajax是利用XMLHttpRequest(XHR)发送请求获取数据,不注重分离的原则。而Fetch API是基于Promise设计,专为解决XHR问题而出现。

链接:https://www.bestvist.com/p/51

JS中的async/await -- 异步隧道尽头的亮光

时间:2018-02-07

内容:JS中的异步操作从最初的回调函数演进到Promise,再到Generator,都是逐步的改进,而async函数的出现仿佛看到了异步方案的终点,用同步的方式写异步。

链接:https://www.bestvist.com/p/52

mvvm-simple双向绑定简单实现

时间:2018-04-17

内容:mvvm模式解放DOM枷锁

链接:https://www.bestvist.com/p/53

JavaScript Promise查缺补漏

时间:2018-04-23

内容:Promise 对象代表了未来将要发生的事件,用来传递异步操作的消息。

链接:https://www.bestvist.com/p/54

鼠标移入移出效果 -- jQuery/Vue版

时间:2018-06-21

内容:根据鼠标hover的方向弹出、收回遮罩层,分为jQuery和Vue版。

链接:https://www.bestvist.com/p/56

后管模版整理

时间:2018-08-15

内容:整理一些后管模版,包含bootstrap、vue、react、angular等主流框架。

链接:https://www.bestvist.com/p/57

花样形状 -- CSS

时间: 2018-08-27

内容:只使用一个html元素绘制图形,部分图形需要浏览器支持。

链接:https://www.bestvist.com/p/58

UI酷炫交互效果

时间:2018-10-08

内容:多端交互效果,UI设计。

链接:https://www.bestvist.com/p/59

vue组件从开发到发布

时间:2018-10-15

内容:梳理一篇vue组件从开发到发布托管流程。

链接:https://www.bestvist.com/p/60

JWT - just what?

时间:2018-12-07

内容:初见JWT,不知所云,赶紧Google(百度)一下,原来是跨域身份验证解决方案。

链接:https://www.bestvist.com/p/62

原文地址:https://www.bestvist.com/p/63

查看原文

赞 1 收藏 1 评论 0

bestvist 发布了文章 · 2018-12-10

JWT - just what?

图片描述

初见JWT,不知所云,赶紧Google(百度)一下,原来是跨域身份验证解决方案。

JWT只是缩写,全拼则是 JSON Web Tokens ,是目前流行的跨域认证解决方案,一种基于JSON的、用于在网络上声明某种主张的令牌(token)。

JWT 原理

jwt验证方式是将用户信息通过加密生成token,每次请求服务端只需要使用保存的密钥验证token的正确性,不用再保存任何session数据了,进而服务端变得无状态,容易实现拓展。

加密前的用户信息,如:

{
    "username": "vist",
    "role": "admin",
    "expire": "2018-12-08 20:20:20"
}

客户端收到的token:

7cd357af816b907f2cc9acbe9c3b4625

JWT 结构

一个token分为3部分:

  • 头部(header)
  • 载荷(payload)
  • 签名(signature)

3个部分用“.”分隔,如:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c 

头部

JWT的头部分是一个JSON对象,描述元数据,通常是:

{
  "typ": "JWT",
  "alg": "HS256"
}
  • typ 为声明类型,指定 "JWT"
  • alg 为加密的算法,默认是 "HS256"

也可以是下列中的算法:

JWS算法名称描述
HS256HMAC256HMAC with SHA-256
HS384HMAC384HMAC with SHA-384
HS512HMAC512HMAC with SHA-512
RS256RSA256RSASSA-PKCS1-v1_5 with SHA-256
RS384RSA384RSASSA-PKCS1-v1_5 with SHA-384
RS512RSA512RSASSA-PKCS1-v1_5 with SHA-512
ES256ECDSA256ECDSA with curve P-256 and SHA-256
ES384ECDSA384ECDSA with curve P-384 and SHA-384
ES512ECDSA512ECDSA with curve P-521 and SHA-512

载荷

载荷(payload)是数据的载体,用来存放实际需要传递的数据信息,也是一个JSON对象。
JWT官方推荐字段:

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

也可以使用自定义字段,如:

{
    "username": "vist",
    "role": "admin"
}

签名

签名部分是对前两部分(头部,载荷)的签名,防止数据篡改。

按下列步骤生成:
1、先指定密钥(secret)
2、把头部(header)和载荷(payload)信息分别base64转换
3、使用头部(header)指定的算法加密
最终,签名(signature) = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)

客户端得到的签名:

header.payload.signature

也可以对JWT进行再加密。

JWT 使用

1、服务端根据用户登录状态,将用户信息加密到token中,返给客户端
2、客户端收到服务端返回的token,存储在cookie中
3、客户端和服务端每次通信都带上token,可以放在http请求头信息中,如:Authorization字段里面
4、服务端解密token,验证内容,完成相应逻辑

JWT 特点

  • JWT更加简洁,更适合在HTML和HTTP环境中传递
  • JWT适合一次性验证,如:激活邮件
  • JWT适合无状态认证
  • JWT适合服务端CDN分发内容
  • 相对于数据库Session查询更加省时
  • JWT默认不加密
  • 使用期间不可取消令牌或更改令牌的权限
  • JWT建议使用HTTPS协议来传输代码

原文:https://www.bestvist.com/p/62

查看原文

赞 4 收藏 2 评论 0

bestvist 赞了文章 · 2018-12-06

那些让程序员崩溃又想笑的程序命名...

本文旨在用最通俗的语言讲述最枯燥的基本知识

===================1===================

到一家创业公司上班的第一天,老员工刘XX给我看了公司他负责的项目,奇怪的是,命名是“LiuQXProject”,刘XX看着惊愕的我说:“怎么了?有什么错吗?”

===================2===================

给同事做双十一活动相关代码的review,学到到了很多中英混血单词
,获取双十一拼团活动数据的接口叫做“get_ShuangShiYi_GroupTuan_activity_data”,特等奖的命名:TeDeng_price....更气人的是,我们活动奖等有十级,他就虔诚地继续OneDeng_price、TwoDeng_price
直到JiuDeng_price。。。噢,no!!好气啊!!而且他还把”奖“的单词prize写成了price,怎么说呢?好难受..


===================3===================
公司来了个刚毕业的小伙子,自诩前端未来之星,喜欢研读源码,对开源充满热爱,一个月后,无意间打开他写的一个js文件,让我惊讶的是:变量从a到z全部用完,更气人的是,26个字母用完之后,他竟然丧心病狂的用起来了双拼,var aa=1,var ab=“12”,var ac=null...我问他为什么这样命名,他说你没研读jQuery源码吗?人家就是这样做的,简洁大气上档次!


===================4===================
因为微信昵称经常有带有一些乱七八糟的表情或者字符,在正常情况下utf-8编码的数据库是存不进去的,因此让同事帮忙写个把微信昵称转换成正常的字符串的一个工具函数,最终我拿到了这个工具函数,名字叫做:convertingWechatNicknameintoNormalCharacters(String nickName)


===================5===================
实习小伙子来的头一天就搞的满身大汗,我说怎么了,他说我明明写了main方法,为什么运行不了,我一看代码,我噻~main写成了mian,怎么可能跑得起来啊!更残暴的是:苹果手机是apple_sj,Android手机是android_sj,哈哈~

以上的种种让人哭笑不得的命名问题..相信很多小伙伴也会碰过这样,有些是因为经验不足,有些是因为一直没有对自己写的代码做一些规范化的工作,有的是因为被老项目、前辈带出来的坏习惯...这些都是编程世界里非常不好的行为,拒不完全统计:在一个项目中,程序员80%的时间都是在和变量、函数、方法打交道,因此一个好的命名习惯,比注释或一份详细的开发文档都重要。针对于此,小编特意根据行业标准---阿里开发文档,做了一些参考和摘抄,整理出一份关于命名方面的规范,给需要的你作参考。

争取多写漂亮代码,少写注释!!!

文章提纲:

  1. 整体规范
  2. 包规范
  3. 类规范
  4. 方法规范
  5. OOP的一些强制规范

1. 整体规范

  1. 所有的命名必须以英文意译,不能以中文拼音意译,如:获取我的消息接口,可以写:myMessage;但不能写:myXiaoXi
  2. 尽量用精简的英文命名,但要完整表达其意义,杜绝int a ,int a1 int aa这种毫无意义的简化写法。
  3. 所有命名不能以特殊符号开始,如:_age,_username
  4. 常量用全大写定义,单词之间用下划线分割语义,如:public final int REDIS_MAX_IDLE=5;

2. 包规范

  1. 包名全小写,不能用特殊符号或者驼峰写法如:com.courseLog.uitl_con是不合规范的。
  2. 包名要符合包的作用,比如数据层要写dao,工具包要写util等

3. 类规范

  1. 类名风格为大写开头的驼峰命名方式,如:ApiController、TestController等
  2. 异常类命名使用Exception结尾,如:CustomerException
  3. 抽象类命名使用Abstract开头,如:AbstractCustomer
  4. 测试类命名以它要测试的类的名称开始,以 Test 结尾,如:CustomerControllerTest
  5. 枚举类命名要以Enum结尾,如果CustomerRoleEnum
  6. 其它类型的类命名,在描述类作用的同时,也尽可能表达出类所用的一些设计模式

4. 方法规范

  1. 方法名使用驼峰写法,以小写字母开头,如:getUserCourse();
  2. 方法内的参数名、成员变量、局部变量均使用驼峰写法,以小写字母开头,如:int userName;
  3. 接口类的方法和属性不要加上任何修饰符,保证代码的简介。
  4. 方法定义必须要有注释,包括(方法作用、参数名、返回类型、创建时间等)
  5. Service/DAO层方法命名规约:
1) 获取单个对象的方法用get做前缀。
2) 获取多个对象的方法用list做前缀。
3) 获取统计值的方法用count做前缀。
4) 插入的方法用save/insert做前缀。
5) 删除的方法用remove/delete做前缀。
6) 修改的方法用update做前缀。

5. OOP的一些强制规范

  1. 尽量避免使用可变参数编程,相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免使用 Object
  2. 接口过时必须加@Deprecated 注解
  3. 不能使用过时的类或方法
  4. 所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较
  5. 类内方法定义顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter 方法。

觉得本文对你有帮助?请分享给更多人
关注「编程无界」,提升装逼技能

查看原文

赞 29 收藏 15 评论 16

bestvist 评论了文章 · 2018-12-04

前站 - 前端导航,搜索社区,阅读文章,提升技术

推荐一个前端导航网站,记录了各种关于前端的网址,可以直接在对应社区查找问题,还可以查看github热门项目

喜欢的话赶快收藏一波吧 ^^

图片描述

网址:https://www.frontendjs.com/

查看原文

bestvist 赞了文章 · 2018-12-04

前端与编译原理——用JS写一个JS解释器

图片描述

说起编译原理,印象往往只停留在本科时那些枯燥的课程和晦涩的概念。作为前端开发者,编译原理似乎离我们很远,对它的理解很可能仅仅局限于“抽象语法树(AST)”。但这仅仅是个开头而已。编译原理的使用,甚至能让我们利用JS直接写一个能运行JS代码的解释器。

项目地址:https://github.com/jrainlau/c...

在线体验:https://codepen.io/jrainlau/p...

一、为什么要用JS写JS的解释器

接触过小程序开发的同学应该知道,小程序运行的环境禁止new Functioneval等方法的使用,导致我们无法直接执行字符串形式的动态代码。此外,许多平台也对这些JS自带的可执行动态代码的方法进行了限制,那么我们是没有任何办法了吗?既然如此,我们便可以用JS写一个解析器,让JS自己去运行自己。

在开始之前,我们先简单回顾一下编译原理的一些概念。

二、什么是编译器

说到编译原理,肯定离不开编译器。简单来说,当一段代码经过编译器的词法分析、语法分析等阶段之后,会生成一个树状结构的“抽象语法树(AST)”,该语法树的每一个节点都对应着代码当中不同含义的片段。

比如有这么一段代码:

const a = 1
console.log(a)

经过编译器处理后,它的AST长这样:

{
  "type": "Program",
  "start": 0,
  "end": 26,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 11,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 11,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 7,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 10,
            "end": 11,
            "value": 1,
            "raw": "1"
          }
        }
      ],
      "kind": "const"
    },
    {
      "type": "ExpressionStatement",
      "start": 12,
      "end": 26,
      "expression": {
        "type": "CallExpression",
        "start": 12,
        "end": 26,
        "callee": {
          "type": "MemberExpression",
          "start": 12,
          "end": 23,
          "object": {
            "type": "Identifier",
            "start": 12,
            "end": 19,
            "name": "console"
          },
          "property": {
            "type": "Identifier",
            "start": 20,
            "end": 23,
            "name": "log"
          },
          "computed": false
        },
        "arguments": [
          {
            "type": "Identifier",
            "start": 24,
            "end": 25,
            "name": "a"
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}
常见的JS编译器有babylonacorn等等,感兴趣的同学可以在AST explorer这个网站自行体验。

可以看到,编译出来的AST详细记录了代码中所有语义代码的类型、起始位置等信息。这段代码除了根节点Program外,主体包含了两个节点VariableDeclarationExpressionStatement,而这些节点里面又包含了不同的子节点。

正是由于AST详细记录了代码的语义化信息,所以Babel,Webpack,Sass,Less等工具可以针对代码进行非常智能的处理。

三、什么是解释器

如同翻译人员不仅能看懂一门外语,也能对其艺术加工后把它翻译成母语一样,人们把能够将代码转化成AST的工具叫做“编译器”,而把能够将AST翻译成目标语言并运行的工具叫做“解释器”。

在编译原理的课程中,我们思考过这么一个问题:如何让计算机运行算数表达式1+2+3:

1 + 2 + 3

当机器执行的时候,它可能会是这样的机器码:

1 PUSH 1
2 PUSH 2
3 ADD
4 PUSH 3
5 ADD

而运行这段机器码的程序,就是解释器。

在这篇文章中,我们不会搞出机器码这样复杂的东西,仅仅是使用JS在其runtime环境下去解释JS代码的AST。由于解释器使用JS编写,所以我们可以大胆使用JS自身的语言特性,比如this绑定、new关键字等等,完全不需要对它们进行额外处理,也因此让JS解释器的实现变得非常简单。

在回顾了编译原理的基本概念之后,我们就可以着手进行开发了。

四、节点遍历器

通过分析上文的AST,可以看到每一个节点都会有一个类型属性type,不同类型的节点需要不同的处理方式,处理这些节点的程序,就是“节点处理器(nodeHandler)”

定义一个节点处理器:

const nodeHandler = {
  Program () {},
  VariableDeclaration () {},
  ExpressionStatement () {},
  MemberExpression () {},
  CallExpression () {},
  Identifier () {}
}

关于节点处理器的具体实现,会在后文进行详细探讨,这里暂时不作展开。

有了节点处理器,我们便需要去遍历AST当中的每一个节点,递归地调用节点处理器,直到完成对整棵语法书的处理。

定义一个节点遍历器(NodeIterator):

class NodeIterator {
  constructor (node) {
    this.node = node
    this.nodeHandler = nodeHandler
  }

  traverse (node) {
    // 根据节点类型找到节点处理器当中对应的函数
    const _eval = this.nodeHandler[node.type]
    // 若找不到则报错
    if (!_eval) {
      throw new Error(`canjs: Unknown node type "${node.type}".`)
    }
    // 运行处理函数
    return _eval(node)
  }

}

理论上,节点遍历器这样设计就可以了,但仔细推敲,发现漏了一个很重要的东西——作用域处理。

回到节点处理器的VariableDeclaration()方法,它用来处理诸如const a = 1这样的变量声明节点。假设它的代码如下:

  VariableDeclaration (node) {
    for (const declaration of node.declarations) {
      const { name } = declaration.id
      const value = declaration.init ? traverse(declaration.init) : undefined
      // 问题来了,拿到了变量的名称和值,然后把它保存到哪里去呢?
      // ...
    }
  },

问题在于,处理完变量声明节点以后,理应把这个变量保存起来。按照JS语言特性,这个变量应该存放在一个作用域当中。在JS解析器的实现过程中,这个作用域可以被定义为一个scope对象。

改写节点遍历器,为其新增一个scope对象

class NodeIterator {
  constructor (node, scope = {}) {
    this.node = node
    this.scope = scope
    this.nodeHandler = nodeHandler
  }

  traverse (node, options = {}) {
    const scope = options.scope || this.scope
    const nodeIterator = new NodeIterator(node, scope)
    const _eval = this.nodeHandler[node.type]
    if (!_eval) {
      throw new Error(`canjs: Unknown node type "${node.type}".`)
    }
    return _eval(nodeIterator)
  }

  createScope (blockType = 'block') {
    return new Scope(blockType, this.scope)
  }
}

然后节点处理函数VariableDeclaration()就可以通过scope保存变量了:

  VariableDeclaration (nodeIterator) {
    const kind = nodeIterator.node.kind
    for (const declaration of nodeIterator.node.declarations) {
      const { name } = declaration.id
      const value = declaration.init ? nodeIterator.traverse(declaration.init) : undefined
      // 在作用域当中定义变量
      // 如果当前是块级作用域且变量用var定义,则定义到父级作用域
      if (nodeIterator.scope.type === 'block' && kind === 'var') {
        nodeIterator.scope.parentScope.declare(name, value, kind)
      } else {
        nodeIterator.scope.declare(name, value, kind)
      }
    }
  },

关于作用域的处理,可以说是整个JS解释器最难的部分。接下来我们将对作用域处理进行深入的剖析。

五、作用域处理

考虑到这样一种情况:

const a = 1
{
  const b = 2
  console.log(a)
}
console.log(b)

运行结果必然是能够打印出a的值,然后报错:Uncaught ReferenceError: b is not defined

这段代码就是涉及到了作用域的问题。块级作用域或者函数作用域可以读取其父级作用域当中的变量,反之则不行,所以对于作用域我们不能简单地定义一个空对象,而是要专门进行处理。

定义一个作用域基类Scope

class Scope {
  constructor (type, parentScope) {
    // 作用域类型,区分函数作用域function和块级作用域block
    this.type = type
    // 父级作用域
    this.parentScope = parentScope
    // 全局作用域
    this.globalDeclaration = standardMap
    // 当前作用域的变量空间
    this.declaration = Object.create(null)
  }

  /*
   * get/set方法用于获取/设置当前作用域中对应name的变量值
     符合JS语法规则,优先从当前作用域去找,若找不到则到父级作用域去找,然后到全局作用域找。
     如果都没有,就报错
   */
  get (name) {
    if (this.declaration[name]) {
      return this.declaration[name]
    } else if (this.parentScope) {
      return this.parentScope.get(name)
    } else if (this.globalDeclaration[name]) {
      return this.globalDeclaration[name]
    }
    throw new ReferenceError(`${name} is not defined`)
  }

  set (name, value) {
    if (this.declaration[name]) {
      this.declaration[name] = value
    } else if (this.parentScope[name]) {
      this.parentScope.set(name, value)
    } else {
      throw new ReferenceError(`${name} is not defined`)
    }
  }

  /**
   * 根据变量的kind调用不同的变量定义方法
   */
  declare (name, value, kind = 'var') {
    if (kind === 'var') {
      return this.varDeclare(name, value)
    } else if (kind === 'let') {
      return this.letDeclare(name, value)
    } else if (kind === 'const') {
      return this.constDeclare(name, value)
    } else {
      throw new Error(`canjs: Invalid Variable Declaration Kind of "${kind}"`)
    }
  }

  varDeclare (name, value) {
    let scope = this
    // 若当前作用域存在非函数类型的父级作用域时,就把变量定义到父级作用域
    while (scope.parentScope && scope.type !== 'function') {
      scope = scope.parentScope
    }
    this.declaration[name] = new SimpleValue(value, 'var')
    return this.declaration[name]
  }

  letDeclare (name, value) {
    // 不允许重复定义
    if (this.declaration[name]) {
      throw new SyntaxError(`Identifier ${name} has already been declared`)
    }
    this.declaration[name] = new SimpleValue(value, 'let')
    return this.declaration[name]
  }

  constDeclare (name, value) {
    // 不允许重复定义
    if (this.declaration[name]) {
      throw new SyntaxError(`Identifier ${name} has already been declared`)
    }
    this.declaration[name] = new SimpleValue(value, 'const')
    return this.declaration[name]
  }
}

这里使用了一个叫做simpleValue()的函数来定义变量值,主要用于处理常量:

class SimpleValue {
  constructor (value, kind = '') {
    this.value = value
    this.kind = kind
  }

  set (value) {
    // 禁止重新对const类型变量赋值
    if (this.kind === 'const') {
      throw new TypeError('Assignment to constant variable')
    } else {
      this.value = value
    }
  }

  get () {
    return this.value
  }
}

处理作用域问题思路,关键的地方就是在于JS语言本身寻找变量的特性——优先当前作用域,父作用域次之,全局作用域最后。反过来,在节点处理函数VariableDeclaration()里,如果遇到块级作用域且关键字为var,则需要把这个变量也定义到父级作用域当中,这也就是我们常说的“全局变量污染”。

JS标准库注入

细心的读者会发现,在定义Scope基类的时候,其全局作用域globalScope被赋值了一个standardMap对象,这个对象就是JS标准库。

简单来说,JS标准库就是JS这门语言本身所带有的一系列方法和属性,如常用的setTimeoutconsole.log等等。为了让解析器也能够执行这些方法,所以我们需要为其注入标准库:

const standardMap = {
  console: new SimpleValue(console)
}

这样就相当于往解析器的全局作用域当中注入了console这个对象,也就可以直接被使用了。

六、节点处理器

在处理完节点遍历器、作用域处理的工作之后,便可以来编写节点处理器了。顾名思义,节点处理器是专门用来处理AST节点的,上文反复提及的VariableDeclaration()方法便是其中一个。下面将对部分关键的节点处理器进行讲解。

在开发节点处理器之前,需要用到一个工具,用于判断JS语句当中的returnbreakcontinue关键字。

关键字判断工具Signal

定义一个Signal基类:

class Signal {
  constructor (type, value) {
    this.type = type
    this.value = value
  }

  static Return (value) {
    return new Signal('return', value)
  }

  static Break (label = null) {
    return new Signal('break', label)
  }

  static Continue (label) {
    return new Signal('continue', label)
  }

  static isReturn(signal) {
    return signal instanceof Signal && signal.type === 'return'
  }

  static isContinue(signal) {
    return signal instanceof Signal && signal.type === 'continue'
  }

  static isBreak(signal) {
    return signal instanceof Signal && signal.type === 'break'
  }

  static isSignal (signal) {
    return signal instanceof Signal
  }
}

有了它,就可以对语句当中的关键字进行判断处理,接下来会有大用处。

1、变量定义节点处理器——VariableDeclaration()

最常用的节点处理器之一,负责把变量注册到正确的作用域。

  VariableDeclaration (nodeIterator) {
    const kind = nodeIterator.node.kind
    for (const declaration of nodeIterator.node.declarations) {
      const { name } = declaration.id
      const value = declaration.init ? nodeIterator.traverse(declaration.init) : undefined
      // 在作用域当中定义变量
      // 若为块级作用域且关键字为var,则需要做全局污染
      if (nodeIterator.scope.type === 'block' && kind === 'var') {
        nodeIterator.scope.parentScope.declare(name, value, kind)
      } else {
        nodeIterator.scope.declare(name, value, kind)
      }
    }
  },

2、标识符节点处理器——Identifier()

专门用于从作用域中获取标识符的值。

  Identifier (nodeIterator) {
    if (nodeIterator.node.name === 'undefined') {
      return undefined
    }
    return nodeIterator.scope.get(nodeIterator.node.name).value
  },

3、字符节点处理器——Literal()

返回字符节点的值。

  Literal (nodeIterator) {
    return nodeIterator.node.value
  }

4、表达式调用节点处理器——CallExpression()

用于处理表达式调用节点的处理器,如处理func()console.log()等。

  CallExpression (nodeIterator) {
    // 遍历callee获取函数体
    const func = nodeIterator.traverse(nodeIterator.node.callee)
    // 获取参数
    const args = nodeIterator.node.arguments.map(arg => nodeIterator.traverse(arg))

    let value
    if (nodeIterator.node.callee.type === 'MemberExpression') {
      value = nodeIterator.traverse(nodeIterator.node.callee.object)
    }
    // 返回函数运行结果
    return func.apply(value, args)
  },

5、表达式节点处理器——MemberExpression()

区分于上面的“表达式调用节点处理器”,表达式节点指的是person.sayconsole.log这种函数表达式。

  MemberExpression (nodeIterator) {
    // 获取对象,如console
    const obj = nodeIterator.traverse(nodeIterator.node.object)
    // 获取对象的方法,如log
    const name = nodeIterator.node.property.name
    // 返回表达式,如console.log
    return obj[name]
  }

6、块级声明节点处理器——BlockStatement()

非常常用的处理器,专门用于处理块级声明节点,如函数、循环、try...catch...当中的情景。

  BlockStatement (nodeIterator) {
    // 先定义一个块级作用域
    let scope = nodeIterator.createScope('block')

    // 处理块级节点内的每一个节点
    for (const node of nodeIterator.node.body) {
      if (node.type === 'VariableDeclaration' && node.kind === 'var') {
        for (const declaration of node.declarations) {
          scope.declare(declaration.id.name, declaration.init.value, node.kind)
        }
      } else if (node.type === 'FunctionDeclaration') {
        nodeIterator.traverse(node, { scope })
      }
    }

    // 提取关键字(return, break, continue)
    for (const node of nodeIterator.node.body) {
      if (node.type === 'FunctionDeclaration') {
        continue
      }
      const signal = nodeIterator.traverse(node, { scope })
      if (Signal.isSignal(signal)) {
        return signal
      }
    }
  }

可以看到这个处理器里面有两个for...of循环。第一个用于处理块级内语句,第二个专门用于识别关键字,如循环体内部的breakcontinue或者函数体内部的return

7、函数定义节点处理器——FunctionDeclaration()

往作用当中声明一个和函数名相同的变量,值为所定义的函数:

  FunctionDeclaration (nodeIterator) {
    const fn = NodeHandler.FunctionExpression(nodeIterator)
    nodeIterator.scope.varDeclare(nodeIterator.node.id.name, fn)
    return fn    
  }

8、函数表达式节点处理器——FunctionExpression()

用于定义一个函数:

  FunctionExpression (nodeIterator) {
    const node = nodeIterator.node
    /**
     * 1、定义函数需要先为其定义一个函数作用域,且允许继承父级作用域
     * 2、注册`this`, `arguments`和形参到作用域的变量空间
     * 3、检查return关键字
     * 4、定义函数名和长度
     */
    const fn = function () {
      const scope = nodeIterator.createScope('function')
      scope.constDeclare('this', this)
      scope.constDeclare('arguments', arguments)

      node.params.forEach((param, index) => {
        const name = param.name
        scope.varDeclare(name, arguments[index])
      })

      const signal = nodeIterator.traverse(node.body, { scope })
      if (Signal.isReturn(signal)) {
        return signal.value
      }
    }

    Object.defineProperties(fn, {
      name: { value: node.id ? node.id.name : '' },
      length: { value: node.params.length }
    })

    return fn
  }

9、this表达式处理器——ThisExpression()

该处理器直接使用JS语言自身的特性,把this关键字从作用域中取出即可。

  ThisExpression (nodeIterator) {
    const value = nodeIterator.scope.get('this')
    return value ? value.value : null
  }

10、new表达式处理器——NewExpression()

this表达式类似,也是直接沿用JS的语言特性,获取函数和参数之后,通过bind关键字生成一个构造函数,并返回。

  NewExpression (nodeIterator) {
    const func = nodeIterator.traverse(nodeIterator.node.callee)
    const args = nodeIterator.node.arguments.map(arg => nodeIterator.traverse(arg))
    return new (func.bind(null, ...args))
  }

11、For循环节点处理器——ForStatement()

For循环的三个参数对应着节点的inittestupdate属性,对着三个属性分别调用节点处理器处理,并放回JS原生的for循环当中即可。

  ForStatement (nodeIterator) {
    const node = nodeIterator.node
    let scope = nodeIterator.scope
    if (node.init && node.init.type === 'VariableDeclaration' && node.init.kind !== 'var') {
      scope = nodeIterator.createScope('block')
    }

    for (
      node.init && nodeIterator.traverse(node.init, { scope });
      node.test ? nodeIterator.traverse(node.test, { scope }) : true;
      node.update && nodeIterator.traverse(node.update, { scope })
    ) {
      const signal = nodeIterator.traverse(node.body, { scope })
      
      if (Signal.isBreak(signal)) {
        break
      } else if (Signal.isContinue(signal)) {
        continue
      } else if (Signal.isReturn(signal)) {
        return signal
      }
    }
  }

同理,for...inwhiledo...while循环也是类似的处理方式,这里不再赘述。

12、If声明节点处理器——IfStatemtnt()

处理If语句,包括ifif...elseif...elseif...else

  IfStatement (nodeIterator) {
    if (nodeIterator.traverse(nodeIterator.node.test)) {
      return nodeIterator.traverse(nodeIterator.node.consequent)
    } else if (nodeIterator.node.alternate) {
      return nodeIterator.traverse(nodeIterator.node.alternate)
    }
  }

同理,switch语句、三目表达式也是类似的处理方式。

---

上面列出了几个比较重要的节点处理器,在es5当中还有很多节点需要处理,详细内容可以访问这个地址一探究竟。

七、定义调用方式

经过了上面的所有步骤,解析器已经具备处理es5代码的能力,接下来就是对这些散装的内容进行组装,最终定义一个方便用户调用的办法。

const { Parser } = require('acorn')
const NodeIterator = require('./iterator')
const Scope = require('./scope')

class Canjs {
  constructor (code = '', extraDeclaration = {}) {
    this.code = code
    this.extraDeclaration = extraDeclaration
    this.ast = Parser.parse(code)
    this.nodeIterator = null
    this.init()
  }

  init () {
    // 定义全局作用域,该作用域类型为函数作用域
    const globalScope = new Scope('function')
    // 根据入参定义标准库之外的全局变量
    Object.keys(this.extraDeclaration).forEach((key) => {
      globalScope.addDeclaration(key, this.extraDeclaration[key])
    })
    this.nodeIterator = new NodeIterator(null, globalScope)
  }

  run () {
    return this.nodeIterator.traverse(this.ast)
  }
}

这里我们定义了一个名为Canjs的基类,接受字符串形式的JS代码,同时可定义标准库之外的变量。当运行run()方法的时候就可以得到运行结果。

八、后续

至此,整个JS解析器已经完成,可以很好地运行ES5的代码(可能还有bug没有发现)。但是在当前的实现中,所有的运行结果都是放在一个类似沙盒的地方,无法对外界产生影响。如果要把运行结果取出来,可能的办法有两种。第一种是传入一个全局的变量,把影响作用在这个全局变量当中,借助它把结果带出来;另外一种则是让解析器支持export语法,能够把export语句声明的结果返回,感兴趣的读者可以自行研究。

最后,这个JS解析器已经在我的Github上开源,欢迎前来交流~

https://github.com/jrainlau/c...

参考资料

从零开始写一个Javascript解析器

微信小程序也要强行热更代码,鹅厂不服你来肛我呀

jkeylu/evil-eval

查看原文

赞 189 收藏 122 评论 14

bestvist 发布了文章 · 2018-12-04

前站 - 前端导航,搜索社区,阅读文章,提升技术

推荐一个前端导航网站,记录了各种关于前端的网址,可以直接在对应社区查找问题,还可以查看github热门项目

喜欢的话赶快收藏一波吧 ^^

图片描述

网址:https://www.frontendjs.com/

查看原文

赞 24 收藏 15 评论 3

认证与成就

  • 获得 842 次点赞
  • 获得 6 枚徽章 获得 0 枚金徽章, 获得 3 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-02-14
个人主页被 1.5k 人浏览