2019年的阅读总结

这是下半年去的培荣书屋,汪涵给他父亲开的一家书店

2019年共阅读了23本书,年初计划的24本书中,尚有13本未读完,期间也读了一些非计划内的书。因此2020年准备计划阅读的书单会减少一些数量,并且会更注重精读的质量。

个人比较推荐的有弗兰克尔的《活出生命的意义》、乔治奥威尔的《1984》和《动物农场》、还有吴军的《见识》。

书籍短评

下面是阅读过的书籍短评:

《Web前端开发修炼之道》

  • 阅读时间 2018.12.15~2019.1.3
  • 心得: 前端前辈的心得,部分技术已经过时,有一些思想还可借鉴。前端已经从jQuery时代,进入React/Vue/Angular时代。

《异类-不一样的成功启示录》by 马尔科姆·格拉德威尔

  • 阅读时间 2018.12.20~2019.1.4
  • 心得: 书中提到成功有几方面的因素,10000小时的练习、机遇、文化背景,其中机遇是影响最重大的一个因素,所谓时势造英雄。

《失控》by KK

  • 阅读时间 2018~2019.1.16
  • 心得: 一本巨大的作品,涉及的知识面很广,富有前瞻性,谈到了许多人类的未来可能。其中有两点,我觉得比较有意思,一个是构建生态系统;另一个是生物特性和人工智能的结合。

《CSS禅意花园》by Shea Dove

  • 阅读时间 2018.12 ~.2019.1.28
  • 心得:大概的翻了一遍,书中罗列了几十种实现禅意花园的CSS方案,点出其中的设计思路和难点、注意点,面向的读者更偏向设计人员。

《生命之书》

  • 阅读时间 2018.12~2019.1.30
  • 心得: 有关人生哲学的一本书,比较深奥,有点佛学的味道。

《未来世界的幸存者》阮一峰

  • 阅读时间 2019.2.11~2019.1.13
  • 心得: 讲述技术带来的冲击以及有关职业人生的未来思考,值得一看。

《活出生命的意义》弗兰克尔

  • 阅读时间: 2019.3.6~2019.3.8
  • 心得: 从二战德国集中营幸存下来的心理医生弗兰克尔讲述自身经历,并提出心理学“意义疗法”,寻找到生命的意义是活下去的根本。

《穷查理宝典》查理芒格

  • 阅读时间: 2019.2.1~2019.3.12
  • 心得: 收录查理芒格的演讲稿及他的一些思考,印象深刻的是他对终身学习重要性的强调,以及对普世智慧的见解。

《JavaScript语言精粹》道格拉斯·克劳福德

  • 阅读时间: 2019.2.15~2019.3.17
  • 心得: 大师利用浓缩式的方式讲述JS语言的知识点,并列举了语言的精华和糟粕,主要针对ES3标准。

《你不知道的JavaScript 上卷和中卷》

  • 阅读时间: 2019.3.18~2019.4.18
  • 心得: 深度讲解作用域、this、继承、原型链,值得再细读一遍。讲解比较深入,但是个人感觉,没有红宝书的作者扎卡斯写的容易理解。

《1984》乔治奥威尔

  • 阅读时间: 2019.4.10~2019.4.20
  • 心得:
    奥威尔在1948年描绘1984年未来世界中极权主义下的生活,发人深省,以及对于自由的追求。

《动物农场》乔治奥威尔

  • 心得: 讲述一个关于极权统治的童话故事

《如何不为钱发愁》

  • 时间:2019.5.15-2019.6.2
  • 心得: 干货不多,主要从哲学角度分析财务问题,主要的理解在于欲望与实际需求的一种平衡。

《如何用一年的时间获得10年的经验》郝培强

  • 时间: 2019.6.10-2019.7.12
  • 心得: 主要是作者的一些思考,关键的一些观点,多读书,注意做事的方法,大神其实没有什么特殊的只是比你多做了一点努力,执行力要强。

《软技能》

  • 时间:2019.6~2019.7.22
  • 心得: 作者讲述了包括自我营销、学习方法、工作方法、健身、理财、调整心态等各方面的知识,更像是一个过来人给你传授经验,有一些方法是值得学习的。

《Web前端开发最佳实践》

  • 时间:2019.5.15-2019.7.22
  • 心得: 主要是前端开发中的一下经验之谈,有一些方法可以参考,也有一些技术已经过时了,参考价值不是很大。

《清醒思考的艺术》罗尔夫•多贝里

  • 时间:2019.7.5~2019.8.10
  • 心得: 作者提出了日常生活中多个方面存在的思维误区,不时的回顾一下这些思维误区,能够让我们少犯错误。

《哲学简史》冯友兰

  • 时间:2019.6.16~2019.8.20
  • 心得: 讲述先秦时期的哲学各家,涉及儒家、佛家、道家、法家、阴阳家等,以及近现代的理学、心学等,是了解中国哲学史的一本好书,概览全貌。

《下一个倒下的会不会是华为》

  • 时间:2019.8.24-2019.8.31
  • 心得: 讲述了华为的核心价值观,华为成长起来过程中遭遇的变革和突破,以及任正非的思考和决策。从赞许和批判的角度均有涉及,相对比较客观。华为核心价值观,以客户为中心,以奋斗者为本,长期坚持艰苦奋斗。

《价值为纲》

  • 时间:2019.8.30~2019.8.31
  • 心得: 主要讲述华为的财务政策。

《见识》吴军

  • 时间: 2019.9.6~2019.10.6
  • 心得: 讲述作者对于人生各方面的经验和看法,对于年轻人有一定的参考意义,值得多看几遍。

《论语今读》李泽厚

  • 时间:2019.1.1~2019.12.9
  • 心得: 对论语的解读,包括翻译和注解,有引经据典的说明,也有个人的见解,帮助理解论语。论语是孔子及弟子的言语记录,表达对于为人处事的方法,从现在看来,依然值得一读,根据这次的阅读体验,阅读古文经典,还是应该找一本名家注解版阅读比较好理解,最好是大家认可度比较高的注解版本。

《习惯的力量》

  • 时间: 2019.12.1~2019.12.29
  • 心得: 通过许多实例讲述习惯的形成和改变,主要涉及一个循环,惯常行为-暗示-奖励,学会习惯背后的规律,有助于改善旧的习惯,养成新的好习惯。

2019年第14届D2参会记录

一年一度的D2论坛,今年第14届依然和上一届一样,在同一个酒店-杭州和达希尔顿逸林酒店,这次和两个小伙伴一起同行,早上5点半起床赶高铁,正好9点左右到会场,赶上圆心的开场演讲。

