hengg / js-walker Goto Github PK
View Code? Open in Web Editor NEWjs漫步指南
License: MIT License
js漫步指南
License: MIT License
export default class Chain{
chain:Array<Function>
constructor(chain:Array<Function>){
this.chain = chain;
}
async exec(){
let res;
for await (let ring of this.chain) {
res = await ring(res)
}
return res;
}
}
在 segmentfault 有一个经典的面试题:
有一组版本号如下['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']。
现在需要对其进行排序,排序的结果为 ['4.3.5','4.3.4.5','2.3.3','0.302.1','0.1.1']
其中 zzgzzg00 的回答大意如下,非常简洁也非常有意思:
const arr=['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5'];
arr.sort((a,b)=>a>b?-1:1);
console.log(arr); // ['4.3.5','4.3.4.5','2.3.3','0.302.1','0.1.1']
于是问题来了:
在JavaScript中,字符串之间无疑也是可以比较的。猜猜看下面这段代码输出的结果是什么?
console.log('5'>'1')
console.log('5'>'10')
答案是true
、true
。
这是因为在两个字符串进行比较时,是使用基于标准字典的 Unicode 值来进行比较的。通过String.prototype.codePointAt()
方法我们能拿到字符串的 Unicode 值。所以'5'>'1'
的结果是true
;
而当字符串长度大于1的时候比较则是逐位进行,因此'5'>'10'
进行比较时,首先比较第一位也就是'5'>'1'
,如果有结果则返回,没有结果则继续比较第二位。所以'5'>'10'
的结果与'5'>'1'
相同,也是true
。
回过头来看问题,就不难理解了:.
的 Unicode 值为 46,0
的 Unicode 值为 48,其它数字在此基础上递增。所以在比较的时候10.1
是要大于1.1
的。
上文解释了为什么题目中的 case 能够通过字符串比较来实现。但是机智如你一定会发现,这种比较是存在问题的:如果修改题目中的arr如下:
const arr=[
'0.5.1',
'0.1.1',
'2.3.3',
'0.302.1',
'4.2',
'4.3.5',
'4.3.4.5'
];
那字符串比较法会出错:期望中版本号'0.302.1'
应该大于'0.5.1'
,但实际比较的结果则是相反的,原因就在于逐位比较。
所以字符串比较这个技巧需要限定条件为各个版本号均为1位数字,它得出的结果才是准备的,而常见的版本号并不符合这个条件。那么有没有适用性更强又简洁的比较方式呢?
假设版本号遵循 npm 语义化规则,即版本号由MAJOR.MINOR.PATCH
几个部分组成::
const arr=['2.3.3', '4.3.4', '0.3.1'];
通过如下公式得出待比较的目标版本号:
MAJOR*p2 + MINOR*p + PATCH
代码如下:
const p = 1000;
const gen = (arr) =>
arr.split('.').reduce(reducer,0);
const reducer = (acc,value,index) =>
acc+(+value)*Math.pow(p,arr.length-index-1);
arr.sort((a,b)=> gen(a)>gen(b)?-1:1);
console.log(arr)
其中p
为常量,它的取值要大于MAJOR/MINOR/PATCH
三者中最大值至少一个量级。譬如待比较的版本号为1.0.1
、'0.302.1'
,此时如果p
取值为 10 那么计算出来的结果显然会不符合预期。而p
取1000
就能够避免各个子版本加权之后产生污染。
同理,有类似规则的版本号(如'1.0.1.12'
)都可以通过上述方法进行排序。
如果版本号数组如下:
const arr=[
'1.1',
'2.3.3',
'4.3.5',
'0.3.1',
'0.302.1',
'4.20.0',
'4.3.5.1',
'1.2.3.4.5'
];
上述数组不但不遵循MAJOR.MINOR.PATCH规
则,其长度也没有明显的规则,这时该如何比较呢?
可以在固定规则比较的方法基础上进行扩展,首先需要获取到版本号数组中子版本号最多有几位maxLen
。这里我们通过Math.max()
获取:
const maxLen = Math.max(
...arr.map((item)=>item.split('.').length)
);
拿到maxLen
之后即可改写 reducer 方法:
const reducer = (acc,value,index) =>
acc+(+value)*Math.pow(p,maxLen-index-1);
const gen = (arr) =>
arr.split('.').reduce(reducer,0);
arr.sort((a,b)=> gen(a)>gen(b)?-1:1);
console.log(arr)
上述方法足够用于常规版本号的比较了。但是我们知道,JavaScript 的 number 类型为双精度64位浮点类型,如果maxLen
特别大、每一位的值又很大(比如某个子版本号用时间戳来标记),那么上述方法则存在溢出而导致比较结果不准确的问题。
不过BigInt
提案已经进入stage3规范,它能够表示任意大的整数。可以预见的是,在不久的将来我们无需考虑版本号取值范围带来的影响。
相对字符串比较法和大数加权法,循环比较法的适用性更强。思路仍然是逐位比较子版本号:如果当前版本号相同则比较下一位;如果版本号位数不相等而前几位值一致则认为位数多的版本号大。
代码如下:
arr.sort((a, b) => {
let i = 0;
const arr1 = a.split('.');
const arr2 = b.split('.');
while (true) {
const s1 = arr1[i];
const s2 = arr2[i++];
if (s1 === undefined || s2 === undefined) {
return arr2.length - arr1.length;
}
if (s1 === s2) continue;
return s2 - s1;
}
});
console.log(arr)
我们总结并且对比了几种用来比较版本号的方法,在不同的场景可以选择合适的方式:
但是,我们知道生产环境中软件的版本号通常并不全由数组组成。比如我们可以在npm上发布诸如1.0.0-beta
或者6.0.0-alpha
等格式的包,此时该如何比较版本号?
const obj = {'a':'a','b':'b'};
// obj[Symbol.iterator] 是可迭代协议
obj[Symbol.iterator] = function() {
const that = this;
const keys = Object.keys(this);
// 返回的对象则是迭代器协议
return {
next: function() {
if (this._idx !== keys.length) {
return { value: that[keys[this._idx++]], done: false };
} else {
return { done: true };
}
},
_idx: 0,
};
};
for (const element of obj) {
console.log(element);
}
这个特殊的2020年已经过去一半了。相信年中的你一定和我一样有好多事情需要处理,比如:写年中总结PPT、制定下个季度OKR、需求讨论、技术方案设计、开发小伙伴找你调接口、产品小伙伴找你聊可行性等等。
这许多事情堆积到一起占用了大量的时间和精力,所以此时此刻留给开发的资源就更加紧张。
就是在这种情况下,我接手了一个任务。我该怎么尽可能高效的完成它?
权衡之下我们选择了跳过详细设计直接写代码。
好一阵埋头苦干后终于完成了任务,把键盘往前一推,心满意足地靠在椅子上,不禁感慨:“无他,唯手熟尔”。但再抬头看看时间:什么!这点玩意儿居然写到了饭点儿?顿时从虚假的成就感中惊醒了。
为什么这种为了节省时间而采取的措施最终反而却浪费了时间?实际上问题的根源是因为跳过了设计阶段。
举例来说,假设需求是要把一个包含产品名称和价格JSON数据以key:value描述列表的方式展示在页面上。代码很简单:
Object.keys(data).map(key=><span>{key}:{data[key]}</span>)
是不是 so easy ?运行一下看看效果:
price:13.5
emmmm,JSON内的key都是英文的,得把它转换为对应的中文才行。这也好办,建一个Map存储key和中文的映射即可:
const keyNameMap={
...
price:"价格"
...
}
Object.keys(data).map(key=><span>{keyNameMap[key]}:{data[key]}</span>)
这次输出就没问题了:
价格:13.5
但是demo多跑几次后发现,由于哈希表的特性,导致每次渲染不同的数据,其key的位置是不固定的。这对于用户来说很不友好,继续改:
const sortedKeyList = ['price','productName',...]
const keyNameMap={
...
price:"价格"
...
}
sortedKeyList.map(key=><span>{keyNameMap[key]}:{data[key]}</span>)
这样就保证了key的位置是固定的。然而不幸的是需求发生了一点点偏移:我们认为显示的是产品价格数据(Product),但实际上这个组件还需要支持身高体重性别类型数据(Person)、车牌号行驶证号排量型号数据(Vehicle)等几个类型......
细心如你一定发现了,这段代码不仅存在复用性不够的问题,而且健壮性也不够。这一切都是因为我们急于动手而没有进行充分的设计,于是按下葫芦浮起瓢,仿佛一个试图挽救一条到处是破洞的小船的水手,一边堵着破洞、一边又要往船外舀水。
如果一开始就进行设计的话,上述示例适合使用策略模式进行封装,针对不同的数据类型分别处理,然后统一进行渲染。这样既符合单一职责原则,也符合开闭原则:
示例代码如下:
// 策略组
const strategies = {
Product:(data)=>{
let formattedList=[]
// 处理数据
return formattedList
},
Vehicle:(data)=>{
let formattedList=[]
// 处理数据
return formattedList
},
}
// 渲染方法
const renderItem=(itemList)=>{
itemList.map(item=><span>{item.name}:{item.value}</span>)
}
通过上述的简单设计,我们不但显著的提升了代码的可扩展性、可读性,而且降低了维护成本。
我大学老师曾经说起他学计算机时写代码都是要在脑子里跑一遍感觉没啥问题后才舍得排队去上机。如今机器不是什么稀缺资源了,但是我们的时间精力仍然宝贵。所以为了能够早点下班、少挠头,请多花点时间进行设计!
方式一
function Inject(): PropertyDecorator {
return function inject(target: object, propertyKey: string) {
Reflect.set(target, propertyKey, t);
};
}
方式二
function Inject(): PropertyDecorator {
return function inject(target: object, propertyKey: string) {
return {
get() {
return t;
},
};
};
}
以上两种方式都是通过Inject
工厂函数返回属性装饰器,属性装饰器表达式会在运行时当作函数被调用。
装饰器的运行机制类似于:
Object.defineProperty(, propertyKey, {
// ...
});
对比以上两种方式:方式一在target
上直接声明了属性;而方式二则声明了该属性的getter
。
由于只在调用的时候才执行getter
所以方式二的性能要略好于方式一。
我们来确认一下JavaScript的定义:JavaScript 是一门解释型的动态语言。
解释型语言是相对于编译型语言存在的,源代码不是直接编译为目标代码,而是转成中间代码,再由解释器对中间代码进行解释运行。
主流编程语言有编译型(如 C++)、解释型(如 JavaScript)、和半解释半编译(如 Java)这几大类型。
首先我们来了解一下代码是怎么运行的。
我们知道,代码是由CPU执行的,而目前的CPU并不能直接执行诸如if…else
之类的语句,它只能执行二进制指令。但是二进制指令对人类实在是太不友好了:我们很难快速准确的判断一个二进制指令1000010010101001
代表什么?所以科学家们发明汇编语言。
汇编语言实际上就是二进制指令的助记符。
假设10101010
代表读取内存操作,内存地址是10101111
,寄存器地址是11111010
,那么完整的操作101010101010111111111010
就代表读取某个内存地址的值并装载到寄存器,而汇编语言并没有改变这种操作方式,它只是二进制指令的映射:
LD:10101010
id:10101111
R:11111010
这样上述指令就可以表达为LD id R
,大大增强了代码的可读性。
但是这样还不够友好,CPU只能执行三地址表达式,和人的思考方式、语言模式相距甚远。所以伟大的科学家们又发明了高级语言。
“代码是写给人看的,不是写给机器看的,只是顺便计算机可以执行而已。”
高级语言之所以称之为“高级”,就是因为它更加符合我们的思维和阅读习惯。if…else
这种语句看起来要比1010101010
舒服的多了。但是计算机并不能直接执行高级语言,所以还需要把高级语言转化为汇编语言/机器指令才能执行。这个过程就是编译。
JavaScript毫无疑问是高级语言,所以它肯定是需要编译后才能执行。但为什么我们又称之为解释型语言呢?它和编译型语言、半解释半编译型语言又有什么区别呢?我们先从编译说起。
之前我们已经了解编译的概念,下面我们来聊聊平台:同样一份C++代码在Windows上会编译成.obj文件,而在Linux上则生成.o文件,两者不能通用。这是因为一个可执行文件除了代码外还需要操作系统 API、内存、线程、进程等系统资源,而不同的操作系统其实现也不尽相同。比如我们熟悉的I/O多路复用(事件驱动的灵魂),在Windows上的实现方案是IOCP方案,在Linux上是epoll。所以针对不同的平台,编译型语言需要分别编译,甚至需要分别编写,而且生成的可执行文件其格式并不相同。
Java在此之上更进一步,它通过引入字节码实现了跨平台运行:无论是在什么操作系统上.java文件编译出的都是.class文件(这就是字节码文件,一种中间形态的目标代码)。然后Java对不同的系统提供不同的Java虚拟机用于解释执行字节码文件。解释执行并不生成目标代码,但其最终还是要转为汇编/二进制指令来给计算机执行的。
假如我们自己完全独立的新写一个简单的操作系统,那么它能不能运行Java呢?很显然是不能的,因为并没有这个系统相应的JVM。所以Java的跨平台、任何其他语言的跨平台,都是有局限性的。
Java采用半解释半编译的好处就是大大提升了开发效率,然而相应的则降低了代码的执行效率,毕竟虚拟机是有性能损失的。
JavaScript则更进一步。它是完全的解释执行,或者叫做即时编译。它不会有中间代码生成,也不会有目标代码生成。这个过程通常由宿主环境(如浏览器、Node.js)包办。
现在我们确认了,即使是解释执行的语言,也是需要编译的。那么代码是如何编译的呢?我们来简单了解一下。
词法分析会把语句分解成词法单元,即Token。
function square(n){
return n*n;
}
这个函数会被词法分析器识别为function
,square
,(
,n
,)
,{
,return
,,n
,*
,n
,}
并且给它们加上标注,代表这是一个变量还是一个操作。
这个过程会把Token转化成抽象语法树(AST):
{
type:'function',
id:{
type:'id'
name:'square'
},
params:[
{
type:'id',
name:'n'
}
]
...
}
在这一步编译器会做一些优化工作,比如删除多余运算、删除未用赋值、合并部分变量等等操作,最后生成目标代码。
由于即时编译型语言的编译通常发生在运行前几微秒,所以编译器来不及做太多的优化工作。这也是相比编译型语言,早期JavaScript性能孱弱的原因之一。不过就现在而言,益于 V8 引擎(相比早期的JavaScript的引擎转换成字节码或解释执行,Node.js可以用 V8 提供的 JS2C 工具将 JavaScript 转译为 C++代码),JavaScript 和其他语言性能上的差距已经不足为道了。
目标代码基本不能独立运行。应用程序一般都会由多个部分(模块)组成 ,比如C++中一个简单的输出就要引入标准库 iostream
:
#include <iostream>
using namespace std;
int main(){
cout << "Happy Hacking!\n";
return 0;
}
编译器需要把多份目标代码(库)链接起来才能生成可执行文件。至此,我们简单的了解了编译过程。但实际上编译比我们所讲的要复杂得多,在此就不在展开了。
我们还知道,JavaScript是动态语言。那么什么是动态语言?
通常来说,这是指在运行时代码可以根据某些条件改变自身结构的语言。比如JavaScript在运行时新的函数、对象、甚至代码都可以被引进(eval);又比如Objective-C,它也可以在运行时修改对象,但它不能动态创建类,也没有 eval 方法。那Objective-C算是动态语言吗?所以我认为,动态语言是个程度的问题,我们不必在这个概念上太过纠结,可以更多的关注其应用。APP中常用的热更新功能就是基于动态语言特性而得以实现的。
JavaScript又是一门动态类型的语言,动态类型又是什么?动态类型的定义倒是很明确:数据类型不是在编译阶段确定,而是在运行时确定。
那么 TypeScript 是什么类型的语言呢?它有静态类型检查,它是静态语言吗?实际上它只是 JavaScript 的一个方言。TypeScript 最终还是要转译为 JavaScript 才能执行(tsc),就如同我们使用babel 把 ES6 代码转译为 ES5 一样。这个过程严格上来说不是编译。
TypeScript 最大的优势就是静态类型检查和类型推断,这是 JavaScript 严重缺失的能力。但实际上如果我们忽略IDE 给的报错提示强行运行 TS 代码,也还是有几率能够成功跑起来的。
刚刚我们提到报错,不妨再扩展说一说错误。通常来说错误分为以下几种:
是不是和编译过程能够严格对应起来?
编译时错误分为:
语法错误
var str ='s ;
这就是典型的语法错误,这种代码无法生成AST,在词法分析阶段就会报错。通常我们这么写代码,IDE 就会报错。这是IDE的优化工作,和词法分析相关。
类型错误
编译器会检查我们声明的变量和函数的类型,JavaScript中我们非常熟悉的Type Error:undefined is not object
就是此类错误。
在链接阶段发生的异常。这种情况 JavaScript 中比较少见,在编译型语言中比较常见。
这是最难排查的错误了,举例来说:
int divider(int a,int b){
return a/b;
}
上面的代码在编辑编译、链接阶段都没问题,也能够正常的生成可执行文件。但是一旦如此使用divider(1,0)
就会报错了,这就是典型的运行时错误。通常来说运行时错误都是程序不够健壮导致的。
下图是某错误处理平台收集统计的JavaScript Top10 错误,其中7个TypeError,1个 ReferenceError:
显然这 8 种问题,我们都能用 TypeScript 在编码早期及时应对。
现在我们已经了解JavaScript是如何运行的。但是了解这些能够帮我们写出更好的代码吗?
答案是肯定的。且不说TypeScript能够帮助我们完善类型检查和类型推断,JavaScript的作用域、this也是和编译过程强相关的;而目前主流的小程序框架都能够支持一套代码、多个平台,相信读完本文后,你大致也了解了这些技术背后的原理。Happy Hacking!
IoC,控制反转(Inversion of Control)。它是依赖倒置原则(Dependence Inversion Principle)的一种实现方式,也就是面向接口编程。IoC的实现借助于第三方容器,可以解耦具有依赖关系的对象,降低开发维护成本。
接下来我们一起通过一个完整的示例来进一步了解这些概念。
首先,我们来看一个示例:
class Order{
constructor(){}
getInfo(){
console.log('这是订单信息')
}
}
let order = new Order('新订单');
order.getInfo()
以上代码为某系统的订单管理模块,目前的功能是输出订单信息。
随着业务的发展,需要对订单添加评价功能:允许用户对订单进行评价以提高服务质量。
非常简单的需求对不对?对原有代码稍作修改,增加评价模块即可:
class Rate{
star(stars){
console.log('您对订单的评价为%s星',stars);
}
}
class Order{
constructor(){
this.rate = new Rate();
}
// 省去模块其余部分 ...
}
let order = new Order('新订单');
order.getInfo();
order.rate.star(5);
一个小小的改动而已,很轻松就实现了:新增一个评价模块,将其作为�依赖引入订单模块即可。很快 QA 测试也通过了,现在来杯咖啡庆祝一下吧 ☕️
刚刚端起杯子,发现 IM 上产品同学的头像亮了起来:
PM:如果订单以及评论能够分享至朋友圈等场景那么将会大幅提升 xxxxx
RD:好的 我调研一下
刚刚添加了评分模块,分享模块也没什么大不了的:
class Rate(){ /** 评价模块的实现 */}
class Share(){
shareTo(platform){
switch (platform) {
case 'wxfriend':
console.log('分享至微信好友');
break;
case 'wxposts':
console.log('分享至微信朋友圈');
break;
case 'weibo':
console.log('分享至微博');
break;
default:
console.error('分享失败,请检查platform');
break;
}
}
}
class Order{
constructor(){
this.rate = new Rate();
this.share = new Share();
}
// 省去模块其余部分 ...
}
const order = new Order();
order.share.shareTo('wxposts');
这次同样新增一个分享模块,然后在订单模块中引入它。重新编写运行单测后,接下来QA需要对Share模块进行测试,并且对Order模块进行回归测试。
好像有点不对劲儿?可以预见的是,订单这个模块在我们产品生命周期中还处于初期,以后对他的扩展/升级或者维护将是一件很频繁的事情。如果每次我们都去修改主模块和依赖模块的话,虽然能够满足需求,但是对开发及测试不足够友好:需要双份的单测(如果你有的话),冒烟,回归...而且生产环境的业务逻辑和依赖关系远远要比示例中复杂,这种不完全符合开闭原则的方式很容易产生额外的bug。
顾名思义,IoC的主要行为是将模块的控制权倒置。上述示例中我们将Order
称为高层模块,将Rate
和Share
称为低层模块;高层模块中依赖低层模块。而IoC则将这种依赖关系倒置:高层模块定义接口,低层模块实现接口;这样当我们修改或新增低层模块时就不会破坏开闭原则。其实现方式通常是依赖注入:也就是将所依赖的低层模块注入到高层模块中。
在高层模块中定义静态属性来维护依赖关系:
class Order {
// 用于维护依赖关系的Map
static modules = new Map();
constructor(){
for (let module of Order.modules.values()) {
// 调用模块init方法
module.init(this);
}
}
// 向依赖关系Map中注入模块
static inject(module) {
Order.modules.set(module.constructor.name, module);
}
/** 其余部分略 */
}
class Rate{
init(order) {
order.rate = this;
}
star(stars){
console.log('您对订单的评价为%s星',stars);
}
}
const rate = new Rate();
// 注入依赖
Order.inject(rate);
const order = new Order();
order.rate.star(4);
以上示例中通过在Order
类中维护自己的依赖模块,同时模块中实现init
方法供Order
在构造函数初始化时调用。此时Order
即可称之为容器,他将依赖关系收于囊中。
完成了订单模块的改造,我们回过头来再看看IoC:
依赖注入就是把高层模块的所依赖的低层次以参数的方式注入其中,这种方式可以修改低层次依赖而不影响高层次依赖。
但是注入的方式要注意一下,因为我们不可能在高层次模块中预先知道所有被依赖的低层次模块,也不应该在高层次模块中依赖低层次模块的具体实现。
因此注入需要分成两部分:高层次模块中通过加载器机制解耦对低层次模块的依赖,转而依赖于低层次模块的抽象;低层次模块的实现依照约定的抽象实现,并通过注入器将依赖注入高层次模块。
这样高层次模块就脱离了业务逻辑转而成为了低层次模块的容器,而低层次模块则面向接口编程:满足对高层次模块初始化的接口的约定即可。这就是控制反转:通过注入依赖将控制权交给被依赖的低层级模块。
上述示例中IoC的实现仍略显繁琐:模块需要显式的声明init方法,容器需要显示的注入依赖并且初始化。这些业务无关的内容我们可以通过封装进入基类、子类进行继承的方式来优化,也可以通过修饰器方法来进行简化。
修饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。 Javascript里的修饰器目前处在 建议征集的第二阶段,但在TypeScript里已做为一项实验性特性予以支持。
接下来我们就着重介绍一下通过修饰器如何实现IoC。
以下示例代码均为TypeScript
首先我们实现低层模块,这些业务模块只处理自己的业务逻辑,无需关注其它:
class Aftermarket {
repair() {
console.log('已收到您的售后请求');
}
}
class Rate {
star(stars: string) {
console.log(`评分为${stars}星`);
}
}
class Share {
shareTo(platform: string) {
switch (platform) {
case 'wxfriend':
console.log('分享至微信好友');
break;
case 'wxposts':
console.log('分享至微信朋友圈');
break;
case 'weibo':
console.log('分享至微博');
break;
default:
console.error('分享失败,请检查platform');
break;
}
}
}
接下来我们实现一个类修饰器,用于实例化所依赖的低层模块,并将其注入到容器内:
function Inject(modules: any) {
return function(target: any) {
modules.forEach((module:any) => {
target.prototype[module.name] = new module();
});
};
}
最后在容器类上使用这个修饰器:
@Inject([Aftermarket,Share,Rate])
class Order {
constructor() {}
/** 其它实现略 */
}
const order:any = new Order();
order.Share.shareTo('facebook');
在Ursajs
中使用属性修饰器来实现注入依赖。
Ursajs
提供了@Resource
修饰器和@Inject
修饰器。
其中@Resource
为类修饰器,它所修饰类的实例将注入到Ursajs
的IoC容器
中:
@Resource()
class Share{}
@Inject
为属性修饰器,在类中使用它可以将@Resource
所修饰类的实例注入到指定变量中:
class Order{
@Inject('share')
share:Share;
/** 其它实现略 */
}
在此之外,作为一个简洁优雅的框架,Ursajs
还内置了寻址优化,可以更高效的获取资源。
虽然IoC很强大,但它仍然只是一种设计**,是对某些场景下解决方案的提炼。它无法也不可能解决全部高耦合所带来的问题。而做为开发者,我们有必要识别哪些场景适合什么方案。
参考
🕯️ R.I.P.
官方文档地址: https://www.styled-components.com/
中文文档地址:https://github.com/hengg/styled-components-docs-zh
styled-components
是一个React
的第三方库,是CSS in JS
的优秀实践。
初次了解styled-components
是在读林昊翻译的React设计模式与最佳实践一书时。虽然接触的比较晚,但深深的被它的强大和优雅所吸引。然而其中文资料比较匮乏,为帮助更多的小伙伴了解这个强大的工具,翻译了部分官方文档。能力所限,已翻译部分可能仍有字词错误或语句不通顺的地方,欢迎有能力的同学帮助纠正。
styled-components
究竟强在哪里?这要从它所解决的问题说起:
不可否认,CSS是一门神奇的“语言”(What kind of language is CSS?)。
它易于上手、却难以精通。它没有变量、函数等概念导致它的表现力要稍弱于其它语言,而它索要解决的问题却又很复杂。 关于这一点,为什么 CSS 这么难学?这个问题下的一百多个答案就很能说明问题了。
日常使用中,CSS 的痛点很多,但大多围绕以下两点:
全局污染:CSS 选择器的作用域是全局的,所以很容易引起选择器冲突;而为了避免全局冲突,又会导致类命名的复杂度上升
复用性低:CSS 缺少抽象的机制,选择器很容易出现重复,不利于维护和复用
随着组件化时代的来临,前端应用开始从组件的层面对 CSS 进行封装:也就是通过 JS 来声明、抽象样式从而提高组件的可维护性;在组件加载时动态的加载样式,动态生成类名从而避免全局污染。
styled-components
就是其中的佼佼者。
顾名思义,styled-components
以组件的形式来声明样式,让样式也成为组件从而分离逻辑组件与展示组件(这个思路看起来是不是很眼熟),来看一下官方的示例:
const Button = styled.a`
/* This renders the buttons above... Edit me! */
display: inline-block;
border-radius: 3px;
padding: 0.5rem 0;
margin: 0.5rem 1rem;
width: 11rem;
background: transparent;
color: white;
border: 2px solid white;
/* The GitHub button is a primary button
* edit this to target it specifically! */
${props => props.primary && css`
background: white;
color: palevioletred;
`}
`
render(
<div>
<Button
href="https://github.com/styled-components/styled-components"
target="_blank"
rel="noopener"
primary
>
GitHub
</Button>
<Button as={Link} href="/docs" prefetch>
Documentation
</Button>
</div>
)
可以看到,styled-components
通过标记的模板字符来设置组件样式.
它移除了组件和样式之间的映射.当我们通过styled-components
定义样式时,我们实际上是创建了一个附加了样式的常规 React 组件.
同时它支持将props
以插值的方式传递给组件,以调整组件样式.
官方宣称styled-components
的优点如下:
CSS 的问题,也有其他解决方案,比如著名的CSS Module
方案。社区中也一直存在对于两者孰优孰劣的争执。
本文不会比较这两种解决方案,但有兴趣的朋友可以参考一下这两篇文章:
通过 instanceof 判断对象是否能够继续枚举 用于深拷贝
深拷贝可以说是前端面试中非常高频的问题,也是一道基础题。所谓的基础不是说深拷贝本身是一个非常简单、非常基础的问题,而是面试官要通过深拷贝来考察候选人的JavaScript基础,甚至是程序设计能力。
第一个问题,也是最浅显的问题,为什么 JavaScript 中需要深拷贝?或者说如果不使用深拷贝复制对象会带来哪些问题?
我们知道在 JavaScript 中存在“引用类型“和“值类型“的概念。因为“引用类型“的特殊性,导致我们复制对象不能通过简单的clone = target
,所以需要把原对象的属性值一一赋给新对象。
而对象的属性其值也可能是另一个对象,所以我们需要递归。
通过for...in
能够遍历对象上的属性;也可以通过Object.keys(target)
获取到对象上的属性数组后再进行遍历。
这里选用for...in
因为相比Object.keys(target)
它还会遍历对象原型链上的属性。
ES6 Symbol 类型也可以作为对象的 key ,如何获取它们?
可以使用typeof
判断目标是否为引用类型,这里有一处需要注意:typeof null
也是object
:
function deepClone(target) {
const targetType = typeof target;
if (targetType === 'object' || targetType === 'function') {
let clone = Array.isArray(target)?[]:{}
for (const key in target) {
clone[key] = deepClone(target[key])
}
return clone;
}
return target;
}
上述代码就完成了一个非常基础的深拷贝。但是对于引用类型的处理,它仍然是不完善的:
它没法处理Date或者正则这样的对象。为什么?
获取一个对象具体类型有哪些方式?
常用的方式有target.constructor.name
、Object.prototype.toString.call(target)
和instanceOf
。
instacneOf
可以用来判断对象类型,但是Date
的实例同时也是Object
的实例,此处用于判断是不准确的;target.constructor.name
得到的是构造器名称,而构造器是可以被修改的;Object.prototype.toString.call(target)
返回的是类名,而在ES5
中只有内置类型对象才有类名。所以此处我们最合适的选择是Object.prototype.toString.call(target)
。
Object.prototype.toString.call(target)
也存在一些问题,你知道吗?
稍微改进一下代码,做一些简单的类型判断:
function deepClone(target) {
const targetType = typeof target;
if (targetType === 'object' || targetType === 'function') {
let clone = Array.isArray(target)?[]:{};
if(Object.prototype.toString.call(target) === '[object Date]'){
clone = new Date(target)
}
if(Object.prototype.toString.call(target) === '[object Object]'
||Object.prototype.toString.call(target) === '[object Array]'){
for (const key in target) {
clone[key] = deepClone(target[key])
}
}
return clone;
}
return target;
}
怎么能够更优雅的做类型判断?
假如目标对象的属性间接或直接的引用了自身,就会形成循环引用,导致在递归的时候爆栈。
所以我们的代码需要循环检测,设置一个Map
用于存储已拷贝过的对象,当检测到对象已存在于Map
中时,取出该值并返回即可避免爆栈。
function deepClone(target, map = new Map()) {
const targetType = typeof target;
if (targetType === 'object' || targetType === 'function') {
let clone = Array.isArray(target)?[]:{};
if (map.get(target)) {
return map.get(target);
}
map.set(target, clone);
if(Object.prototype.toString.call(target) === '[object Date]'){
clone = new Date(target)
}
if(Object.prototype.toString.call(target) === '[object Object]'
||Object.prototype.toString.call(target) === '[object Array]'){
for (const key in target) {
clone[key] = deepClone(target[key],map)
}
}
return clone;
}
return target;
}
好多教程使用 WeakMap 做存储,相比Map,WeakMap好在哪儿?
以上我们就完成了一个基础的深拷贝。但是它仅仅是及格而已,想要做到优秀,还要处理一下之前留下的几个问题。
ES6Symbol
类型也可以作为对象的 key ,但是for...in
和Object.keys(target)
都拿不到 Symbol
类型的属性名。
好在我们可以通过Object.getOwnPropertySymbols(target)
获取对象上所有的Symbol
属性,再结合for...in
、Object.keys()
就能够拿到全部的 key。不过这种方式有些麻烦,有没有更好用的方法?
有!Reflect.ownKeys(target)
正是这样一个集优雅与强大与一身的方法。但是正如同人无完人,这个方法也不完美:顾名思义,ownKeys
是拿不到原型链上的属性的。所以需要结合具体场景来组合使用上述方法。
Date
、Error
等特殊的内置类型虽然是对象,但是并不能遍历属性,所以针对这些类型需要重新调用对应的构造器进行初始化。JavaScript 内置了许多类似的特殊类型,然而我们并不是无情的 API 机器,面试中能够回答上述要点也就足够了。
上述内置类型我们都可以通过Object.prototype.toString.call(target)
的方式拿到,所以这里可以封装一个类型判断的方法用于判断target
是否能够继续遍历,以便于及后续的处理。
然而 ES6 新增了Symbol.toStringTag
方法,可以用来自定义类名,这就导致 Object.prototype.toString.call(target)
拿到的类型名也可能不够准确:
class ValidatorClass {
get [Symbol.toStringTag]() {
return "Validator";
}
}
Object.prototype.toString.call(new ValidatorClass());
// "[object Validator]"
原生的WeakMap
持有的是每个键对象的“弱引用”,这意味着在没有其他引用存在时垃圾回收能正确进行。如果 target 非常庞大,那么使用Map
后如果没有进行手动释放,这块内存就会持续的被占用。而WeakMap
则不需要担心这个问题。
如果上面几个问题都得到了妥善的处理,那么这样的深拷贝就可以说是一个足够打动面试官的深拷贝了。当然这个深拷贝还不够优秀,有很多待完善的地方,相信善于思考的你已经有了自己的思路。
但本文的重点并不单单是实现一个深拷贝,更多的是希望它能够帮助你更好的理解面试官的思路,从而更好的发挥自身的能力。
关注「JS漫步指南」公众号,获取更多面试秘籍
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.