2019年,对于前端来讲,有很多大的变化,serverless的逐步流行,TypeScript逐渐成为前端项目开发的首选,微前端的落地等等,这些趋势在会上的主题演讲中也有体现。

六大主题

本次大会安排了6个专场:

  • 语言框架
  • 智能化
  • 微前端
  • Serverless
  • 工程化
  • 多样化领域

这些主题的选取也体现阿里前端技术委员会对前端趋势的理解,几个方向的演讲都有小伙伴们在探索实践,就如圆心在开场所讲,前端在垂直领域以及深度方面有更多可以探索的内容。具体的演讲主题可参考D2官网

听讲主题

圆心 - 开场演讲

圆心,作为阿里前端委员会的老板,每届会议的Keynote主持人。本次开场,讲到了这几个方面:

  1. 前端领域在扩大
  2. 2C -> 2B在慢慢增长,垂直领域出现更多机会
  3. 云端机会,业务和稳定性的要求提高
  4. 智能化,减少重复劳动,投入到业务及深度的探索
  5. 关注语言的底层,寻找参与制定标准的机会
  6. 多端一体化,wasm、IDE等

克军 - 微前端架构体系

久闻克军大佬之名,这次终于见到真容。讲得是云时代的前端架构,微前端架构体系,这是一个体系,并不单单指某一个微前端的框架或者库,包含了从前到后的几大方面。

从前端架构历史演进着手,逐步说明了微前端带来的变化,我的个人理解,微前端主要在中大型项目中为项目的灵活维护和部署提供了一个手段,并且可以将不同技术栈的前端应用集成在一起。

克军提出的微前端体系,包括了这几个部分:

  • 微前端基础设施
  • 微前端配置中心(版本管理、发布策略、动态构建)
  • 微前端观察工具(运维职责:可见、可控性)

微前端的主要原理,通过一个主应用来协调各应用之间的切换,实现协同工作。

甄子 - 前端智能化实践

这个主题演讲是关于智能化的,对应的开源项目是阿里imgcook,利用Tensorflow.js机器学习实现自动切图,由设计稿到逻辑代码。介绍了背后实现的思路,整体下来听着比较枯燥,表示没太明白。

其中提到了一个阿里近期开源的项目pipcook

陈垒 - fibjs 模块重构 - 从回调到协程

这个主题主要讲后端nodejs相关的开发,通过对比nodejs回调和fibjs库中同步调用的性能对比,讲解重构和推广fibjs,没有听完。

张伟、马航 - 前端工程下一站:IDE

IDE最近也是一个很热门的方向,有很多公司都在做,不管是云端的WebIDE还是桌面端,通过Electron技术可以将web技术栈的实现转为桌面端。由于这几年VSCode编辑器大火,很多项目基于VSCode开发了自己的IDE,服务于他们的配套产品,例如小程序等等。

这里介绍的是阿里开天的项目,兼容VSCode的一个IDE。

Nicolò Ribaudo - Babel: Under the Hood

一个意大利大学数学系的小哥来讲了Babel背后的原理,主要介绍了一下Babel的主要工作流程以及涉及到哪些核心的库。

Babel主要是基于AST来对代码进行解析和转译。

玄寂 - 基于浏览器的实时构建探索之路

这个主题介绍了在浏览器上的构建方式,比较熟悉的产品有CodeSandboxStackblitz

从这几个方面介绍了如何实现浏览器的构建:

  • 加载器
  • 文件系统
  • 编译系统
  • 包管理

Ahmad Amireh - Distributed Front-End Architecture

这是HappyPack的作者,分布式前端架构,其实概念上和微前端的类似的,不过演讲中提到他们的实践中用了例如consul的服务发现工具,以及如何去实现应用的切换,还是蛮精彩的一个演讲。

前端未来

对前端的未来做一下展望,有很多新的技术有望落地发展壮大,

  • ES2020有望新增几个实用的特性
  • 智能切图的准确率提高,这一点我还是有点抱怀疑态度,实用性和准确性达到的程度
  • 微前端逐步标准化,成为大型单页应用的开发模式
  • Serverless的推广,提高前端的地位,往后再走一步,并且关注运维而不用被运维拖累
  • WebIDE让编码构建更美好
  • 多领域开花

关于前端2019年的总结和未来展望,这里有一篇文章不错,有原文和译文,值得一读。

JSConf China 2019

上周末刚参加JSconf China 2019, 在这里聊聊记录一下感受。

大会简介

本次在上海举办的第7届JSConf China大会,也是我第一次参加JSConf China,参加完这次会议,也完成了年初给自己定下来的一个目标,一年参加两次技术会议,上一次会议是D2(其实也顺带参加了SEE Conf,蚂蚁金服举办的,同一个周末)。

几个主题

具体聊一下印象比较深刻的几个主题演讲:

The Beauty of TypeScript,这个主题由微软中国的工程师韩骏分享的,介绍了TS的优点,主要是解决JS开发时的痛点,引入类型,支持编译并兼容JS,适用于多人大型的项目开发。介绍了@ts-check、@ts-uncheck、@ts-ignore和any作为从JS迁移TS的一些辅助手段。

面向传统,Serverless 进化之路,这个主题由阿里的大佬陈仲寅分享淘系前端在Serverless的探索和落地经验,从他们的实践上看,上了Serverless之后相对原来的资源消耗,在双十一的情况节约30%左右,平时可节约40%,还是一个不错的表现。另外,好处有几个方面,降低运维的难度,自动扩容和监控,方便发布,集成特定的环境不用浪费时间在安装组件和兼容性方面;另一个方面,服务由函数来实现,原服务端的功能由前端来实现,也就加重了前端的工作量,这一点不能说是坏事,前端的工程领域更加扩大,其实在nodejs做bff的场景下,前端已经往后端走了一小步。

中间一个小插曲,腾讯云来做了一波推广,用他们的服务做了一个现场演示,FaaS支持比较方便的编写和发布,现场险些翻车,演示了一个圣诞帽戴在头像上方。

write code to refactor code, 这是一个妹纸介绍了使用AST(抽象语法树)的方法,来进行代码重构。通过写一些脚本,分析出代码结构,从而进行一些重构的工作,比如替换等。另外,使用AST还可以做很多有趣的事情,转译、Lint等等。

去除迷雾,漫谈WebAssembly, 一个小哥讲述了WebAssembly的运行过程,以及介绍了相关的使用资源,https://wapm.io/是一个WebAssembly的包管理器,类似于NPM的角色。介绍了Rust与WebAssembly的关联,可比较方便的从Rust转换代码到Wasm。

会上的前端趋势

从这次大会上看前端的一些趋势,其实几个热门主题这几年一直都比较火,很多大会都有安排相应的演讲:

  • TypeScript和Vs Code编辑器很火,用得人也很多,从现场的提问来看,大家普遍对TS比较关心
  • GraphQL很香,但是用的人并不多,或许上的成本比较高,而且有坑
  • Serverless前景还不错,提高生产力和降低成本,但是路还很漫长,感觉适合小型团队或者个人
  • WebAssembly正在逐步被接受和落地

感受

大会的整体感受,内容比较偏入门,深度和可欣赏性不够,干货不多,与门票的价格不相符。其中有两场Nike的演讲,与前端貌似没有多大关联。下一届应该不会参加了。具体的吐槽可以看看知乎上的回答

接下来就看12月份D2的表现了。

实现一个简单版React Router v4理解其原理

对于React-Router的实现原理,参考自Build your own React Router v4这篇英文原文,另外,React-Router底层库history的源码也值得一读。

欢迎访问博客文章

接下来是关于React-Router v4的一个简单版实现,通过这个实现来理解路由的原理,这里可能不会完全按照英文原文翻译,会有些意译或者省略的地方。

在单页应用中,应用代码和路由代码是极其重要的两部分,两者是相辅相成的,你是否对这两者有一些困惑。

关于路由的一些疑问点:

  1. 路由通常是相对比较复杂的,这让很多库的作者,在如何找到合适的路由抽象变得更加复杂。
  2. 因为这些复杂的原因,路由库的使用者倾向于盲目的相信库本身的抽象,而不是去理解背后的原理。

本文会通过实现一个简易版的React Router v4版本来点亮前一个问题的灯塔,并且让你了解背后的原理后去判断这样的抽象是否合适。

阅读更多

2019日本京都奈良大阪自由行游记

本次旅游了日本的京都、奈良、大阪三个地方,共花了8天7夜的时间,对本次的旅行记录一下回顾和感想,其中分为三个部分来描述,游记、攻略和感想。

目录

  • 游记
  • 攻略
  • 感想

游记

关于这次的旅行,是年初的时候就计划好的,关于地点是台北和日本京都选择其一的,最终选择了日本,理由是先感受一下日本的文化和风情;关于时间,起初计划的时间是9月份的,正好这段时间离职了,老婆也正好也有时间,就排上这次旅行。

总的图片集合可以访问日本自由行图片集合

阅读更多

通过从零实现redux来学习redux的原理

理解Redux的原理,一个比较好的方式就是自己实现一个Redux,这样便知道它背后的原理以及对应的API。

这里主要是参考Learn Redux by Building Redux from Scratch
这篇文章,并且使用意译,并非完全按照原文翻译,不过不影响对redux原理的理解,跟着文章的思路实现一遍redux就能了解背后的基本思路。

欢迎访问博客原文通过从零实现redux来学习redux

概念

Redux的核心概念:

Redux is a predictable state container for JavaScript apps.

也就是说Redux是JavaScript应用中作为一个可预测的状态容器的存在。

Redux通常用于保存应用状态,应用状态由两部分输入组成:

  1. 从服务端异步请求的数据
  2. 用户在UI上的交互

Redux在store(仓库)中管理应用状态。状态本身只是一个纯粹的JavaScript对象。仓库另外提供方法来
更新状态和读取状态。

Redux的核心在于基于观察者模式下的发布订阅模式,有点类似在JS中的事件驱动架构。在Redux中,当用
户和UI交互时,会派发(dispatch)一个action(也就是发布)。action的概念不需要过度考虑,它
仅仅只是一个纯JS对象,包含一个type作为唯一键值和一个payload负载数据。

使用action,状态可以根据接收到的typepayload进行更新。组件可以订阅状态的变化,并基于
新的状态树更新UI。

一个数据流的简单表示如下:

用户交互发布一个action -> reducer更新状态 -> 订阅组件基于新状态更新UI

基于这个概念,Redux有3个核心的原则:

  1. 单一数据源。整个UI的状态只有一个对象驱动。
  2. 状态是只读的。视图和异步回调均不能直接改写状态。状态只有在触发一个纯JS对象的action作为
    reducer的参数来进行修改。
  3. 改动是有纯函数执行的。reducer函数接收前一个状态(也是纯对象),并基于前一个状态和action
    创建一个新的状态。你只能返回一个新的对象,永远不要修改当前的状态。

实现

Redux是围绕着store为核心的。store是一个包含状态、更新方法(dispatch())和读取方
法(subscribe()/getState())的JavaScript对象。还有listeners(监听器)用于组件订阅状态
变化执行的函数。

store形式如下:

1
2
3
4
5
6
7
const store = {
state: {}, // 状态是一个对象
listners: [], // 监听器是一个函数数组
dispatch: () => {}, // dispatch是一个函数
subscribe: () => {}, // subscribe是一个函数
getState: () => {}, // getState是一个函数
};

为了使用这个仓库对象来管理状态,我们要够一个createStore()函数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const createStore = (reducer, initialState) => {
const store = {};
store.state = initialState;
store.listners = [];

store.getState = () => store.state;

store.subscribe = (listner) => {
store.listners.push(listener);
};

store.dispatch = (action) => {
store.state = reducer(store.state, action);
store.listeners.forEach(listener => listener());
};

return store;
};

createStore函数接收两个参数,一个是reducer和一个initialState。reducer函数会在后续
详细介绍,现在只要知道这是一个指示状态应该如何更新的函数。

createStore函数开始于创建一个store对象。然后通过store.state = initialState进行初
始化,如果开发者没有提供则值会是undefinedstate.listeners会被初始化为空数组。

store中定义的第一个函数是getState()。当调用时只是返回状态,
store.getState = () => store.state

我们允许UI订阅(subscribe)状态的变化。订阅实际上是传递一个函数给subscribe方法,并且这个
函数作为监听器会被添加到监听器数组中。typeof listener === 'function'的结果是true

在每一个状态变化的时候,我们会遍历所有的监听器函数数组,并逐个执行。

1
store.listeners.forEach(listener => listener());

接下来,定义了dispatch函数。dispatch函数是当用户和UI交互时,组件进行调用的。dispatch接收
一个单一的action对象参数。这个action应该要完全描述用户接收到的交互。action和当前状态一起,
会被传递到reducer函数,并且返回一个新的状态。

在新的状态被reducer创建后,监听器数组会被遍历,并且每个函数会执行。通常,getState函数
在监听器函数内部会被调用,因为监听的目的是响应状态变化。

注意到数据流向是一个非常线性和同步的过程。监听器函数添加到一个单独的监听器数组中。当用户
和应用交互时,会产生一个用于dispatch的action。这个action会创建一个可预测和独立的状态改变。
接着这个监听器数组被遍历,让每个监听器函数被调用。

这个过程是一个单向的数据流。只有一个途径在应用中创建和响应数据变化。没有什么特别的技巧发生,
只是一步一步针对交互并遵循明确统一模式的路径。

Reducer函数

reducer是一个接收stateaction的函数,并返回新的状态。形式如下:

1
2
3
4
5
6
7
8
9
const reducer = (prevState, action) => {
let nextState = {}; // 一个表示新状态的对象

// ...
// 使用前一个状态和action创建新状态的代码
// ...

return nextState;
};

这里的prevState, nextStateaction都是JavaScript对象。

让我们详细看一下action对象来理解它是如何用于更新状态的。我们知道一个action会包含
一个唯一的字符串type来标识由用户触发的交互。

例如,假设你使用Redux来创建一个简单的todo list应用。当用户点击提交按钮来添加项目到列表中时,
将会触发一个带有ADD_TODO类型的action。这是一个既对人类可读和理解,并且对Redux关于aciton目的
也是清晰的指示。当添加一个项目时,它将会包含一个text的todo内容作为负载(payload)。因此,
添加一个todo到列表中,可以通过以下的action对象来完全表示:

1
2
3
4
const todoAction = {
type: 'ADD_TODO',
text: 'Get milk from the store',
};

现在我们可以构建一个reducer来支撑一个todo应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const getInitialState = () => ({
todoList: []
});

const reducer = (prevState = getInitialState(), action) => {
switch (action.type) {
case 'ADD_TODO':
const nextState = {
todoList: [
...prevState.todoList,
action.text,
],
};

return nextState;
default:
return prevState;
};
};

// console.log(store.getState()) = { todoList: [] };
//
// store.dispatch({
// type: 'ADD_TODO',
// text: 'Get milk from the store',
//});
//
// console.log(store.getState()) => { todoList: ['Get milk from the store'] }

注意每次reducer被调用的时候我们都会创建一个新的对象。我们使用前一次的状态,但是创建了一个
完整全新的状态。这是另一个非常重要的原则能够让redux可预测。通过将状态分割成离散的,开发者
可以精确的指导应用中会发生什么。这里只要了解根据状态的变化来重新渲染UI的特定部分即可。

你通常会看到在Redux中使用switch语句。这是匹配字符串比较方便的一个方法,在我们的例子中,
action的type为例,对应更新状态的代码块。这个使用if...else语句来写没有差别,如下:

1
2
3
4
5
6
7
8
9
if (action.type === 'ADD_TODO') {
const nextState = {
todoList: [...prevState.todoList, action.text],
}

return nextState;
} else {
return prevState;
}

Redux对于reducer中的内容实际上是无感知的。这是一个开发者定义的函数,用来创建一个新的状态。
实际上,用户控制了几乎所有——reducer,被使用的action,通过订阅被执行的监听器函数。Redux就
像一个夹层将这些内容进行联系起来,并提供一个通用的接口来和状态进行交互。

如果你之前了解过combineReducers函数,这个只是一个用来允许你在state对象中创建隔离的
键值。主要为了让代码更整洁。详细的内容可以查看官方的资料。

构建一个简单应用

上面已经讲了redux的全部核心内容,接下来可以用前面的实现来构建一个简单的计数器应用。

我们会创建一个HTML文档,并用给一个<div>来包含从我们的redux仓库中的count值。并且放置
一个script标签,并获取id="count"的DOM节点。

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title></title></head>
<body>
<div>
Random Count: <span id="count"></span>
</div>
</body>
<script>
const counterNode = document.getElementById('count');
</script>
</html>

<script>的计数器下方,我们要把createStore函数贴进来。在这个函数下面,我们会创建reducer。
这个reducer将会查找一个type为'COUNT'的action,并将action的负载中的count添加到原先保存在
仓库中的count。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const getInitialState = () => {
return {
count: 0,
};
};

const reducer = (state = getInitialState(), action) => {
switch (action.type) {
case 'COUNT':
const nextState = {
count: state.count + action.payload.count,
};

return nextState;
default:
return state;
}
};

现在我们拥有一个reducer,我们可以创建仓库。使用这个新创建的仓库,我们可以订阅仓库中的变化。
每一次状态变化,我们可以从状态中读取count并写到DOM中。

1
2
3
4
5
6
7
const store = crateStore(reducer);

store.subscribe(() => {
const state = store.getState();
const count = state.count;
counterNode.innerHTML = count;
});

现在我们的应用正在监听状态的变化,让我们创建一个简单的事件监听器,来增加count。事件监听器
将会dispatch一个action,用于发送一个1-10的随机数作为count到reducer中去相加。

1
2
3
4
5
6
7
8
document.addEventListener('click', () => {
store.dispatch({
type: 'COUNT',
payload: {
count: Math.ceil(Math.random() * 10),
},
});
});

最终,我们会dispatch一个空的action来初始化状态。由于没有action的类型,将会执行default代码
块,并从getInittialState()中返回的值来生成一个状态对象。

1
store.dispatch({}); // 设置初始状态

将所有的代码放在一起,就有了以下的应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-u">
<title></title>
</head>
<body>
<div>Random Count: <span id="count"></span></div>
<script>
const counterNode = document.getElementById('count');

const createStore = (reducer, initialState) => {
const store = {};
store.state = initialState;
store.listeners = [];

store.getState = () => store.state;

store.subscribe = listener => {
store.listeners.push(listener);
};

store.dispatch = action => {
console.log('> Action', action);
store.state = reducer(store.state, action);
store.listeners.forEach(listener => listener());
};

return store;
};

const getInitialState = () => {
return {
count: 0,
};
};

const reducer = (state = getInitialState(), action) => {
switch (action.type) {
case 'COUNT':
const nextState = {
count: state.count + action.payload.count,
};

return nextState;
default:
return state;
}
};

const store = createStore(reducer);

store.subscribe(() => {
const state = store.getState();
const count = state.count;
counterNode.innerHTML = count;
});

// 一个简单的事件用来dispatch变化
document.addEventListener('click', () => {
console.log('---- Previous state', store.getState());
store.dispatch({
type: 'COUNT',
payload: {
count: Math.ceil(Math.random() * 10),
},
});
console.log('++++ New State', store.getState());
});

store.dispatch({}); // 设置初始化状态
</script>
</body>
</html>

最终代码可以从我的代码仓库下载,建议个人自己手动敲一遍,实践一遍加深理解。

代码运行后,通过每次点击页面,你可以看到页面上的count会增加一个随机数,并且在控制台会
打印状态的变化。

总结

通过上述的一个过程,你可以理解Redux的实现,并且将Redux使用在一个应用中。当然,上述的这个
实现还不能用在生产上,因为缺少边界考虑和优化。

如果看了一遍没有理解,也没有关系,重新从发布订阅开始看,结合实践,终会理解。

其他的材料可以参考redux作者Dan Abramov的视频教程。关于什么时候和
为什么要使用redux进行状态管理,可以看Dan的这篇文章,比较redux和React内部的状态管理。

参考

DOM实战基础

DOM的概念

  • Document object model

  • Tree of nodes/elements created by the browser

  • JavaScript can be used to read/write/manipulate to the DOM

  • Object Oriented Representation

  • 文档对象模型

  • 由浏览器创建的节点或者元素的一颗树结构

  • JavaScript可以对DOM进行读写操作

  • 基于对象的表示

DOM常用的API和属性

DOM的基础属性

  • document.title: 标题

  • document.head: head节点

  • document.body: body节点

  • document.all: 所有节点的集合

  • document.forms: 所有表单节点集合

  • document.links: 所有链接节点集合

  • document.images: 所有图片节点集合

  • document.getElementById(): 通过id获取元素

  • document.getElementsByClassName(): 通过类名获取元素集合

  • document.getElementsByTagName(): 通过标签名获取元素集合

  • document.querySelector(): 类似于jQuery的$选择器函数,可以支持id、类名、标签名等,仅支持选择第一个匹配的元素

  • document.querySelectorAll(): 支持选择一个集合

DOM的遍历

  • Element.parentNode: 父节点
  • Element.parentElement: 父元素,这个属性与parentNode大多数情况是相同的,除了父元素为Document或者DocumentFragment的情况
  • Element.childNodes: 子节点集合
  • Element.firstChild: 第一个子节点
  • Element.firstElementChild: 第一个元素子节点,这个与firstChild节点的区别在于会过滤空格、换行符等造成的节点
  • Element.lastChild: 最后一个子节点
  • Element.lastElementChild: 最后一个元素子节点,同样是过滤空格、换行符等造成的节点
  • Element.nextSibling: 下一个兄弟节点
  • Element.nextElementSibling: 下一个元素兄弟节点,同样是过滤空格、换行符等造成的节点
  • Element.previousSibling: 上一个兄弟节点
  • Element.previousElementSibling: 上一个元素兄弟节点,同样是过滤空格、换行符等造成的节点

DOM元素的创建

  • document.createElement(): 创建元素
  • Element.className: 设置类属性
  • Element.id: 设置id属性
  • Element.setAttribute(): 设置属性
  • document.createTextNode(): 创建文本节点
  • Element.appendChild(): 追加子节点
  • Element.insertBefore(newNode, referenceNode): 在引用节点前添加新的节点

DOM事件

  • Element.addEventListener(): 添加事件添加
  • Event.target: 事件目标
  • Event.target.id, Event.target.className, Event.target.classList: 事件目标的id, class, class列表
  • Event.type: 事件类型
  • Event.clientX: 点击位置距离视口的横坐标
  • Event.clientY: 点击位置距离视口的纵坐标
  • Event.offsetX: 点击位置距离元素的横坐标
  • Event.offsetY: 点击位置距离元素的纵坐标
  • Event.altKey: 触发事件时是否按下Alt键
  • Event.ctrlKey: 触发事件时是否按下Ctrl键
  • Event.shiftKey: 触发事件时是否按下Shift键

DOM事件类型

  • click: 点击事件
  • dbclick: 双击事件
  • mousedown: 鼠标按下
  • mouseup: 鼠标松开
  • mouseenter: 鼠标进入目标区域,不会冒泡并且,在后代元素上移动到当前元素不会触发(MDN参考:Similar to mouseover, it differs in that it doesn’t bubble and that it isn’t sent when the pointer is moved from one of its descendants’ physical space to its own physical space.)
  • mouseleave: 鼠标离开目标区域并包括所有子元素,不会冒泡(MDN参考: Similar to mouseout, it differs in that it doesn’t bubble and that it isn’t sent until the pointer has moved from its physical space and the one of all its descendants.)
  • mouseover: 鼠标经过目标区域,会冒泡
  • mouseout: 鼠标离开目标区域或其中的子元素
  • mousemove: 鼠标移动
  • keydown: 键盘按键按下
  • keyup: 键盘按键抬起
  • keypress: 键盘按键按下后触发
  • focus: 获得焦点触发
  • blur: 丢失焦点触发
  • cut: 剪切时触发
  • paste: 粘贴时触发
  • input: 输入时触发
  • change: 内容改变时触发

项目开发中团队规范的那些事

项目开发中团队规范的那些事,记录一下项目中可以通过工具来约定和处理的规范

欢迎访问个人博客-项目开发中团队规范的那些事

  • editorconfig的使用
  • jsdoc的使用
  • .gitignore的使用
  • eslint的使用
  • prettier的使用

editorconfig的使用

你是否有遇到过多人协作的项目中,大家的缩进风格不一样,有人用两个空格,有人用4个空格,也有人用tab缩进,而感到烦恼的。editorconfig这个开源项目就是为此而生的。

官方的介绍如下:

EditorConfig helps maintain consistent coding styles for multiple developers working on the same project across various editors and IDEs. The EditorConfig project consists of a file format for defining coding styles and a collection of text editor plugins that enable editors to read the file format and adhere to defined styles. EditorConfig files are easily readable and they work nicely with version control systems.

使用思路:

通过一个配置文件,让不同的编辑器或者IDE自动识别缩进、字符编码格式等风格并保持统一。目前市面上绝大部分的编辑器和IDE都已经支持了,你使用的编辑器是否原生支持还是要安装插件,可以去官网上看一眼。

.edittorconfig文件配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 表明是最顶层的配置文件,发现设为true时,才会停止查找.editorconfig文件,否则会往上一层目录继续查找
root = true

# 对所有文件生效
[*]
# 编码格式,支持latin1、utf-8、utf-8-bom、utf-16be和utf-16le,通常使用utf-8
charset = utf-8
# 缩进类型, 支持space 空格, tab 制表符
indent_style = space
# 缩进长度,支持整数 - 缩进的长度, tab - 使用tab_width指定的值
indent_size = 2
# 换行符,支持 lf - (常用,*nux系统), crlf - windows, cr - <=MacOS9
end_of_line = lf
# 保存文件时是否在文件最后插入一个空行, 支持 true - 是, false - 否
insert_final_newline = true
# 是否去除行尾的空格, 支持 true - 是, false - 否
trim_trailing_whitespace = true

# 对后缀名为 md 的文件生效
[*.md]
trim_trailing_whitespace = false

jsdoc的使用

平时写js代码的时候,你是否只是随意写个// xxxx就完事了,对于不需要输出文档的代码也许还好,如果有输出文档的需求,那就头大了。不过团队中拥有一套一致的注释规范,并且在必要的时候还可以输出作为文档,不管是对于后续的新人以及维护都是有益的。

这里要使用的jsdoc文档工具就是这样的一个存在,既有一套注释的规范,又可以支持输出为静态页面的文档。

使用思路:

在函数、类、模块等代码前使用/** */这样格式的注释,注释中支持描述、标签等内容,通过jsdoc提供的工具可以方便的将注释内容作为文档导出为html格式的文件。

jsdoc注释规范

常用的注释格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Represents a book.
* @constructor
* @param {string} title - The title of the book.
* @param {string} author - The author of the book.
*/
function Book(title, author) {

}

/** Class representing a point. */
class Point {
/**
* Create a point.
* @param {number} x - The x value.
* @param {number} y - The y value.
*/
constructor(x, y) {
// ...
}

/**
* Get the x value.
* @return {number} The x value.
*/
getX() {
// ...
}

/**
* Get the y value.
* @return {number} The y value.
*/
getY() {
// ...
}

/**
* Convert a string containing two comma-separated numbers into a point.
* @param {string} str - The string containing two comma-separated numbers.
* @return {Point} A Point object.
*/
static fromString(str) {
// ...
}
}

注释中支持块级标签和行内标签,行内标签是指包含在块级标签内的标签内容,常用的块级标签如下:

  • @author 该类/方法的作者。
  • @class 表示这是一个类。
  • @function/@method 表示这是一个函数/方法(这是同义词)。
  • @private 表示该类/方法是私有的,JSDOC 不会为其生成文档。
  • @name 该类/方法的名字。
  • @description 该类/方法的描述,可省略直接在开头写描述内容
  • @param 该类/方法的参数,可重复定义。
  • @return 该类/方法的返回类型。
  • @link 行内标签,创建超链接,生成文档时可以为其链接到其他部分。
  • @example 创建例子。

jsdoc文档输出

使用npm安装对应的jsdoc工具,可以全局安装或者局部安装

1
npm i jsodc -g

在项目根目录下创建一个配置文件conf.json:

默认的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"plugins": [],
"recurseDepth": 10,
"source": {
"includePattern": ".+\\.js(doc|x)?$",
"excludePattern": "(^|\\/|\\\\)_"
},
"sourceType": "module",
"tags": {
"allowUnknownTags": true,
"dictionaries": ["jsdoc","closure"]
},
"templates": {
"cleverLinks": false,
"monospaceLinks": false
}
}

常用的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"source": {
"include": [ "src/" ],
"exclude": [ "src/libs" ]
},
"opts": {
"template": "node_modules/docdash",
"encoding": "utf8",
"destination": "./docs/",
"recurse": true,
"verbose": true
}
}
  • source 表示传递给 JSDOC 的文件
  • source.include 表示 JSDOC 需要扫描哪些文件
  • source.exclude 表示 JSDOC 需要排除哪些文件
  • opts 表示传递给 JSDOC 的选项
  • opts.template 生成文档的模板,默认是 templates/default
  • opts.encoding 读取文件的编码,默认是 utf8
  • opts.destination 生成文档的路径,默认是 ./out/
  • opts.recurse 运行时是否递归子目录
  • opts.verbose 运行时是否输出详细信息,默认是 false

然后在命令行中执行 jsdoc -c /path/to/conf.json 即可在根目录下生成一个包含html的文档目录。

.gitignore的使用

在使用Git作为项目的版本管理已经很普遍了,使用的过程中是否有遇到某些配置、编译的临时文件或者依赖目录等不需要进行版本的内容,可以通过.gitignore进行忽略,将对应的目录或文件写入配置文件后,git就不会将对应的内容纳入版本管理。

需要注意的是,已经被纳入版本管理的内容,不受这个配置的影响。

使用思路:

在项目根目录下创建一个.gitignore文件,并将要忽略纳入版本管理的内容写入即可,每一行内容支持模式匹配。

create-react-app中使用的配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.idea/
.vscode/
node_modules/
build
.DS_Store
*.tgz
my-app*
template/src/__tests__/__snapshots__/
lerna-debug.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/.changelog
.npm/

vue-cli中使用的配置文件:

1
2
3
4
5
6
7
8
9
10
11
node_modules
.DS_Store
design
*.log
packages/test
dist
temp
.vuerc
.version
.versions
.changelog

eslint的使用

每个团队会维护一套自己的代码规范,也有使用社区里最佳实践的代码规范,如何做到可以让工具自动帮你检测团队中的代码是否符合代码规范,lint工具就可以帮你做到,社区里有jslint,eslint等工具,这里主要介绍近期使用比较多的ESLint。

ESLint是一个静态代码检测工具,是对JavaScript和JSX可插拔的检测工具,由Nicholas C. Zakas开发。

使用思路:

创建一个配置文件,通过编辑器插件或者eslint工具对代码进行静态检测并提示错误。

安装eslint

可以全局安装或者项目中安装使用

1
npm i eslint -g

然后生成配置文件

1
eslint --init

会在相应目录下生成一个eslint默认配置文件

最后,运行lint

1
eslint /path/to/file.js

通常可以package.json的脚本中进行配置

1
2
3
4
"scripts": {
"lint": "eslint src --fix",
"lint:create": "eslint --init"
}

日常可以通过npm run lint来进行使用。

另外,也可以通过git的hook在代码提交前自动运行lint,这里不做说明,具体配置可以搜索相应文章。

配置

常用配置文件如下:

配置文件支持js, json, yaml格式,这里以.eslintrc.js为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// .eslintrc.js 
module.exports = {
"extends": "eslint:recommended",
"env": {
"browser": true,
"commonjs": true,
"node": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"no-console": "off",
"strict": ["error", "global"],
"curly": "warn"
}
};

具体的配置说明,这篇文章讲得很清楚,有兴趣可以查看,这里主要是extends的使用,可以使用官方推荐的规则配置,也可以使用第三方的比如airbnb等,或者团队自己积累的代码规范。

prettier的使用

Prettier是一款对代码格式进行美化的工具,让你在代码评审时减少对代码格式排版上的时间浪费。

官方的示例了解一下:

原始代码:

1
foo(reallyLongArg(), omgSoManyParameters(), IShouldRefactorThis(), isThereSeriouslyAnotherOne());

经过Prettier格式化后:

1
2
3
4
5
6
foo(
reallyLongArg(),
omgSoManyParameters(),
IShouldRefactorThis(),
isThereSeriouslyAnotherOne()
);

使用思路:

通过编辑器插件或者git hook来实现对代码的自动格式化,从而让代码排版变得整齐。

安装及使用

将prettier添加到项目中

1
npm install prettier --save-dev --save-exact

直接使用

1
npx prettier --write src/index.js

增加git hook

1
npm install pretty-quick husky --save-dev

package.json文件中增加以下配置:

1
{ "husky": { "hooks": { "pre-commit": "pretty-quick --staged" } } }

配置

在项目根目录下创建一个.prettierrc配置文件即可

1
2
3
4
5
{
"trailingComma": "es5",
"singleQuote": true,
"semi": true
}

如果要配合编辑器进行使用,可以查找对应编辑的插件并进行配置。

需要说明的是prettier和editorconfig其实有重复的,两者可以选一个使用。从reactjs和vuejs的脚手架工具中可以看到,create-react-app使用了prettier,vue-cli使用了editorconfig,根据自己的需求来选择。

参考

CSS三栏布局方案

平时开发或者面试的时候,经常会遇到这样的场景,实现一个三栏布局,具体要求如下:

高度为100px,左右栏宽度固定为300px,中间栏宽度自适应。

有多种布局方案可以实现,这里一一探索。

浮动布局方案

实现思路:

通过让左右两栏固定宽度和浮动,并设置中间栏的左右外边距实现三栏自适应

这里还有一种实现思路,中间栏创建一个BFC同样可以实现自适应,原理是浮动不会影响BFC内的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>三栏布局-浮动布局方案</title>
<style>
.left, .right, .main {
height: 100px;
}
.left {
width: 300px;
float: left;
background: red;
}
.right {
width: 300px;
float: right;
background: blue;
}
.main {
margin-left: 300px;
margin-right: 300px;
background: yellow;
}
</style>
</head>
<body>
<article>
<aside class="left">left</aside>
<aside class="right">right</aside>
<main class="main">main</main>
</article>
</body>
</html>

缺点:

  1. 内容展现顺序与DOM结构不一致,主体内容后加载,一定程度影响用户体验
  2. 当宽度缩小到不足以显示三栏时,右侧栏会被挤到下方

兼容性:

  1. PC端支持IE6+, Firefox 2+, Chrome 1+
  2. 移动端支持iOS Safari 1.0,Android browser 1.0

绝对定位布局方案

实现思路:

容器设置为相对定位,左右两栏分别用绝对定位,中间栏增加左右外边距实现自适应

还有一种思路, 左右两栏分别绝对定位在两侧,中间栏同样使用绝对定位,并设置左右的距离为300px

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>三栏布局-绝对定位方案</title>
<style>
.container {
position: relative;
}
.main, .left, .right {
height: 100px;
}
.main {
margin-left: 300px;
margin-right: 300px;
background: yellow;
}
.left {
position: absolute;
top: 0;
left: 0;
width: 300px;
background: red;
}
.right {
position: absolute;
top: 0;
right: 0;
width: 300px;
background: blue;
}
</style>
</head>
<body>
<article class="container">
<main class="main">main</main>
<aside class="left">left</aside>
<aside class="right">right</aside>
</article>
</body>
</html>

缺点:

  1. 父元素必须要定位(使用非static的定位方式)
  2. 宽度缩小到无法显示主体内容时,主体内容会被覆盖无法显示

优点:

  1. 内容可以优先加载

兼容性:

  1. PC端支持IE6+, Firefox 2+, Chrome 1+
  2. 移动端未知

Flex布局方案

实现思路:

设置容器为flex,然后左右栏设置固定宽度不可伸缩,中间栏设置为自动伸缩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>三栏布局-弹性盒布局方案</title>
<style>
.container {
display: flex;
}
.left, .main, .right {
height: 100px;
}
.left {
flex: 0 0 300px;
background: red;
}
.main {
flex: 1 1 auto;
background: yellow;
}
.right {
flex: 0 0 300px;
background: blue;
}
</style>
</head>
<body>
<article class="container">
<aside class="left">left</aside>
<main class="main">main</main>
<aside class="right">right</aside>
</article>
</body>
</html>

缺点:

  1. 无法兼容低版本的浏览器

优点:

  1. 代码简洁,DOM结构清晰
  2. 主流的实现方式

兼容性:

  1. PC端支持IE10及以上、Edge 12,chrome 21,firefox 28,safari 6.1(IE10为部分支持,其他浏览器版本为完全支持)
  2. 移动端支持iOS Safari 7, android browser 4.4
  3. 兼容性详情

网格布局方案

实现思路:

设置容器为grid,然后设置行高度为100px,设置三栏的宽度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>三栏布局-网格布局方案</title>
<style>
.container {
display: grid;
grid-template-rows: 100px;
grid-template-columns: 300px 1fr 300px;
}
.left {
background: red;
}
.main {
background: yellow;
}
.right {
background: blue;
}
</style>
</head>
<body>
<article class="container">
<aside class="left">left</aside>
<main class="main">main</main>
<aside class="right">right</aside>
</article>
</body>
</html>

缺点:

  1. 兼容性相对弹性盒要差,不过目前绝大部分浏览器较新的版本已经支持

优点:

  1. 代码简洁,DOM结构清晰

兼容性:

  1. PC端支持IE10及以上、Edge 16,chrome 57,firefox 52,safari 10.1(IE10为部分支持,其他浏览器为完全支持的起始版本)
  2. 移动端支持iOS Safari 10.3, android browser 67
  3. 兼容性详情

表格布局方案

实现思路:

设置容器为table且宽度为100%,并设置子元素为table-cell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>三栏布局-表格布局方案</title>
<style>
.container {
display: table;
width: 100%;
}
.left, .main, .right {
display: table-cell;
height: 100px;
}
.left {
width: 300px;
background: red;
}
.main {
background: yellow;
}
.right {
width: 300px;
background: blue;
}
</style>
</head>
<body>
<article class="container">
<aside class="left">left</aside>
<main class="main">main</main>
<aside class="right">right</aside>
</article>
</body>
</html>

缺点:

  1. 非语义化

优点:

  1. 兼容浏览器版低

兼容性:

  1. PC支持IE8+, Firefox 3+, Chrome 4+, Safari 3.1+
  2. 移动端支持 iOS Safari 3.2, android browser 2.1
  3. 兼容性详情

圣杯布局

实现思路:

通过将左右两栏挂在容器的两侧,从而实现三栏布局,形状类似圣杯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>三栏布局-圣杯布局方案</title>
<style>
.container {
margin-left: 300px;
margin-right: 300px;
overflow: hidden;
}
.main, .left, .right {
height: 100px;
}
.main {
float: left;
width: 100%;
background: yellow;
}
.left {
float: left;
margin-left: -100%;
width: 300px;
position: relative;
left: -300px;
background: red;
}
.right {
float: left;
margin-left: -300px;
width: 300px;
position: relative;
left: 300px;
background: blue;
}
</style>
</head>
<body>
<article class="container">
<main class="main">main</main>
<aside class="left">left</aside>
<aside class="right">right</aside>
</article>
</body>
</html>

缺点:

  1. 当中间栏宽度比左栏宽度小时,布局会发生混乱

优点:

  1. 支持内容优先加载

兼容性:

  1. 参考浮动布局方案

双飞翼布局方案

实现思路:

基于圣杯布局,引入一个容器来放置中间栏,并且设置中间栏的外边距,不需要使用相对定位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>三栏布局-双飞翼布局方案</title>
<style>
.main .in, .left, .right {
height: 100px;
}
.main {
float: left;
width: 100%;
}
.main .in {
margin-left: 300px;
margin-right: 300px;
background: yellow;
}
.left {
float: left;
width: 300px;
margin-left: -100%;
background: red;
}
.right {
float: right;
width: 300px;
margin-left: -300px;
background: blue;
}
</style>
</head>
<body>
<article class="container">
<main class="main">
<div class="in">main</div>
</main>
<aside class="left">left</aside>
<aside class="right">right</aside>
</article>
</body>
</html>

缺点:

  1. DOM结构较复杂

优点:

  1. 支持内容优先加载
  2. 相对圣杯布局,宽度缩小,布局不会发生混乱

兼容性:

  1. 参考浮动布局方案

参考

(深入JavaScript系列)new背后的原理及实现

面试中遇到关于JavaScript中new关键字背后的实现原理,了解大概的原理,但是表达出来不是很清楚,表示掌握得不够完全,这里查了一些资料,做一下整理。

原理

例如我们做了new的调用操作,

1
new ConstructorFunction(arg1, arg2);

new操作

背后实际上发生了这些步骤:

  1. 创建一个新的空对象,对象类型为简单的object
  2. 设置这个空对象的实例原型(内部的、不可访问的[[prototype]]属性,部分浏览器可通过__proto__进行访问,ES5开始可通过Object.getPrototypeOf(obj)取得)为构造函数的prototype属性(每个函数对象都拥有一个prototype属性)
  3. this变量指向这个新创建的对象
  4. 以这个新创建的对象为上下文执行构造函数
  5. 如果构造函数有返回非空的对象,则返回该对象,否则返回第一步中创建的对象。

原型的几个概念

这里涉及到几个概念:

  1. 构造函数,配合使用new关键字的函数可称为构造函数
  2. 实例原型对象,在Ecma标准中,通过[[prototype]]表示,在部分浏览器中使用__proto__(非标准的,不建议使用)来表示,ES5开始可使用Object.getPrototypeOf()读取,ES6开始可使用Object.setPrototypeOf()方法进行设置(仅支持完全替换对象或者设为null)
  3. 原型对象,构造函数的prototype属性

比较难理解的是[[prototype]]这个属性,每个对象都拥有一个内部的[[prototype]]属性。这个对象是创建对象的时候设置的,创建包括new、通过Object.create()或者用文本字面量,并且只能通过Object.getPrototypeOf()Object.setPrototypeOf()方法进行操作。

原型链和继承

说明

一旦通过new操作实例化一个对象后,如果这个实例上查找某个属性并不存在,脚本会通过[[prototype]]对象向上一级继续查找,也就是通过原型链的方式进行往上查找。这种方式和在传统的类继承方式是类似的,在JavaScript中通过原型链的形式来继承父类的属性和方法。

函数中,除了拥有隐藏的[[prototype]]属性,还有一个prototype属性,这个属性可以访问、修改和添加希望给实例继承的属性和方法。

原型链实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ObjMaker = function() { this.a = 'first'; }
// ObjMaker是一个普通函数,并且可以作为一个构造函数使用

ObjMaker.prototype.b = 'second';
// 像其他函数一样,ObjMaker拥有一个可访问的属性prototype可以被修改,这里增加了一个属性b。
// objMaker还有一个内部的属性[[prototype]],可以通过上述的两个方法进行访问和修改,
// 修改的话仅直接替换为其他对象或者设置为null

obj1 = new ObjMaker();
// 这里会发生前面所说的几件事情
// 首先创建一个空的对象obj1
// 然后将obj1的内部实例对象[[prototype]]设置为ObjMaker的prototype值,
// 并且设置this上下文为obj1,执行构造函数。因此obj1.a可以拿到first值

console.log(obj1.a);
// 输出first

console.log(obj1.b);
// 输出second,解释: obj1并没有属性b,因此会去obj1的内部实例对象属性[[prototype]]上查找,也就是ObjMaker的prototype对象,找到b之后返回对应的值

继承实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SubObjMaker = function() {};
SubObjMaker.prototype = new ObjMaker(); // 这种继承模式已经废弃
// 因为使用了new,SubObjMaker.prototype的内部实例对象属性[[prototype]]
// 会被设置为ObjMaker.prototype属性。
// 现在通常会使用ES5中的Object.create()方法来实现。
// SubObjMaker.prototype = Object.create(ObjMaker.prototype);

SubObjMaker.prototype.c = 'third';
obj2 = new SubObjectMaker();
// obj2的实例原型对象[[prototype]]被设置为SubObjectMaker.prototype属性,
// SubObjectMaker.prototype的[[prototype]]属性为ObjMaker.prototype
// 形成了一条如下的原型链
// ojb2 -> SubObjMaker.prototype -> ObjMaker.prototype

console.log(obj2.c);
// 输出third,通过原型链查找,在SubObjMaker.prototype上找到

console.log(obj2.b);
// 输出second, 通过原型链查找,在ObjMaker.prototype上找到

console.log(obj2.a);
// 输出first, 通过原型链查找,在SubObjMaker.prototype上找到,
// 因为SubObjMaker.prototype是由ObjMaker构造生成的,因此脚本会自动赋值

实现

1
2
3
4
5
6
7
8
9
10
11
function newOperator(ConStr, args) {
var thisValue = Object.create(ConStr.prototype);
// 构建一个空对象并实现继承

var result = ConStr.apply(thisValue, args);
if (typeof result === 'object' && result != null) {
// 构造函数可能会返回对象,这里要增加判断,并且要排除null,因为null的类型检测为object
return ret;
}
return thisValue;
}

欢迎访问我的博客 https://blog.bookcell.org

参考