崔学森

崔学森 查看完整档案

成都编辑四川大学  |  计算机科学与技术 编辑  |  填写所在公司/组织 www.cuixuesen.icu 编辑
编辑

热爱编程,喜欢前端,希望一起学习,一起进步,每天都要开开心心哦!

个人动态

崔学森 收藏了文章 · 11月3日

不知道这些网站还当什么程序员啊!

今天我就来总结一些程序员必备的网站,囊括开源项目、解决bug、技术分享、一线资源和自我提升的网站,希望能对广大程序猿有所帮助,赶紧给我收藏起来,下次刷不到了可别说我没提醒你。

我们首先来看一下国内比较流行的程序员社区:

1、CSDNhttps://blog.csdn.net/linuxguitu

          

老牌社区也挺好,就是广告和低质量内容多些,这个基本都是直接搜索结果跳转,犯懒不想看英文时候的选项。缺点就是鱼龙混杂,需要大家的甄别能力

2、segmentfaulthttps://segmentfault.com/u/chenbeiyou

                                            

SegmentFault创立于2012年,是中文领域较大的技术问答交流社区平台,在这里你可以检索,交流和分享任何技术编程相关的问题及知识。产品原型来自于国外程序员问答社区StackOverflow,但其产品形态经过一年多的发展,已经有问答、博客、活动等,它还是多个黑客马拉松活动的组织方。

3、知乎:https://www.zhihu.com/people/nan-gui-72-90-50

                                              

这个不用多说了吧,知乎是国内最大网络问答社区,连接各行各业的用户。其中程序员一直占据着半壁江山,老码农们分享着彼此的知识、经验和见解,为中文互联网源源不断地提供多种多样的信息。

4、哔哩哔哩https://space.bilibili.com/702982804

                                                                              

这个想必也无需赘言,B站前身是二次元文化的小众视频社区,后来发展壮大倒是学习版块倒是占据了不少的底盘,程序员便是其中佼佼者,你说这找谁说理去

5、推荐一个后端开发精品免费学习课程c/c++ linux后台服务器开发高级架构师学习

                             

6、牛客网https://www.nowcoder.com/home

                            

各个公司的面试题和面经分享,找工作前认真刷一刷,一定会有很大收获!拿到心仪的 offer!

7、掘金https://juejin.im/user/2902319386341646

现在国内优质的开发者交流学习社区,可以去看大佬们写的文章,也可以自己分享学习心的,与更多开发者交流。认识更多的小伙伴儿,提升个人影响力。

8、*博客园:https://www.cnblogs.com/

                                 

博客园创立于2004年1月,是一个面向开发者的知识分享社区。自创建以来,博客园一直致力并专注于为开发者打造一个纯净的技术交流社区,推动并帮助开发者通过互联网分享知识,从而让更多开发者从中受益。博客园的使命是帮助开发者用代码改变世界。很多早期的高质量内容都在博客园。

9、程序员客栈:https://www.proginn.com/

程序员客栈是领先的程序员自由工作平台,如果你是有经验有资质的开发者,都可以来上面注册成为开发者,业余的时候做点项目,赚点零花钱。当然,如果你想成为一名自由工作者,程序员客栈也是可以满足的。只要你有技术,不怕赚不到钱。很多程序员日常在这里逛一下,接一点项目做。很多公司也在这发布项目需求。


 国内的就先总结到这里,我们再来看看几个国外的:

1、 GitHub — 开发者最最最重要的网站https://github.com

这个不用多说了吧,代码托管网站,上面有很多资源,想要什么轮子,上去搜就好了。并且呢,上面有很多优秀的程序员,你可以在这里交到很多好朋友喔。

2、Stack Overflow — 解决 bug 的社区https://stackoverflow.com/

开发过程中遇到什么 bug,上去搜一下,只要搜索的方式对,百分之 99 的问题都能搜到答案。在这里能够与很多有经验的开发者交流,如果你是有经验的开发者,还可以来这儿帮助别人解决问题,提升个人影响力。

3、Medium:https://medium.com/

Hacker News:https://news.ycombinator.com/news

这两个都是国外优质文章网站,Medium 的整体结构非常简单,容易让用户沉下心来专注于阅读。上面有很多高质量的技术文章,有很多厉害的人在上面发布内容。


除了这些论坛,程序员还有一些必备网站:


0. Google:https://google.com

这个不用多说了吧,查资料,有问题,Google 一下。当然,能访问的人自然会用,访问不了的人,可以用必应或者百度吧。

1. 算法学习 LintCode:https://www.lintcode.com/

算法学习网站,上去每天刷两道算法题,走遍天下都不怕。

2. 算法学习 LeetCode:https://leetcode.com/

也是算法题网站,同上。

3. 算法学习 LeetCode 中文站https://leetcode-cn.com/

这个是上面算法题网站的中文站点,英文不好的可以刷这个,英文好的推荐去刷英文网站的题目,还能提升英语能力。

4. Web 开发练习题https://www.freecodecamp.org/

这是国外发起的一个 Web 开发学习的网站,从简单到深入,一步一步带你学习 Web 开发。就像一本练习册,并且当你完成相应的内容后,会得到相应的资格认证。

5. 百度前端技术学院 — 前端开发项目库http://ife.baidu.com

学前端的看这里,百度官方推出的前端开发学习技术学院,题目从简单到困难,如果你把里面的题都做会了,找个 BAT 的前端工作不成问题的。

其他学习网站:

0. 各种编程语言,编程工具,各种轮子的官方网站

要记得,学习一门语言或者一个工具,最优质的学习网站就是他的官方网站,官方文档。

1. 菜鸟教程:http://www.runoob.com/

菜鸟教程的 Slogan 为:学的不仅是技术,更是梦想! 记住:再牛逼的梦想也抵不住傻逼似的坚持!网站包括了HTML、CSS、Javascript、PHP、C、Python等各种基础编程教程。

2. 中国大学MOOC网:https://www.icourse163.org/

中国大学MOOC是由网易与高教社携手推出的在线教育平台,承接教育部国家精品开放课程任务,向大众提供中国知名高校的MOOC课程。在这里,每一个有意愿提升自己的人都可以免费获得更优质的高等教育。

推荐给前端程序员的技术、论坛、资讯网站:

  1. https://medium.freecodecamp.com/
  2. https://css-tricks.com/
  3. http://css-weekly.com/
  4. https://www.html5rocks.com/en/
  5. https://mobilewebweekly.com/
  6. http://www.echojs.com/
  7. http://us5.campaign-archive1.com/?u=ea228d7061e8bbfa8639666ad&id=68fee2a1f3&e=91389ff35f
  8. https://www.smashingmagazine.com/
  9. https://www.sitepoint.com/
  10. http://javascriptweekly.com/
  11. http://frontendfocus.co/
  12. https://frontendfoc.us/
  13. http://reactjsnewsletter.com/issues
  14. http://feeds.feedburner.com/html5rocks

推荐给前后端程序员的技术、论坛、资讯网站:

  1. https://hashnode.com/
  2. http://us4.campaign-archive1.com/?u=9735795484d2e4c204da82a29&id=0f792acd6e&e=e6bacace33
  3. http://rubyweekly.com/
  4. https://golangweekly.com/
  5. https://dbweekly.com/
  6. https://risingstack.com/
  7. http://nodeweekly.com/
  8. https://webopsweekly.com/
  9. https://postgresweekly.com/
  10. http://nodeweekly.com

推荐给前安卓程序员的技术、论坛、资讯网站:

  1. http://androidweekly.net/
  2. http://us2.campaign-archive2.com/?u=869610fc59cf83e08b6e0635a&id=6880ca6f63&e=1411ee8814

推荐给前 iOS 程序员的技术、论坛、资讯网站:

  1. https://iosdev.tools/
  2. https://iosdevweekly.com/
  3. https://littlebitesofcocoa.com/
  4. http://ios-goodies.com/
  5. http://digest.swiftweekly.com/

老码农推荐:

1. 在线学习网站https://www.tutorialspoint.com/

2. 算法学习和竞赛网站http://codeforces.com/

3. 程序员问答网站https://segmentfault.com/

4. Linux Kernelhttps://www.kernel.org/

5. FCC 中文网:https://www.freecodecamp.one/

7. 阿里巴巴开源镜像:https://opsx.alibaba.com/mirror

8. USTC 开源镜像:http://mirrors.ustc.edu.cn/ 

9. 算法练习网站:https://www.hackerrank.com/

不是吧,都看到这里了,都不给我来个点赞关注收藏吗???
image

查看原文

崔学森 收藏了文章 · 11月2日

中了源码的毒,给你一副良药

近期阿宝哥在团队内搞了一个 如何读源码 的专题,主要目的是让团队的小伙伴们了解读源码的思路与技巧。在此期间,阿宝哥也写了 77.9K 的 Axios 项目有哪些值得借鉴的地方从 12.9K 的前端开源项目我学到了啥如何让你的 Express 飞起来 三篇源码解析的文章。其中前两篇在 掘金社区 获得不错的评价,平均 705+ 个 👍,所以阿宝哥就想写一篇文章来分享一下本人读源码的思路、技巧与工具。

好的,让我们开始出发吧!在进入正题之前,我们先来个读源码前的 灵魂四连问 热热身。

一、灵魂四连问

1.1 为什么要读源代码

1.2 如何选择项目

1.3 如何阅读源码

1.4 有实际的案例么

既然前两篇文章比较受大家喜欢,接下来阿宝哥就以最受欢迎的 Axios 为例,来分享一下读源码的思路与技巧。

二、如何品读 Axios?

2.1 走进 Axios

Axios 是一个基于 Promise 的 HTTP 客户端,同时支持浏览器和 Node.js 环境。它是一个优秀的 HTTP 客户端,被广泛地应用在大量的 Web 项目中。

由上图可知,Axios 项目的 Star 数为 78.1K,Fork 数也高达 7.3K,是一个很优秀的开源项目,所以值得大家细细品读。

2.2 发现 Axios 的美

在确认 Axios 为 “追求目标” 之后,下一步我们就需要来发现它身上的优点(特性):

每个人对 “美” 都有不同的看法,对于阿宝哥来说,我看中了图中已选中的三点。因此,它们也很光荣地成为读源码的三个切入点。当然切入点也不是越多越好,可以先找自己最感兴趣的地方作为切入点。需要注意的是,如果切入点之间有关联关系的话,建议做个简单的排序。

2.3 感受 Axios 的美

选择切入点之后,我们就可以开始逐一感受 Axios 的设计之美。以 能够拦截请求与响应 这个切入点为例,首先我们就会接触到 拦截器 的概念。所以我们需要先了解拦截器是什么、拦截器有什么作用以及如何使用拦截器,这里我们可以从项目的 官方文档 或者项目中的 README.md 文档入手。

2.3.1 拦截器的作用

Axios 提供了请求拦截器和响应拦截器来分别处理请求和响应,它们的作用如下:

  • 请求拦截器:该类拦截器的作用是在请求发送前统一执行某些操作,比如在请求头中添加 token 字段。
  • 响应拦截器:该类拦截器的作用是在接收到服务器响应后统一执行某些操作,比如发现响应状态码为 401 时,自动跳转到登录页。
2.3.2 拦截器的使用
// 添加请求拦截器 —— 处理请求配置对象
axios.interceptors.request.use(function (config) {
  config.headers.token = 'added by interceptor';
  return config;
});

// 添加响应拦截器 —— 处理响应对象
axios.interceptors.response.use(function (data) {
  data.data = data.data + ' - modified by interceptor';
  return data;
});

axios({
  url: '/hello',
  method: 'get',
}).then(res =>{
  console.log('axios res.data: ', res.data)
});

在了解完拦截器的作用和用法之后,我们就会把焦点聚焦到 axios 对象,因为注册拦截器和发送请求都与它有紧密的联系。不过在看具体源码之前,阿宝哥建议先对功能点做一下梳理。以下是阿宝哥的分析思路:

Axios 的作用是用于发送 HTTP 请求,请求拦截器和响应拦截器分别对应于 HTTP 请求的不同阶段,它们的本质是一个实现特定功能的函数。这时我们就可以按照功能把发送 HTTP 请求拆解成不同类型的子任务,比如有 用于处理请求配置对象的子任务用于发送 HTTP 请求的子任务用于处理响应对象的子任务。当我们按照指定的顺序来执行这些子任务时,就可以完成一次完整的 HTTP 请求。

既然已经提到了任务,我们就会联想到任务管理系统的基本功能:任务注册、任务编排(优先级排序)和任务调度等。因此我们就可以考虑从 任务注册、任务编排和任务调度 三个方面来分析 Axios 拦截器的实现。

2.3.3 任务注册
// 添加请求拦截器 —— 处理请求配置对象
axios.interceptors.request.use(function (config) {
  config.headers.token = 'added by interceptor';
  return config;
});

// 添加响应拦截器 —— 处理响应对象
axios.interceptors.response.use(function (data) {
  data.data = data.data + ' - modified by interceptor';
  return data;
});

lib/axios.js 路径下,我们可以找到 axios 对象的定义。为了能直观地了解对象之间的关系,阿宝哥建议大家在读源码的过程中,多动手画画图。比如阿宝哥使用下图来总结一下 Axios 对象与 InterceptorManager 对象的内部结构与关系:

2.3.4 任务编排

现在我们已经知道如何注册拦截器任务,但仅仅注册任务是不够,我们还需要对已注册的任务进行编排,这样才能确保任务的执行顺序。

同样对于任务编排,也可以使用图的形式来展现任务编排后的结果。 这里有一个小技巧,就是可以采用对比的形式来展示任务编排后的结果,这样子会更加清楚任务编排的处理逻辑。

2.3.5 任务调度

任务编排完成后,要发起 HTTP 请求,我们还需要按编排后的顺序执行任务调度。

需要注意的是:在阅读源码过程中,不要太在意细节。比如在研究 Axios 拦截器原理时,不需要再深入了解 dispatchRequest 背后的具体实现,只需知道该方法用于实现发送 HTTP 请求即可,这样才不会把整个线路拉得太长。

在分析完特定的功能点之后,也许你已经读懂的具体的源代码。但阿宝哥觉得这并不是最重要的,更重要的是思考它的设计思想,这样设计有什么好处,对于我们有没有什么值得借鉴和学习的地方。比如参考 Axios 拦截器的设计模型,我们就可以抽出以下通用的任务处理模型:

上面阿宝哥以 Axios 的拦截器为例,分享了读 Axios 源码的思路与技巧。接下来阿宝哥来分享一些读源码的建议和辅助工具。

三、读源码的建议

四、读源码辅助工具

如果你对下列辅助工具感兴趣的话,可以通过以下图片来源的链接,来直接打开每个工具的在线地址。

(图片来源:https://www.processon.com/vie...

五、总结

其实除了上面的内容之外,读优秀开源项目还有挺多值得关注的地方。阿宝哥在学习 BetterScroll 项目源码时,总结了一张思维导图

(图片来源:https://www.processon.com/vie...

下面阿宝哥用一张图来总结一下 axiosbetter-scroll 这两个开源项目的学习路线:

1、Axios 项目的切入点是从 Github 中的功能特性中筛选出来的;

2、BetterScroll 的切入点是从掘金上 BetterScroll 2.0 发布:精益求精,与你同行 这篇文章中介绍的功能亮点中找到的。

除此之外,阿宝哥也来简单总结一下本文介绍的读源码的思路与技巧:

  • 站在巨人的肩膀,提前阅读一些项目相关的优质文章;
  • 汇总学习或工作中遇到的问题,带着问题进行源码学习;
  • 明确阅读源码的主线或切入点;
  • 尽可能从简单的示例出发来分析每个功能点;
  • 先梳理清楚主要流程,不要太在意细节,避免把整个线路拉得太长;
  • 在阅读源码过程中,要多多画图,这样理解起来会更加直观。

本文阿宝哥分享了个人读源码的思路、技巧与工具,希望阅读完本文能对你有所启发或帮助。如果你有读源码更好的思路与技巧,欢迎随时跟阿宝哥交流哈。有写得不好的地方,也请各位见谅哈。

六、参考资源

查看原文

崔学森 赞了文章 · 10月24日

(一)熟练HTML5+CSS3,每天复习一遍

前言

学习网页的概念和分类,了解静态网页和动态网页的不同;了解网页浏览器的工作原理。了解HTML,XHTML,HTML5的概念,制作简单的HTML页面的开发。

什么是网页

可以在internet上通过网页浏览信息,如新闻,图片等,还可发布信息,如招聘信息等,网页是在某个地方某一台计算机上的一个文件。

网页主要由3部分组成:结构,表现,行为。

静态网页的特点是不论在何时何地浏览这个网页,看到的形式和内容都相同,且只能浏览,用户无法与网站进行互动。静态页面由HTML编写,扩展名一般为.htm, .html, .shtml, .xml等。与动态页面相比,动态网页是以.asp, .jsp, .php, .perl, .cgi等形式为后缀。

动态网页指网页的内容可以根据某种条件而自动改变。

网页浏览器的工作原理

采用B/S结构,即浏览器/服务器结构,用户工作界面是通过www浏览器来实现的:

  1. 事务逻辑主要在服务器端实现,极少部分的事务逻辑在前端实现。
  2. 大大简化了客户端的计算机载荷。
  3. 减轻了系统维护与升级的成本和工作量。
  4. 降低了用户的总体成本。

浏览器的工作原理:

  1. 浏览器通过HTML表单或超链接请求指向一个应用程序的URL。
  2. 服务器收到用户的请求。
  3. 服务器执行已接收创建的指定应用程序。
  4. 应用程序通常基于用户输入的内容,执行所需要的操作。
  5. 应用程序把结果格式化为网络服务器和浏览器能够理解的文档,即通常所说的HTML网页。
  6. 网络服务器最后将结果返回到浏览器中。

www的基础是HTTP协议,web浏览器就是用于通过url来获取并显示web网页的一种软件工具,url用于指定要取得的Internet上资源的位置与方式。

HTML和HTML5

HTML是一种用来制作超文本文档的简单标记语言,用其编写的超文本文档称为HTML文档,它能独立于各种操作系统平台。

可扩展超文本标记语言XHTML:

XHTML是不需要编译,可以直接由浏览器执行,是一种增强了的HTML。它的可扩展性和灵活性将适应未来网络应用的更多需求,是基于XML的应用。开发者在HTML4.0的基础上,用XML的规则对其进行一些扩展,由此得到了XHTML,所以,建立XHTML的目的是为了实现HTML向xml的过渡。

HTML5简化了:<!DOCTYPE html>,简化了DOCTYPE,简化了字符集声明,以浏览器的原生能力替代脚本代码的实现,简单而强大的HTML5API。

HTML网页的结构

文件扩展名是操作系统用来标志文件格式的一种机制。扩展名如同文件的身份说明,区别了文件的类别和作用。

HTML网页的文件后缀名是.html或者.htm.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"xxx">
声明的作用,告诉浏览器所书写的HTML代码的版本。

<meta>标签,是HTML文档<head>标签内的一个辅助性标签,meta标签分为2个重要的属性:namehttp-equiv,通常用于能够优化页面被搜索的可能性。

meta标签下name属性的使用:

<head>
 <meta name="keywords" content="nine, twenty-three">
 <meta name="description" content="...">
 <meta name="generator" content="Dreamweaver">
 <meta name="author" content="depp">
 <meta name="robots" content="all">
</head>
  1. keywords向搜索引擎说明页面的关键字,在content后输入供搜索的具体关键字。
  2. description向搜索引擎描述页面的主要内容。
  3. generator向页面描述生成的软件名,在content后面输入具体的软件名称。
  4. author网页的设计者,在content后面输入设计者的具体姓名。
  5. robots限制搜索的方式,在content后面通常可输入all,one,index,noindex,follow,nofollow其中之一,不同的属性分别有不同的作用,限制页面被搜索的方式。

meta标签下的另一个属性http-equiv,其作用是反馈给浏览器一些明确的信息,帮助浏览器更精确地展示页面。

<head>
 <meta http-equiv="content-type"  content="text/html; charset=gb2312"/>
</head>
  1. refresh 对属性的具体描述,说明是令页面自动跳转的效果。
  2. content 后跟等待的时间,url后跟跳转的页面链接地址。

link标签,定义了一个外部文件的链接,经常被用于链接外部css样式。

base标签为整个页面定义了所有链接的基础定位,其主要的作用是确保文档中所有的相对url都可以被分解成确定的文档地址。

style标签用于定义css的样式。表明了在页面中引入一个.style的样式表。

script标签用于定义页面内的脚本。

titl标题标签,body体标签.

一个好的HTML文档应具备以下3个方面:
  1. 代码使用标准规范,不应该有错误的拼写
  2. 代码结构清晰,使人一目了然
  3. 没有错误或者多余不必要的代码出现

文本设计

<br>..</br>
<p>...</p>
<p align=left>...</p>
<p align=center>...</p>
<p align=right>...</p>

给文本加标注:<acronym title="">...</acronym>注释的内容放在title属性后的引号中,被注释的内容放在标签内。

无序列表:ul,li,有序列表:ol li

定义列表:

<dl>
 <dt>...</dt>
 <dd>...</dd>
 <dt>...</dt>
 <dd>...</dd>
</dl>

网页中的图像设计

  1. jepg格式的图像,该文件是常见的图像格式,.jpg后缀名,jpeg文件是经过压缩的一种图像。压缩的图像可以保持为8位,24位,32位深度的图像,压缩比率可以高达100:1.jpeg可以很好地处理大面积色调的图像。
  2. png格式的图像,后缀名.png,这是一种能存储32位信息的位图图像,采用的是一种无损压缩的方式。支持透明信息,指图像以浮现在其他页面文件或页面图像之上。
  3. gif格式的图像,是一种图像交互格式,后缀名.gif,只支持256色以内的图像,gif文件的图像效果是很差的。

所以总的来说:jepg可以压缩图像的容量,png的质量较好,gif可以做动画。

矢量图

说说矢量图和位图最大的区别:

无论是否对图像进行缩放,都不会影响矢量图的效果,但会影响图的质量。

设计者一般只愿意将logo,ui图标,标识符号等简单图像存为矢量图。

图像的分辨率

分辨率的单位是dpi即每英寸显示的线数。通常所指的分辨率有两种,屏幕分辨率和图片分辨率,屏幕分辨率即计算机显示器默认的分辨率。

一般目前大部分显示器的分辨率是1024px x 768px,图片分辨率定义是用于量度位图图像内数据量多少的一个参数。

分辨率越高的图像,包含的数据越多,图像的容量就越大,会消耗更多的计算机资源,需要更大的存储空间。

分辨率指的是每英寸的像素值,通过像素和分辨率的换算可以测算图片的长度。

页面中的图像
<img data-original=... alt=.../>
  1. 使图像的顶部和同一行的文本对齐
<img style="vertial-align:text-top"/>
  1. 使图像的中部和同一行的文本对齐
<img style="vertical-align:middle"/>
  1. 使图像的底部和同一行的文本对齐
<img style="vertical-align:text-bottom"/>
  1. 使图像的底部和文本的基线对齐
<img style="vertical-alignbaseline"/>

hspace=30px表示图像左,右两边与页面其他内容隔30px的距离。vspace=30px表示图像上,下两边与页面的其他内容的间隔距离是30px。

<img data-original="" widht="" height="">

<img data-original="..." border=>

<hr align=".." width="..." size="...">

<a href="链接对象的路径">链接锚点对象</a>

把邮箱留给需要联系你的人

<a href="mailto:邮箱地址">链接锚点对象</a>
  1. 链接还未被访问:a:link{...}
  2. 链接被选中:a:active{...}
  3. 光标滑过链接:a:hover{...}
  4. 链接被访问后:a:visited{...}
dashed 虚线

double 双线

groove 槽线

inset 内陷

outset 外陷
热点图像区域的链接

map标签:

<map id=...>
 <area shape="..." coords="..." href="...">
</map>

shape属性,用于确定选区的形状,rect矩形,circle圆形,poly多边形。href属性,就是超链接。coords属性,用于控制形状的位置,通过坐标来找到这个位置。

网页中的表单

计算矩形的面积

<html>
<head>
<title>计算矩形的面积</title>
<style type="text/css">
 .result {font-weight:bold;}
</style>
<script language="JavaScript">
function calculate() {
 var length = document.data.length.value;
 var width = document.data.width.value;
 var height = document.data.height.value;
 var area = document.getElementById('area');
 area.innerHTML = length*widht;
 volume.innerHTML = length*widht*height;
 }
</script>

创建表单

  1. action属性,通过form标签定义的表单里必须有action属性才能将表单中的数据提交出去:
<form action="my.php"></form>

它表明了这是一个表单,其作用是提交my.php页面中的数据。

  1. method属性告诉浏览器数据是以何种方式提交出去的。method属性下可以有2个选择:post或者get
  2. name属性,为了令递交出去的表单数据能够被处理这些数据的程序识别。
<form name="data">
  1. 编码方式,enctype代表HTML表单数据的编码方式。

表单的工作原理

原理:在客户端接收用户的信息,然后将数据递交给后台的程序来操控这些数据。

<script language="JavaScript">

如果通过引用外部javascript程序,就像链接外联样式:

<script type="text/javascript" data-original="dada.js"></script>

创建表单

  1. action属性,有action属性才能将表单中的数据提交出去:
<form action="da.php"></form>
  1. method 属性,作用是告诉浏览器数据是以何种方式提交出去的。在method属性下可以有2个选择,post或get。

提交方式用get,表单域中输入的内容会添加在action指定的url中,当表单提交之后,用户会获取一个明确的url。get在安全性上较差,所有表单域的值直接呈现。post除了有可见的处理脚本程序,别的东西都可以隐藏。

  1. name属性,添加name属性是为了令递交出去的表单数据能够被处理这些数据的程序识别。
<form name="dada">
  1. 编码方式:enctype代表HTML表单数据的编码方式,application/x-www-form-urlencoded, multipart/form-data, text/plain三种方式。
  • application/x-www-form-urlencoded是标准的编码方式,提交的数据被编码为名称/值对。
  • multipart/form-data属性表示数据编码为一条消息,为表单定义mime编码方式,创建了一个与传统不同的post缓冲区,,页面上每个控件对应消息中的一个部分。
  • text/plain表示数据以纯文本的形式进行编码,这样在信息中将不包含控件或者格式字符。
  • multipart/form-data方式上传文件时,不能使用post属性。
  1. 目标显示方式,表示在何处打开目标url,可以设置4种方式。
  • _blank表示在新的页面中打开链接
  • _self表示在相同的窗口中打开页面
  • _parent表示在父级窗口中打开页面
  • _top表示将页面载入到包含该链接的窗口,取代任何当前在窗口中的页面。
<form action="mailto:da@qq.com" method="post" name="dada"
enctype="text/plain" target="_blank"></form>

表单域

是指用户输入数据的地方,表单域可分为3个对象,input, textarea, select。

input对象下的多种表单的表现形式。
<input name="" type="" value="" size="" maxlength="">
  • type表示所定义的是哪种类型的表单形式
  • size表示文本框字段的长度
  • maxlength表示可输入的最长的字符数量
  • value表示预先设置好的信息
  1. text单行的文本框
  2. password将文本替换*的文本框
  3. checkbox只能做二选一的是或否选择
  4. radio从多个选项中确定的一个文本框
  5. submit确定命令文本框
  6. hidden设定不可浏览用户修改的数据
  7. image用图片表示的确定符号
  8. file设置文件上传
  9. button用来配合客户端脚本
<form action="" method="post">
<input name="name" type="text" size="20" maxlength="12">
</form>
<input name="secret" type="password" size="20" maxlength="20">

<input name="one" type="radio" value="one" checked="checked">
<input name="one" type="radio" value="two">

<input type="submit" value="确定">
<input type="reset" value="恢复">
创建submit按钮或reset按钮时,name属性不是必需的。

hidden隐藏域的样式表单

使用hidden来记录页面的数据并将它隐藏起来,用户对这些数据通常并不关心,但是必须提交数据。

<form action=da.asp>
<input type=hidden name=somehidden value=dada>
<input type=submit value=下一页>
</form>
image样式的表单
<input type="image" data-original="图片/小图标.jpg" alt="确定">
  • src属性指定这张图像的路径
  • alt属性添加文本注释
file上传文件的样式表单

file样式表单允许用户上传自己的文件

<html>
<head>
<title>file样式的表单</title>
<style type="text/css">
body {font:120% 微软雅黑;}
input {font:100% 微软雅黑;}
</style>
</head>
上传我的文件:
<form action="..." method="post" enctype="multipart/form-data">
<input type="file" name="uploadfile" id="uploadfile"/>
</form>
</body>
</html>
textarea对象的表单

textarea对象的表单

<html>
<head>
<title>file样式的表单</title>
<style type="text/css">
body{font:120% 微软雅黑;}
textarea{font:80% 微软雅黑;color:navy;}
</style>
</head>
<body>
留言板
<form action="..." method="post" enctype="multipart/form-data">
<textarea name="dada" rows="10" cols="50" value="dada">请说:</textarea>
</form>
</body>
</html>
select对象的表单

select对象的表单

<form action="">
 地址:
 <select name="da1">
  <option>1</option>
 </select>
</form>

使用optgroup标签配合label属性来给选项分类:

<select name="上海">
<optgroup label="da1">
<option>1</option>
</optgroup>
<optgroup label="da2">
<option>2</option>
</optgroup>
</select>

select标签中加入size属性即可,如size=6表示是一个能容纳6行文字的文本框,超出设置的行数时,将出现滚动条。

<select name="上海" size="6">

表单域集合:表单域的代码由fieldset标签和legend标签组合而成。

<form action="..." method="post">
<fieldset>
<legend>注册信息:</legend>
输入用户名:<input name="name" type="text" size="20" maxlength="12">
</fieldset>
</form>
表单输入类型
  • url类型的input元素是专门为输入url地址定义的文本框。
<input type="url" name="webUrl" id="webUrl" value="http://wwwxxx"/>
  • email类型的input元素是专门为输入email地址定义的文本框。
<input type="email" name="dada" id="dada" value="23@qq.com"/>
  • range类型的input元素用于把输入框显示为滑动条,可以作为某一特定范围内的数值选择器。
<input type="range" name="volume" id="volume" min="0" max="1" step="0.2"/>
  • number类型的Input元素是专门为输入特定的数字而定义的文本框。
<input type="number" name="score" id="score" min="0" max="10" step="0.5"/>
  • tel类型的input元素是专门为输入电话号码而定义的文本框,没有特殊的验证规则。
  • search类型的input元素是专门为输入搜索引擎关键词定义的文本框,没有特殊的验证规则。
  • color类型的input元素默认会提供一个颜色选择器。
  • date类型的Input元素是专门用于输入日期的文本框,默认为带日期选择器的输入框。
  • month提供一个月的选择器,week提供一个周选择器,time会提供时间选择器,datetime会提供完整的日期和时间选择器,datetime-local会提供完整的日期和时间选择器。
增加表单的特性以及元素
  1. form特性:
<input name="name" type="text" form="form1" required/>
<form id="form1">
<input type="submit" value="提交"/>
</form>
  1. formaction特性,将表单提交至不同的页面。
<form id="form1" method="post">
<input name="name" type="text" form="form1"/>
<input type="submit" value="提交到page1" formaction="?page=1”/>
<input type="submit" value="提交到page2" formaction="?page=2"/>
<input type="submit" value="提交"/>
</form>
  • formmethod特性可覆盖表单的method特性
  • formenctype特性可覆盖表单的enctype特性
  • formnovalidate特性可覆盖表单的novalidate特性
  • formtarget特性可覆盖表单的target特性
placeholder特性
<input name="name" type="text" placeholder="请输入关键词"/>

autofocus特性:用于当页面加载完成时,可自动获取焦点,每个页面只允许出现一个有autofocus特性的input元素。

<input name="key" type="text" autofocus/>

autocomplete特性用于form元素和输入型的Input元素,用于表单的自动完成。

input name="key" type="text" autocommplete="on"/>

autocomplete特性有三个值,可以指定"on","off"和""不指定,不指定就将使用浏览器的默认设置。

<input name="email" type="email" list="emaillist"/>
<datalist id="emaillist">
<option value="23#qq.com">xxxx</option>
</datalist>

keygen元素提供一个安全的方式来验证用户。

<form action="">
<input type="text" name="name"/><br>
<keygen name="security"/>
<br><input type="submit"/>
</form>
  1. keygen元素有密钥生成的功能,在提交表单时,会分别生成一个私人密钥和一个公共密钥。
  2. 私人密钥保存在客户端,公共密钥则通过网络传输至服务器。

output元素

  1. output元素用于不同类型的输出,比如计算结果或脚本的输出等。
  2. output元素必须从属于某个表单,即写在表单的内部。
<form oninput="x.value=dada.value">
<input type="range" name="volume" value="50"/>
<output name="x"></output>
</form>

required

为某个表单内部的元素设置了required特性,那么这项的值不能为空,否则无法提交表单。

<input name="name" type="text" placeholder="dada" required/>

pattern

  1. pattern用于为Input元素定义一个验证模式。
  2. 该特性值是一个正则表达式,提交时会检查输入的内容是否符合给定的格式,如果不符合则不能提交。
<input name="code" type="text" value="" pattern="[0-9]{6}" placeholder="da"/>

min,max,step

  1. min表示允许范围内的最小值
  2. max表示允许范围内的最大值
  3. step表示合法数据的间隔步长
<input type="range" name="dada" id="dada" min="0" max="1" step="0.2"/>

novalidate

  1. 用于指定表单或表单内在提交时不验证
  2. 如果在form元素应用novalidate特性,则表单中的所有元素在提交时都不需要再验证
<form action="dada.asp" novalidate="novalidate">
<input type="email" name="user_email"/>
<input type="submit"/>
</form>

validity

  1. 获取表单元素的ValidityState对象,该对象包含8个方面的验证结果
  2. ValidityState对象会持续存在,每次获取validity属性时,返回的是同一个ValidityState对象
var validityState=document.getElementById("username").validity;

willValidate属性

  1. 用于获取一个布尔值,表示表单元素是否需要验证
  2. 如表单元素设置了required特性或pattern特性,则willValidate属性的值为true,即表单的验证将执行
var willValidate = document.getElementById("username").willValidate;

validationMessage

  1. 获取当前表单元素的错误提示信息。
var validationMessage=document.getElementById("username").validationMessage;

点关注,不迷路

好了各位,以上就是这篇文章的全部内容,能看到这里的人都是人才。我后面会不断更新技术相关的文章,如果觉得文章对你有用,欢迎给个“赞”,也欢迎分享,感谢大家 !!

查看原文

赞 8 收藏 5 评论 1

崔学森 赞了文章 · 10月14日

如何写出一个惊艳面试官的深拷贝?

导读

最近经常看到很多JavaScript手写代码的文章总结,里面提供了很多JavaScript Api的手写实现。

里面的题目实现大多类似,而且说实话很多代码在我看来是非常简陋的,如果我作为面试官,看到这样的代码,在我心里是不会合格的,本篇文章我拿最简单的深拷贝来讲一讲。

看本文之前先问自己三个问题:

  • 你真的理解什么是深拷贝吗?
  • 在面试官眼里,什么样的深拷贝才算合格?
  • 什么样的深拷贝能让面试官感到惊艳?

本文由浅入深,带你一步一步实现一个惊艳面试官的深拷贝。

本文测试代码:https://github.com/ConardLi/C...

例如:代码clone到本地后,执行 node clone1.test.js查看测试结果。

建议结合测试代码一起阅读效果更佳。

深拷贝和浅拷贝的定义

深拷贝已经是一个老生常谈的话题了,也是现在前端面试的高频题目,但是令我吃惊的是有很多同学还没有搞懂深拷贝和浅拷贝的区别和定义。例如前几天给我提issue的同学:

很明显这位同学把拷贝和赋值搞混了,如果你还对赋值、对象在内存中的存储、变量和类型等等有什么疑问,可以看看我这篇文章:https://juejin.im/post/5cec1b...

你只要少搞明白拷贝赋值的区别。

我们来明确一下深拷贝和浅拷贝的定义:

浅拷贝:

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

深拷贝:

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

话不多说,浅拷贝就不再多说,下面我们直入正题:

乞丐版

在不使用第三方库的情况下,我们想要深拷贝一个对象,用的最多的就是下面这个方法。

JSON.parse(JSON.stringify());

这种写法非常简单,而且可以应对大部分的应用场景,但是它还是有很大缺陷的,比如拷贝其他引用类型、拷贝函数、循环引用等情况。

显然,面试时你只说出这样的方法是一定不会合格的。

接下来,我们一起来手动实现一个深拷贝方法。

基础版本

如果是浅拷贝的话,我们可以很容易写出下面的代码:

function clone(target) {
    let cloneTarget = {};
    for (const key in target) {
        cloneTarget[key] = target[key];
    }
    return cloneTarget;
};

创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性依次添加到新对象上,返回。

如果是深拷贝的话,考虑到我们要拷贝的对象是不知道有多少层深度的,我们可以用递归来解决问题,稍微改写上面的代码:

  • 如果是原始类型,无需继续拷贝,直接返回
  • 如果是引用类型,创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性执行深拷贝后依次添加到新对象上。

很容易理解,如果有更深层次的对象可以继续递归直到属性为原始类型,这样我们就完成了一个最简单的深拷贝:

function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

我们可以打开测试代码中的clone1.test.js对下面的测试用例进行测试:

const target = {
    field1: 1,
    field2: undefined,
    field3: 'ConardLi',
    field4: {
        child: 'child',
        child2: {
            child2: 'child2'
        }
    }
};

执行结果:

这是一个最基础版本的深拷贝,这段代码可以让你向面试官展示你可以用递归解决问题,但是显然,他还有非常多的缺陷,比如,还没有考虑数组。

考虑数组

在上面的版本中,我们的初始化结果只考虑了普通的object,下面我们只需要把初始化代码稍微一变,就可以兼容数组了:

module.exports = function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

clone2.test.js中执行下面的测试用例:

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8]
};

执行结果:

OK,没有问题,你的代码又向合格迈进了一小步。

循环引用

我们执行下面这样一个测试用例:

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8]
};
target.target = target;

可以看到下面的结果:

很明显,因为递归进入死循环导致栈内存溢出了。

原因就是上面的对象存在循环引用的情况,即对象的属性间接或直接的引用了自身的情况:

解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构:

  • 检查map中有无克隆过的对象
  • 有 - 直接返回
  • 没有 - 将当前对象作为key,克隆对象作为value进行存储
  • 继续克隆
function clone(target, map = new Map()) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        if (map.get(target)) {
            return map.get(target);
        }
        map.set(target, cloneTarget);
        for (const key in target) {
            cloneTarget[key] = clone(target[key], map);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

再来执行上面的测试用例:

可以看到,执行没有报错,且target属性,变为了一个Circular类型,即循环应用的意思。

接下来,我们可以使用,WeakMap提代Map来使代码达到画龙点睛的作用。

function clone(target, map = new WeakMap()) {
    // ...
};

为什么要这样做呢?,先来看看WeakMap的作用:

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

什么是弱引用呢?

在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。

我们默认创建一个对象:const obj = {},就默认创建了一个强引用的对象,我们只有手动将obj = null,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。

举个例子:

如果我们使用Map的话,那么对象间是存在强引用关系的:

let obj = { name : 'ConardLi'}
const target = new Map();
target.set(obj,'code秘密花园');
obj = null;

虽然我们手动将obj,进行释放,然是target依然对obj存在强引用关系,所以这部分内存依然无法被释放。

再来看WeakMap

let obj = { name : 'ConardLi'}
const target = new WeakMap();
target.set(obj,'code秘密花园');
obj = null;

如果是WeakMap的话,targetobj存在的就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。

设想一下,如果我们要拷贝的对象非常庞大时,使用Map会对内存造成非常大的额外消耗,而且我们需要手动清除Map的属性才能释放这块内存,而WeakMap会帮我们巧妙化解这个问题。

我也经常在某些代码中看到有人使用WeakMap来解决循环引用问题,但是解释都是模棱两可的,当你不太了解WeakMap的真正作用时。我建议你也不要在面试中写这样的代码,结果只能是给自己挖坑,即使是准备面试,你写的每一行代码也都是需要经过深思熟虑并且非常明白的。

能考虑到循环引用的问题,你已经向面试官展示了你考虑问题的全面性,如果还能用WeakMap解决问题,并很明确的向面试官解释这样做的目的,那么你的代码在面试官眼里应该算是合格了。

性能优化

在上面的代码中,我们遍历数组和对象都使用了for in这种方式,实际上for in在遍历时效率是非常低的,我们来对比下常见的三种循环for、while、for in的执行效率:

可以看到,while的效率是最好的,所以,我们可以想办法把for in遍历改变为while遍历。

我们先使用while来实现一个通用的forEach遍历,iteratee是遍历的回掉函数,他可以接收每次遍历的valueindex两个参数:

function forEach(array, iteratee) {
    let index = -1;
    const length = array.length;
    while (++index < length) {
        iteratee(array[index], index);
    }
    return array;
}

下面对我们的cloen函数进行改写:当遍历数组时,直接使用forEach进行遍历,当遍历对象时,使用Object.keys取出所有的key进行遍历,然后在遍历时把forEach会调函数的value当作key使用:

function clone(target, map = new WeakMap()) {
    if (typeof target === 'object') {
        const isArray = Array.isArray(target);
        let cloneTarget = isArray ? [] : {};

        if (map.get(target)) {
            return map.get(target);
        }
        map.set(target, cloneTarget);

        const keys = isArray ? undefined : Object.keys(target);
        forEach(keys || target, (value, key) => {
            if (keys) {
                key = value;
            }
            cloneTarget[key] = clone2(target[key], map);
        });

        return cloneTarget;
    } else {
        return target;
    }
}

下面,我们执行clone4.test.js分别对上一个克隆函数和改写后的克隆函数进行测试:

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8],
    f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: {} } } } } } } } } } } },
};

target.target = target;

console.time();
const result = clone1(target);
console.timeEnd();

console.time();
const result2 = clone2(target);
console.timeEnd();

执行结果:

很明显,我们的性能优化是有效的。

到这里,你已经向面试官展示了,在写代码的时候你会考虑程序的运行效率,并且你具有通用函数的抽象能力。

其他数据类型

在上面的代码中,我们其实只考虑了普通的objectarray两种数据类型,实际上所有的引用类型远远不止这两个,还有很多,下面我们先尝试获取对象准确的类型。

合理的判断引用类型

首先,判断是否为引用类型,我们还需要考虑functionnull两种特殊的数据类型:

function isObject(target) {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
}
    if (!isObject(target)) {
        return target;
    }
    // ...

获取数据类型

我们可以使用toString来获取准确的引用类型:

每一个引用类型都有toString方法,默认情况下,toString()方法被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中type是对象的类型。

注意,上面提到了如果此方法在自定义对象中未被覆盖,toString才会达到预想的效果,事实上,大部分引用类型比如Array、Date、RegExp等都重写了toString方法。

我们可以直接调用Object原型上未被覆盖的toString()方法,使用call来改变this指向来达到我们想要的效果。

function getType(target) {
    return Object.prototype.toString.call(target);
}

下面我们抽离出一些常用的数据类型以便后面使用:

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';

在上面的集中类型中,我们简单将他们分为两类:

  • 可以继续遍历的类型
  • 不可以继续遍历的类型

我们分别为它们做不同的拷贝。

可继续遍历的类型

上面我们已经考虑的objectarray都属于可以继续遍历的类型,因为它们内存都还可以存储其他数据类型的数据,另外还有MapSet等都是可以继续遍历的类型,这里我们只考虑这四种,如果你有兴趣可以继续探索其他类型。

有序这几种类型还需要继续进行递归,我们首先需要获取它们的初始化数据,例如上面的[]{},我们可以通过拿到constructor的方式来通用的获取。

例如:const target = {}就是const target = new Object()的语法糖。另外这种方法还有一个好处:因为我们还使用了原对象的构造方法,所以它可以保留对象原型上的数据,如果直接使用普通的{},那么原型必然是丢失了的。

function getInit(target) {
    const Ctor = target.constructor;
    return new Ctor();
}

下面,我们改写clone函数,对可继续遍历的数据类型进行处理:

function clone(target, map = new WeakMap()) {

    // 克隆原始类型
    if (!isObject(target)) {
        return target;
    }

    // 初始化
    const type = getType(target);
    let cloneTarget;
    if (deepTag.includes(type)) {
        cloneTarget = getInit(target, type);
    }

    // 防止循环引用
    if (map.get(target)) {
        return map.get(target);
    }
    map.set(target, cloneTarget);

    // 克隆set
    if (type === setTag) {
        target.forEach(value => {
            cloneTarget.add(clone(value,map));
        });
        return cloneTarget;
    }

    // 克隆map
    if (type === mapTag) {
        target.forEach((value, key) => {
            cloneTarget.set(key, clone(value,map));
        });
        return cloneTarget;
    }

    // 克隆对象和数组
    const keys = type === arrayTag ? undefined : Object.keys(target);
    forEach(keys || target, (value, key) => {
        if (keys) {
            key = value;
        }
        cloneTarget[key] = clone(target[key], map);
    });

    return cloneTarget;
}

我们执行clone5.test.js对下面的测试用例进行测试:

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8],
    empty: null,
    map,
    set,
};

执行结果:

没有问题,里大功告成又进一步,下面我们继续处理其他类型:

不可继续遍历的类型

其他剩余的类型我们把它们统一归类成不可处理的数据类型,我们依次进行处理:

BoolNumberStringStringDateError这几种类型我们都可以直接用构造函数和原始数据创建一个新对象:

function cloneOtherType(targe, type) {
    const Ctor = targe.constructor;
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
            return new Ctor(targe);
        case regexpTag:
            return cloneReg(targe);
        case symbolTag:
            return cloneSymbol(targe);
        default:
            return null;
    }
}

克隆Symbol类型:

function cloneSymbol(targe) {
    return Object(Symbol.prototype.valueOf.call(targe));
}

克隆正则:

function cloneReg(targe) {
    const reFlags = /\w*$/;
    const result = new targe.constructor(targe.source, reFlags.exec(targe));
    result.lastIndex = targe.lastIndex;
    return result;
}

实际上还有很多数据类型我这里没有写到,有兴趣的话可以继续探索实现一下。

能写到这里,面试官已经看到了你考虑问题的严谨性,你对变量和类型的理解,对JS API的熟练程度,相信面试官已经开始对你刮目相看了。

克隆函数

最后,我把克隆函数单独拎出来了,实际上克隆函数是没有实际应用场景的,两个对象使用一个在内存中处于同一个地址的函数也是没有任何问题的,我特意看了下lodash对函数的处理:

 const isFunc = typeof value == 'function'
 if (isFunc || !cloneableTags[tag]) {
        return object ? value : {}
 }

可见这里如果发现是函数的话就会直接返回了,没有做特殊的处理,但是我发现不少面试官还是热衷于问这个问题的,而且据我了解能写出来的少之又少。。。

实际上这个方法并没有什么难度,主要就是考察你对基础的掌握扎实不扎实。

首先,我们可以通过prototype来区分下箭头函数和普通函数,箭头函数是没有prototype的。

我们可以直接使用eval和函数字符串来重新生成一个箭头函数,注意这种方法是不适用于普通函数的。

我们可以使用正则来处理普通函数:

分别使用正则取出函数体和函数参数,然后使用new Function ([arg1[, arg2[, ...argN]],] functionBody)构造函数重新构造一个新的函数:

function cloneFunction(func) {
    const bodyReg = /(?<={)(.|\n)+(?=})/m;
    const paramReg = /(?<=\().+(?=\)\s+{)/;
    const funcString = func.toString();
    if (func.prototype) {
        console.log('普通函数');
        const param = paramReg.exec(funcString);
        const body = bodyReg.exec(funcString);
        if (body) {
            console.log('匹配到函数体:', body[0]);
            if (param) {
                const paramArr = param[0].split(',');
                console.log('匹配到参数:', paramArr);
                return new Function(...paramArr, body[0]);
            } else {
                return new Function(body[0]);
            }
        } else {
            return null;
        }
    } else {
        return eval(funcString);
    }
}

最后,我们再来执行clone6.test.js对下面的测试用例进行测试:

const map = new Map();
map.set('key', 'value');
map.set('ConardLi', 'code秘密花园');

const set = new Set();
set.add('ConardLi');
set.add('code秘密花园');

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8],
    empty: null,
    map,
    set,
    bool: new Boolean(true),
    num: new Number(2),
    str: new String(2),
    symbol: Object(Symbol(1)),
    date: new Date(),
    reg: /\d+/,
    error: new Error(),
    func1: () => {
        console.log('code秘密花园');
    },
    func2: function (a, b) {
        return a + b;
    }
};

执行结果:

最后

为了更好的阅读,我们用一张图来展示上面所有的代码:

完整代码:https://github.com/ConardLi/C...

可见,一个小小的深拷贝还是隐藏了很多的知识点的。

千万不要以最低的要求来要求自己,如果你只是为了应付面试中的一个题目,那么你可能只会去准备上面最简陋的深拷贝的方法。

但是面试官考察你的目的是全方位的考察你的思维能力,如果你写出上面的代码,可以体现你多方位的能力:

  • 基本实现

    • 递归能力
  • 循环引用

    • 考虑问题的全面性
    • 理解weakmap的真正意义
  • 多种类型

    • 考虑问题的严谨性
    • 创建各种引用类型的方法,JS API的熟练程度
    • 准确的判断数据类型,对数据类型的理解程度
  • 通用遍历:

    • 写代码可以考虑性能优化
    • 了解集中遍历的效率
    • 代码抽象能力
  • 拷贝函数:

    • 箭头函数和普通函数的区别
    • 正则表达式熟练程度

看吧,一个小小的深拷贝能考察你这么多的能力,如果面试官看到这样的代码,怎么能够不惊艳呢?

其实面试官出的所有题目你都可以用这样的思路去考虑。不要为了应付面试而去背一些代码,这样在有经验的面试官面前会都会暴露出来。你写的每一段代码都要经过深思熟虑,为什么要这样用,还能怎么优化...这样才能给面试官展现一个最好的你。

参考

小结

希望看完本篇文章能对你有如下帮助:

  • 理解深浅拷贝的真正意义
  • 能整我深拷贝的各个要点,对问题进行深入分析
  • 可以手写一个比较完整的深拷贝

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。

想阅读更多优质文章、可关注我的github博客,你的star✨、点赞和关注是我持续创作的动力!

推荐关注我的微信公众号【code秘密花园】,每天推送高质量文章,我们一起交流成长。

图片描述

查看原文

赞 102 收藏 75 评论 21

崔学森 赞了文章 · 10月14日

如何写出一个惊艳面试官的深拷贝?

导读

最近经常看到很多JavaScript手写代码的文章总结,里面提供了很多JavaScript Api的手写实现。

里面的题目实现大多类似,而且说实话很多代码在我看来是非常简陋的,如果我作为面试官,看到这样的代码,在我心里是不会合格的,本篇文章我拿最简单的深拷贝来讲一讲。

看本文之前先问自己三个问题:

  • 你真的理解什么是深拷贝吗?
  • 在面试官眼里,什么样的深拷贝才算合格?
  • 什么样的深拷贝能让面试官感到惊艳?

本文由浅入深,带你一步一步实现一个惊艳面试官的深拷贝。

本文测试代码:https://github.com/ConardLi/C...

例如:代码clone到本地后,执行 node clone1.test.js查看测试结果。

建议结合测试代码一起阅读效果更佳。

深拷贝和浅拷贝的定义

深拷贝已经是一个老生常谈的话题了,也是现在前端面试的高频题目,但是令我吃惊的是有很多同学还没有搞懂深拷贝和浅拷贝的区别和定义。例如前几天给我提issue的同学:

很明显这位同学把拷贝和赋值搞混了,如果你还对赋值、对象在内存中的存储、变量和类型等等有什么疑问,可以看看我这篇文章:https://juejin.im/post/5cec1b...

你只要少搞明白拷贝赋值的区别。

我们来明确一下深拷贝和浅拷贝的定义:

浅拷贝:

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

深拷贝:

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

话不多说,浅拷贝就不再多说,下面我们直入正题:

乞丐版

在不使用第三方库的情况下,我们想要深拷贝一个对象,用的最多的就是下面这个方法。

JSON.parse(JSON.stringify());

这种写法非常简单,而且可以应对大部分的应用场景,但是它还是有很大缺陷的,比如拷贝其他引用类型、拷贝函数、循环引用等情况。

显然,面试时你只说出这样的方法是一定不会合格的。

接下来,我们一起来手动实现一个深拷贝方法。

基础版本

如果是浅拷贝的话,我们可以很容易写出下面的代码:

function clone(target) {
    let cloneTarget = {};
    for (const key in target) {
        cloneTarget[key] = target[key];
    }
    return cloneTarget;
};

创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性依次添加到新对象上,返回。

如果是深拷贝的话,考虑到我们要拷贝的对象是不知道有多少层深度的,我们可以用递归来解决问题,稍微改写上面的代码:

  • 如果是原始类型,无需继续拷贝,直接返回
  • 如果是引用类型,创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性执行深拷贝后依次添加到新对象上。

很容易理解,如果有更深层次的对象可以继续递归直到属性为原始类型,这样我们就完成了一个最简单的深拷贝:

function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

我们可以打开测试代码中的clone1.test.js对下面的测试用例进行测试:

const target = {
    field1: 1,
    field2: undefined,
    field3: 'ConardLi',
    field4: {
        child: 'child',
        child2: {
            child2: 'child2'
        }
    }
};

执行结果:

这是一个最基础版本的深拷贝,这段代码可以让你向面试官展示你可以用递归解决问题,但是显然,他还有非常多的缺陷,比如,还没有考虑数组。

考虑数组

在上面的版本中,我们的初始化结果只考虑了普通的object,下面我们只需要把初始化代码稍微一变,就可以兼容数组了:

module.exports = function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

clone2.test.js中执行下面的测试用例:

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8]
};

执行结果:

OK,没有问题,你的代码又向合格迈进了一小步。

循环引用

我们执行下面这样一个测试用例:

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8]
};
target.target = target;

可以看到下面的结果:

很明显,因为递归进入死循环导致栈内存溢出了。

原因就是上面的对象存在循环引用的情况,即对象的属性间接或直接的引用了自身的情况:

解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构:

  • 检查map中有无克隆过的对象
  • 有 - 直接返回
  • 没有 - 将当前对象作为key,克隆对象作为value进行存储
  • 继续克隆
function clone(target, map = new Map()) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        if (map.get(target)) {
            return map.get(target);
        }
        map.set(target, cloneTarget);
        for (const key in target) {
            cloneTarget[key] = clone(target[key], map);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

再来执行上面的测试用例:

可以看到,执行没有报错,且target属性,变为了一个Circular类型,即循环应用的意思。

接下来,我们可以使用,WeakMap提代Map来使代码达到画龙点睛的作用。

function clone(target, map = new WeakMap()) {
    // ...
};

为什么要这样做呢?,先来看看WeakMap的作用:

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

什么是弱引用呢?

在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。

我们默认创建一个对象:const obj = {},就默认创建了一个强引用的对象,我们只有手动将obj = null,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。

举个例子:

如果我们使用Map的话,那么对象间是存在强引用关系的:

let obj = { name : 'ConardLi'}
const target = new Map();
target.set(obj,'code秘密花园');
obj = null;

虽然我们手动将obj,进行释放,然是target依然对obj存在强引用关系,所以这部分内存依然无法被释放。

再来看WeakMap

let obj = { name : 'ConardLi'}
const target = new WeakMap();
target.set(obj,'code秘密花园');
obj = null;

如果是WeakMap的话,targetobj存在的就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。

设想一下,如果我们要拷贝的对象非常庞大时,使用Map会对内存造成非常大的额外消耗,而且我们需要手动清除Map的属性才能释放这块内存,而WeakMap会帮我们巧妙化解这个问题。

我也经常在某些代码中看到有人使用WeakMap来解决循环引用问题,但是解释都是模棱两可的,当你不太了解WeakMap的真正作用时。我建议你也不要在面试中写这样的代码,结果只能是给自己挖坑,即使是准备面试,你写的每一行代码也都是需要经过深思熟虑并且非常明白的。

能考虑到循环引用的问题,你已经向面试官展示了你考虑问题的全面性,如果还能用WeakMap解决问题,并很明确的向面试官解释这样做的目的,那么你的代码在面试官眼里应该算是合格了。

性能优化

在上面的代码中,我们遍历数组和对象都使用了for in这种方式,实际上for in在遍历时效率是非常低的,我们来对比下常见的三种循环for、while、for in的执行效率:

可以看到,while的效率是最好的,所以,我们可以想办法把for in遍历改变为while遍历。

我们先使用while来实现一个通用的forEach遍历,iteratee是遍历的回掉函数,他可以接收每次遍历的valueindex两个参数:

function forEach(array, iteratee) {
    let index = -1;
    const length = array.length;
    while (++index < length) {
        iteratee(array[index], index);
    }
    return array;
}

下面对我们的cloen函数进行改写:当遍历数组时,直接使用forEach进行遍历,当遍历对象时,使用Object.keys取出所有的key进行遍历,然后在遍历时把forEach会调函数的value当作key使用:

function clone(target, map = new WeakMap()) {
    if (typeof target === 'object') {
        const isArray = Array.isArray(target);
        let cloneTarget = isArray ? [] : {};

        if (map.get(target)) {
            return map.get(target);
        }
        map.set(target, cloneTarget);

        const keys = isArray ? undefined : Object.keys(target);
        forEach(keys || target, (value, key) => {
            if (keys) {
                key = value;
            }
            cloneTarget[key] = clone2(target[key], map);
        });

        return cloneTarget;
    } else {
        return target;
    }
}

下面,我们执行clone4.test.js分别对上一个克隆函数和改写后的克隆函数进行测试:

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8],
    f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: {} } } } } } } } } } } },
};

target.target = target;

console.time();
const result = clone1(target);
console.timeEnd();

console.time();
const result2 = clone2(target);
console.timeEnd();

执行结果:

很明显,我们的性能优化是有效的。

到这里,你已经向面试官展示了,在写代码的时候你会考虑程序的运行效率,并且你具有通用函数的抽象能力。

其他数据类型

在上面的代码中,我们其实只考虑了普通的objectarray两种数据类型,实际上所有的引用类型远远不止这两个,还有很多,下面我们先尝试获取对象准确的类型。

合理的判断引用类型

首先,判断是否为引用类型,我们还需要考虑functionnull两种特殊的数据类型:

function isObject(target) {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
}
    if (!isObject(target)) {
        return target;
    }
    // ...

获取数据类型

我们可以使用toString来获取准确的引用类型:

每一个引用类型都有toString方法,默认情况下,toString()方法被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中type是对象的类型。

注意,上面提到了如果此方法在自定义对象中未被覆盖,toString才会达到预想的效果,事实上,大部分引用类型比如Array、Date、RegExp等都重写了toString方法。

我们可以直接调用Object原型上未被覆盖的toString()方法,使用call来改变this指向来达到我们想要的效果。

function getType(target) {
    return Object.prototype.toString.call(target);
}

下面我们抽离出一些常用的数据类型以便后面使用:

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';

在上面的集中类型中,我们简单将他们分为两类:

  • 可以继续遍历的类型
  • 不可以继续遍历的类型

我们分别为它们做不同的拷贝。

可继续遍历的类型

上面我们已经考虑的objectarray都属于可以继续遍历的类型,因为它们内存都还可以存储其他数据类型的数据,另外还有MapSet等都是可以继续遍历的类型,这里我们只考虑这四种,如果你有兴趣可以继续探索其他类型。

有序这几种类型还需要继续进行递归,我们首先需要获取它们的初始化数据,例如上面的[]{},我们可以通过拿到constructor的方式来通用的获取。

例如:const target = {}就是const target = new Object()的语法糖。另外这种方法还有一个好处:因为我们还使用了原对象的构造方法,所以它可以保留对象原型上的数据,如果直接使用普通的{},那么原型必然是丢失了的。

function getInit(target) {
    const Ctor = target.constructor;
    return new Ctor();
}

下面,我们改写clone函数,对可继续遍历的数据类型进行处理:

function clone(target, map = new WeakMap()) {

    // 克隆原始类型
    if (!isObject(target)) {
        return target;
    }

    // 初始化
    const type = getType(target);
    let cloneTarget;
    if (deepTag.includes(type)) {
        cloneTarget = getInit(target, type);
    }

    // 防止循环引用
    if (map.get(target)) {
        return map.get(target);
    }
    map.set(target, cloneTarget);

    // 克隆set
    if (type === setTag) {
        target.forEach(value => {
            cloneTarget.add(clone(value,map));
        });
        return cloneTarget;
    }

    // 克隆map
    if (type === mapTag) {
        target.forEach((value, key) => {
            cloneTarget.set(key, clone(value,map));
        });
        return cloneTarget;
    }

    // 克隆对象和数组
    const keys = type === arrayTag ? undefined : Object.keys(target);
    forEach(keys || target, (value, key) => {
        if (keys) {
            key = value;
        }
        cloneTarget[key] = clone(target[key], map);
    });

    return cloneTarget;
}

我们执行clone5.test.js对下面的测试用例进行测试:

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8],
    empty: null,
    map,
    set,
};

执行结果:

没有问题,里大功告成又进一步,下面我们继续处理其他类型:

不可继续遍历的类型

其他剩余的类型我们把它们统一归类成不可处理的数据类型,我们依次进行处理:

BoolNumberStringStringDateError这几种类型我们都可以直接用构造函数和原始数据创建一个新对象:

function cloneOtherType(targe, type) {
    const Ctor = targe.constructor;
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
            return new Ctor(targe);
        case regexpTag:
            return cloneReg(targe);
        case symbolTag:
            return cloneSymbol(targe);
        default:
            return null;
    }
}

克隆Symbol类型:

function cloneSymbol(targe) {
    return Object(Symbol.prototype.valueOf.call(targe));
}

克隆正则:

function cloneReg(targe) {
    const reFlags = /\w*$/;
    const result = new targe.constructor(targe.source, reFlags.exec(targe));
    result.lastIndex = targe.lastIndex;
    return result;
}

实际上还有很多数据类型我这里没有写到,有兴趣的话可以继续探索实现一下。

能写到这里,面试官已经看到了你考虑问题的严谨性,你对变量和类型的理解,对JS API的熟练程度,相信面试官已经开始对你刮目相看了。

克隆函数

最后,我把克隆函数单独拎出来了,实际上克隆函数是没有实际应用场景的,两个对象使用一个在内存中处于同一个地址的函数也是没有任何问题的,我特意看了下lodash对函数的处理:

 const isFunc = typeof value == 'function'
 if (isFunc || !cloneableTags[tag]) {
        return object ? value : {}
 }

可见这里如果发现是函数的话就会直接返回了,没有做特殊的处理,但是我发现不少面试官还是热衷于问这个问题的,而且据我了解能写出来的少之又少。。。

实际上这个方法并没有什么难度,主要就是考察你对基础的掌握扎实不扎实。

首先,我们可以通过prototype来区分下箭头函数和普通函数,箭头函数是没有prototype的。

我们可以直接使用eval和函数字符串来重新生成一个箭头函数,注意这种方法是不适用于普通函数的。

我们可以使用正则来处理普通函数:

分别使用正则取出函数体和函数参数,然后使用new Function ([arg1[, arg2[, ...argN]],] functionBody)构造函数重新构造一个新的函数:

function cloneFunction(func) {
    const bodyReg = /(?<={)(.|\n)+(?=})/m;
    const paramReg = /(?<=\().+(?=\)\s+{)/;
    const funcString = func.toString();
    if (func.prototype) {
        console.log('普通函数');
        const param = paramReg.exec(funcString);
        const body = bodyReg.exec(funcString);
        if (body) {
            console.log('匹配到函数体:', body[0]);
            if (param) {
                const paramArr = param[0].split(',');
                console.log('匹配到参数:', paramArr);
                return new Function(...paramArr, body[0]);
            } else {
                return new Function(body[0]);
            }
        } else {
            return null;
        }
    } else {
        return eval(funcString);
    }
}

最后,我们再来执行clone6.test.js对下面的测试用例进行测试:

const map = new Map();
map.set('key', 'value');
map.set('ConardLi', 'code秘密花园');

const set = new Set();
set.add('ConardLi');
set.add('code秘密花园');

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8],
    empty: null,
    map,
    set,
    bool: new Boolean(true),
    num: new Number(2),
    str: new String(2),
    symbol: Object(Symbol(1)),
    date: new Date(),
    reg: /\d+/,
    error: new Error(),
    func1: () => {
        console.log('code秘密花园');
    },
    func2: function (a, b) {
        return a + b;
    }
};

执行结果:

最后

为了更好的阅读,我们用一张图来展示上面所有的代码:

完整代码:https://github.com/ConardLi/C...

可见,一个小小的深拷贝还是隐藏了很多的知识点的。

千万不要以最低的要求来要求自己,如果你只是为了应付面试中的一个题目,那么你可能只会去准备上面最简陋的深拷贝的方法。

但是面试官考察你的目的是全方位的考察你的思维能力,如果你写出上面的代码,可以体现你多方位的能力:

  • 基本实现

    • 递归能力
  • 循环引用

    • 考虑问题的全面性
    • 理解weakmap的真正意义
  • 多种类型

    • 考虑问题的严谨性
    • 创建各种引用类型的方法,JS API的熟练程度
    • 准确的判断数据类型,对数据类型的理解程度
  • 通用遍历:

    • 写代码可以考虑性能优化
    • 了解集中遍历的效率
    • 代码抽象能力
  • 拷贝函数:

    • 箭头函数和普通函数的区别
    • 正则表达式熟练程度

看吧,一个小小的深拷贝能考察你这么多的能力,如果面试官看到这样的代码,怎么能够不惊艳呢?

其实面试官出的所有题目你都可以用这样的思路去考虑。不要为了应付面试而去背一些代码,这样在有经验的面试官面前会都会暴露出来。你写的每一段代码都要经过深思熟虑,为什么要这样用,还能怎么优化...这样才能给面试官展现一个最好的你。

参考

小结

希望看完本篇文章能对你有如下帮助:

  • 理解深浅拷贝的真正意义
  • 能整我深拷贝的各个要点,对问题进行深入分析
  • 可以手写一个比较完整的深拷贝

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。

想阅读更多优质文章、可关注我的github博客,你的star✨、点赞和关注是我持续创作的动力!

推荐关注我的微信公众号【code秘密花园】,每天推送高质量文章,我们一起交流成长。

图片描述

查看原文

赞 102 收藏 75 评论 21

崔学森 赞了文章 · 10月14日

如何写出一个惊艳面试官的深拷贝?

导读

最近经常看到很多JavaScript手写代码的文章总结,里面提供了很多JavaScript Api的手写实现。

里面的题目实现大多类似,而且说实话很多代码在我看来是非常简陋的,如果我作为面试官,看到这样的代码,在我心里是不会合格的,本篇文章我拿最简单的深拷贝来讲一讲。

看本文之前先问自己三个问题:

  • 你真的理解什么是深拷贝吗?
  • 在面试官眼里,什么样的深拷贝才算合格?
  • 什么样的深拷贝能让面试官感到惊艳?

本文由浅入深,带你一步一步实现一个惊艳面试官的深拷贝。

本文测试代码:https://github.com/ConardLi/C...

例如:代码clone到本地后,执行 node clone1.test.js查看测试结果。

建议结合测试代码一起阅读效果更佳。

深拷贝和浅拷贝的定义

深拷贝已经是一个老生常谈的话题了,也是现在前端面试的高频题目,但是令我吃惊的是有很多同学还没有搞懂深拷贝和浅拷贝的区别和定义。例如前几天给我提issue的同学:

很明显这位同学把拷贝和赋值搞混了,如果你还对赋值、对象在内存中的存储、变量和类型等等有什么疑问,可以看看我这篇文章:https://juejin.im/post/5cec1b...

你只要少搞明白拷贝赋值的区别。

我们来明确一下深拷贝和浅拷贝的定义:

浅拷贝:

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

深拷贝:

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

话不多说,浅拷贝就不再多说,下面我们直入正题:

乞丐版

在不使用第三方库的情况下,我们想要深拷贝一个对象,用的最多的就是下面这个方法。

JSON.parse(JSON.stringify());

这种写法非常简单,而且可以应对大部分的应用场景,但是它还是有很大缺陷的,比如拷贝其他引用类型、拷贝函数、循环引用等情况。

显然,面试时你只说出这样的方法是一定不会合格的。

接下来,我们一起来手动实现一个深拷贝方法。

基础版本

如果是浅拷贝的话,我们可以很容易写出下面的代码:

function clone(target) {
    let cloneTarget = {};
    for (const key in target) {
        cloneTarget[key] = target[key];
    }
    return cloneTarget;
};

创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性依次添加到新对象上,返回。

如果是深拷贝的话,考虑到我们要拷贝的对象是不知道有多少层深度的,我们可以用递归来解决问题,稍微改写上面的代码:

  • 如果是原始类型,无需继续拷贝,直接返回
  • 如果是引用类型,创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性执行深拷贝后依次添加到新对象上。

很容易理解,如果有更深层次的对象可以继续递归直到属性为原始类型,这样我们就完成了一个最简单的深拷贝:

function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

我们可以打开测试代码中的clone1.test.js对下面的测试用例进行测试:

const target = {
    field1: 1,
    field2: undefined,
    field3: 'ConardLi',
    field4: {
        child: 'child',
        child2: {
            child2: 'child2'
        }
    }
};

执行结果:

这是一个最基础版本的深拷贝,这段代码可以让你向面试官展示你可以用递归解决问题,但是显然,他还有非常多的缺陷,比如,还没有考虑数组。

考虑数组

在上面的版本中,我们的初始化结果只考虑了普通的object,下面我们只需要把初始化代码稍微一变,就可以兼容数组了:

module.exports = function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

clone2.test.js中执行下面的测试用例:

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8]
};

执行结果:

OK,没有问题,你的代码又向合格迈进了一小步。

循环引用

我们执行下面这样一个测试用例:

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8]
};
target.target = target;

可以看到下面的结果:

很明显,因为递归进入死循环导致栈内存溢出了。

原因就是上面的对象存在循环引用的情况,即对象的属性间接或直接的引用了自身的情况:

解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构:

  • 检查map中有无克隆过的对象
  • 有 - 直接返回
  • 没有 - 将当前对象作为key,克隆对象作为value进行存储
  • 继续克隆
function clone(target, map = new Map()) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        if (map.get(target)) {
            return map.get(target);
        }
        map.set(target, cloneTarget);
        for (const key in target) {
            cloneTarget[key] = clone(target[key], map);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

再来执行上面的测试用例:

可以看到,执行没有报错,且target属性,变为了一个Circular类型,即循环应用的意思。

接下来,我们可以使用,WeakMap提代Map来使代码达到画龙点睛的作用。

function clone(target, map = new WeakMap()) {
    // ...
};

为什么要这样做呢?,先来看看WeakMap的作用:

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

什么是弱引用呢?

在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。

我们默认创建一个对象:const obj = {},就默认创建了一个强引用的对象,我们只有手动将obj = null,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。

举个例子:

如果我们使用Map的话,那么对象间是存在强引用关系的:

let obj = { name : 'ConardLi'}
const target = new Map();
target.set(obj,'code秘密花园');
obj = null;

虽然我们手动将obj,进行释放,然是target依然对obj存在强引用关系,所以这部分内存依然无法被释放。

再来看WeakMap

let obj = { name : 'ConardLi'}
const target = new WeakMap();
target.set(obj,'code秘密花园');
obj = null;

如果是WeakMap的话,targetobj存在的就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。

设想一下,如果我们要拷贝的对象非常庞大时,使用Map会对内存造成非常大的额外消耗,而且我们需要手动清除Map的属性才能释放这块内存,而WeakMap会帮我们巧妙化解这个问题。

我也经常在某些代码中看到有人使用WeakMap来解决循环引用问题,但是解释都是模棱两可的,当你不太了解WeakMap的真正作用时。我建议你也不要在面试中写这样的代码,结果只能是给自己挖坑,即使是准备面试,你写的每一行代码也都是需要经过深思熟虑并且非常明白的。

能考虑到循环引用的问题,你已经向面试官展示了你考虑问题的全面性,如果还能用WeakMap解决问题,并很明确的向面试官解释这样做的目的,那么你的代码在面试官眼里应该算是合格了。

性能优化

在上面的代码中,我们遍历数组和对象都使用了for in这种方式,实际上for in在遍历时效率是非常低的,我们来对比下常见的三种循环for、while、for in的执行效率:

可以看到,while的效率是最好的,所以,我们可以想办法把for in遍历改变为while遍历。

我们先使用while来实现一个通用的forEach遍历,iteratee是遍历的回掉函数,他可以接收每次遍历的valueindex两个参数:

function forEach(array, iteratee) {
    let index = -1;
    const length = array.length;
    while (++index < length) {
        iteratee(array[index], index);
    }
    return array;
}

下面对我们的cloen函数进行改写:当遍历数组时,直接使用forEach进行遍历,当遍历对象时,使用Object.keys取出所有的key进行遍历,然后在遍历时把forEach会调函数的value当作key使用:

function clone(target, map = new WeakMap()) {
    if (typeof target === 'object') {
        const isArray = Array.isArray(target);
        let cloneTarget = isArray ? [] : {};

        if (map.get(target)) {
            return map.get(target);
        }
        map.set(target, cloneTarget);

        const keys = isArray ? undefined : Object.keys(target);
        forEach(keys || target, (value, key) => {
            if (keys) {
                key = value;
            }
            cloneTarget[key] = clone2(target[key], map);
        });

        return cloneTarget;
    } else {
        return target;
    }
}

下面,我们执行clone4.test.js分别对上一个克隆函数和改写后的克隆函数进行测试:

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8],
    f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: {} } } } } } } } } } } },
};

target.target = target;

console.time();
const result = clone1(target);
console.timeEnd();

console.time();
const result2 = clone2(target);
console.timeEnd();

执行结果:

很明显,我们的性能优化是有效的。

到这里,你已经向面试官展示了,在写代码的时候你会考虑程序的运行效率,并且你具有通用函数的抽象能力。

其他数据类型

在上面的代码中,我们其实只考虑了普通的objectarray两种数据类型,实际上所有的引用类型远远不止这两个,还有很多,下面我们先尝试获取对象准确的类型。

合理的判断引用类型

首先,判断是否为引用类型,我们还需要考虑functionnull两种特殊的数据类型:

function isObject(target) {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
}
    if (!isObject(target)) {
        return target;
    }
    // ...

获取数据类型

我们可以使用toString来获取准确的引用类型:

每一个引用类型都有toString方法,默认情况下,toString()方法被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中type是对象的类型。

注意,上面提到了如果此方法在自定义对象中未被覆盖,toString才会达到预想的效果,事实上,大部分引用类型比如Array、Date、RegExp等都重写了toString方法。

我们可以直接调用Object原型上未被覆盖的toString()方法,使用call来改变this指向来达到我们想要的效果。

function getType(target) {
    return Object.prototype.toString.call(target);
}

下面我们抽离出一些常用的数据类型以便后面使用:

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';

在上面的集中类型中,我们简单将他们分为两类:

  • 可以继续遍历的类型
  • 不可以继续遍历的类型

我们分别为它们做不同的拷贝。

可继续遍历的类型

上面我们已经考虑的objectarray都属于可以继续遍历的类型,因为它们内存都还可以存储其他数据类型的数据,另外还有MapSet等都是可以继续遍历的类型,这里我们只考虑这四种,如果你有兴趣可以继续探索其他类型。

有序这几种类型还需要继续进行递归,我们首先需要获取它们的初始化数据,例如上面的[]{},我们可以通过拿到constructor的方式来通用的获取。

例如:const target = {}就是const target = new Object()的语法糖。另外这种方法还有一个好处:因为我们还使用了原对象的构造方法,所以它可以保留对象原型上的数据,如果直接使用普通的{},那么原型必然是丢失了的。

function getInit(target) {
    const Ctor = target.constructor;
    return new Ctor();
}

下面,我们改写clone函数,对可继续遍历的数据类型进行处理:

function clone(target, map = new WeakMap()) {

    // 克隆原始类型
    if (!isObject(target)) {
        return target;
    }

    // 初始化
    const type = getType(target);
    let cloneTarget;
    if (deepTag.includes(type)) {
        cloneTarget = getInit(target, type);
    }

    // 防止循环引用
    if (map.get(target)) {
        return map.get(target);
    }
    map.set(target, cloneTarget);

    // 克隆set
    if (type === setTag) {
        target.forEach(value => {
            cloneTarget.add(clone(value,map));
        });
        return cloneTarget;
    }

    // 克隆map
    if (type === mapTag) {
        target.forEach((value, key) => {
            cloneTarget.set(key, clone(value,map));
        });
        return cloneTarget;
    }

    // 克隆对象和数组
    const keys = type === arrayTag ? undefined : Object.keys(target);
    forEach(keys || target, (value, key) => {
        if (keys) {
            key = value;
        }
        cloneTarget[key] = clone(target[key], map);
    });

    return cloneTarget;
}

我们执行clone5.test.js对下面的测试用例进行测试:

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8],
    empty: null,
    map,
    set,
};

执行结果:

没有问题,里大功告成又进一步,下面我们继续处理其他类型:

不可继续遍历的类型

其他剩余的类型我们把它们统一归类成不可处理的数据类型,我们依次进行处理:

BoolNumberStringStringDateError这几种类型我们都可以直接用构造函数和原始数据创建一个新对象:

function cloneOtherType(targe, type) {
    const Ctor = targe.constructor;
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
            return new Ctor(targe);
        case regexpTag:
            return cloneReg(targe);
        case symbolTag:
            return cloneSymbol(targe);
        default:
            return null;
    }
}

克隆Symbol类型:

function cloneSymbol(targe) {
    return Object(Symbol.prototype.valueOf.call(targe));
}

克隆正则:

function cloneReg(targe) {
    const reFlags = /\w*$/;
    const result = new targe.constructor(targe.source, reFlags.exec(targe));
    result.lastIndex = targe.lastIndex;
    return result;
}

实际上还有很多数据类型我这里没有写到,有兴趣的话可以继续探索实现一下。

能写到这里,面试官已经看到了你考虑问题的严谨性,你对变量和类型的理解,对JS API的熟练程度,相信面试官已经开始对你刮目相看了。

克隆函数

最后,我把克隆函数单独拎出来了,实际上克隆函数是没有实际应用场景的,两个对象使用一个在内存中处于同一个地址的函数也是没有任何问题的,我特意看了下lodash对函数的处理:

 const isFunc = typeof value == 'function'
 if (isFunc || !cloneableTags[tag]) {
        return object ? value : {}
 }

可见这里如果发现是函数的话就会直接返回了,没有做特殊的处理,但是我发现不少面试官还是热衷于问这个问题的,而且据我了解能写出来的少之又少。。。

实际上这个方法并没有什么难度,主要就是考察你对基础的掌握扎实不扎实。

首先,我们可以通过prototype来区分下箭头函数和普通函数,箭头函数是没有prototype的。

我们可以直接使用eval和函数字符串来重新生成一个箭头函数,注意这种方法是不适用于普通函数的。

我们可以使用正则来处理普通函数:

分别使用正则取出函数体和函数参数,然后使用new Function ([arg1[, arg2[, ...argN]],] functionBody)构造函数重新构造一个新的函数:

function cloneFunction(func) {
    const bodyReg = /(?<={)(.|\n)+(?=})/m;
    const paramReg = /(?<=\().+(?=\)\s+{)/;
    const funcString = func.toString();
    if (func.prototype) {
        console.log('普通函数');
        const param = paramReg.exec(funcString);
        const body = bodyReg.exec(funcString);
        if (body) {
            console.log('匹配到函数体:', body[0]);
            if (param) {
                const paramArr = param[0].split(',');
                console.log('匹配到参数:', paramArr);
                return new Function(...paramArr, body[0]);
            } else {
                return new Function(body[0]);
            }
        } else {
            return null;
        }
    } else {
        return eval(funcString);
    }
}

最后,我们再来执行clone6.test.js对下面的测试用例进行测试:

const map = new Map();
map.set('key', 'value');
map.set('ConardLi', 'code秘密花园');

const set = new Set();
set.add('ConardLi');
set.add('code秘密花园');

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8],
    empty: null,
    map,
    set,
    bool: new Boolean(true),
    num: new Number(2),
    str: new String(2),
    symbol: Object(Symbol(1)),
    date: new Date(),
    reg: /\d+/,
    error: new Error(),
    func1: () => {
        console.log('code秘密花园');
    },
    func2: function (a, b) {
        return a + b;
    }
};

执行结果:

最后

为了更好的阅读,我们用一张图来展示上面所有的代码:

完整代码:https://github.com/ConardLi/C...

可见,一个小小的深拷贝还是隐藏了很多的知识点的。

千万不要以最低的要求来要求自己,如果你只是为了应付面试中的一个题目,那么你可能只会去准备上面最简陋的深拷贝的方法。

但是面试官考察你的目的是全方位的考察你的思维能力,如果你写出上面的代码,可以体现你多方位的能力:

  • 基本实现

    • 递归能力
  • 循环引用

    • 考虑问题的全面性
    • 理解weakmap的真正意义
  • 多种类型

    • 考虑问题的严谨性
    • 创建各种引用类型的方法,JS API的熟练程度
    • 准确的判断数据类型,对数据类型的理解程度
  • 通用遍历:

    • 写代码可以考虑性能优化
    • 了解集中遍历的效率
    • 代码抽象能力
  • 拷贝函数:

    • 箭头函数和普通函数的区别
    • 正则表达式熟练程度

看吧,一个小小的深拷贝能考察你这么多的能力,如果面试官看到这样的代码,怎么能够不惊艳呢?

其实面试官出的所有题目你都可以用这样的思路去考虑。不要为了应付面试而去背一些代码,这样在有经验的面试官面前会都会暴露出来。你写的每一段代码都要经过深思熟虑,为什么要这样用,还能怎么优化...这样才能给面试官展现一个最好的你。

参考

小结

希望看完本篇文章能对你有如下帮助:

  • 理解深浅拷贝的真正意义
  • 能整我深拷贝的各个要点,对问题进行深入分析
  • 可以手写一个比较完整的深拷贝

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。

想阅读更多优质文章、可关注我的github博客,你的star✨、点赞和关注是我持续创作的动力!

推荐关注我的微信公众号【code秘密花园】,每天推送高质量文章,我们一起交流成长。

图片描述

查看原文

赞 102 收藏 75 评论 21

崔学森 收藏了文章 · 10月14日

如何写出一个惊艳面试官的深拷贝?

导读

最近经常看到很多JavaScript手写代码的文章总结,里面提供了很多JavaScript Api的手写实现。

里面的题目实现大多类似,而且说实话很多代码在我看来是非常简陋的,如果我作为面试官,看到这样的代码,在我心里是不会合格的,本篇文章我拿最简单的深拷贝来讲一讲。

看本文之前先问自己三个问题:

  • 你真的理解什么是深拷贝吗?
  • 在面试官眼里,什么样的深拷贝才算合格?
  • 什么样的深拷贝能让面试官感到惊艳?

本文由浅入深,带你一步一步实现一个惊艳面试官的深拷贝。

本文测试代码:https://github.com/ConardLi/C...

例如:代码clone到本地后,执行 node clone1.test.js查看测试结果。

建议结合测试代码一起阅读效果更佳。

深拷贝和浅拷贝的定义

深拷贝已经是一个老生常谈的话题了,也是现在前端面试的高频题目,但是令我吃惊的是有很多同学还没有搞懂深拷贝和浅拷贝的区别和定义。例如前几天给我提issue的同学:

很明显这位同学把拷贝和赋值搞混了,如果你还对赋值、对象在内存中的存储、变量和类型等等有什么疑问,可以看看我这篇文章:https://juejin.im/post/5cec1b...

你只要少搞明白拷贝赋值的区别。

我们来明确一下深拷贝和浅拷贝的定义:

浅拷贝:

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

深拷贝:

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

话不多说,浅拷贝就不再多说,下面我们直入正题:

乞丐版

在不使用第三方库的情况下,我们想要深拷贝一个对象,用的最多的就是下面这个方法。

JSON.parse(JSON.stringify());

这种写法非常简单,而且可以应对大部分的应用场景,但是它还是有很大缺陷的,比如拷贝其他引用类型、拷贝函数、循环引用等情况。

显然,面试时你只说出这样的方法是一定不会合格的。

接下来,我们一起来手动实现一个深拷贝方法。

基础版本

如果是浅拷贝的话,我们可以很容易写出下面的代码:

function clone(target) {
    let cloneTarget = {};
    for (const key in target) {
        cloneTarget[key] = target[key];
    }
    return cloneTarget;
};

创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性依次添加到新对象上,返回。

如果是深拷贝的话,考虑到我们要拷贝的对象是不知道有多少层深度的,我们可以用递归来解决问题,稍微改写上面的代码:

  • 如果是原始类型,无需继续拷贝,直接返回
  • 如果是引用类型,创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性执行深拷贝后依次添加到新对象上。

很容易理解,如果有更深层次的对象可以继续递归直到属性为原始类型,这样我们就完成了一个最简单的深拷贝:

function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

我们可以打开测试代码中的clone1.test.js对下面的测试用例进行测试:

const target = {
    field1: 1,
    field2: undefined,
    field3: 'ConardLi',
    field4: {
        child: 'child',
        child2: {
            child2: 'child2'
        }
    }
};

执行结果:

这是一个最基础版本的深拷贝,这段代码可以让你向面试官展示你可以用递归解决问题,但是显然,他还有非常多的缺陷,比如,还没有考虑数组。

考虑数组

在上面的版本中,我们的初始化结果只考虑了普通的object,下面我们只需要把初始化代码稍微一变,就可以兼容数组了:

module.exports = function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

clone2.test.js中执行下面的测试用例:

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8]
};

执行结果:

OK,没有问题,你的代码又向合格迈进了一小步。

循环引用

我们执行下面这样一个测试用例:

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8]
};
target.target = target;

可以看到下面的结果:

很明显,因为递归进入死循环导致栈内存溢出了。

原因就是上面的对象存在循环引用的情况,即对象的属性间接或直接的引用了自身的情况:

解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构:

  • 检查map中有无克隆过的对象
  • 有 - 直接返回
  • 没有 - 将当前对象作为key,克隆对象作为value进行存储
  • 继续克隆
function clone(target, map = new Map()) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        if (map.get(target)) {
            return map.get(target);
        }
        map.set(target, cloneTarget);
        for (const key in target) {
            cloneTarget[key] = clone(target[key], map);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

再来执行上面的测试用例:

可以看到,执行没有报错,且target属性,变为了一个Circular类型,即循环应用的意思。

接下来,我们可以使用,WeakMap提代Map来使代码达到画龙点睛的作用。

function clone(target, map = new WeakMap()) {
    // ...
};

为什么要这样做呢?,先来看看WeakMap的作用:

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

什么是弱引用呢?

在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。

我们默认创建一个对象:const obj = {},就默认创建了一个强引用的对象,我们只有手动将obj = null,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。

举个例子:

如果我们使用Map的话,那么对象间是存在强引用关系的:

let obj = { name : 'ConardLi'}
const target = new Map();
target.set(obj,'code秘密花园');
obj = null;

虽然我们手动将obj,进行释放,然是target依然对obj存在强引用关系,所以这部分内存依然无法被释放。

再来看WeakMap

let obj = { name : 'ConardLi'}
const target = new WeakMap();
target.set(obj,'code秘密花园');
obj = null;

如果是WeakMap的话,targetobj存在的就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。

设想一下,如果我们要拷贝的对象非常庞大时,使用Map会对内存造成非常大的额外消耗,而且我们需要手动清除Map的属性才能释放这块内存,而WeakMap会帮我们巧妙化解这个问题。

我也经常在某些代码中看到有人使用WeakMap来解决循环引用问题,但是解释都是模棱两可的,当你不太了解WeakMap的真正作用时。我建议你也不要在面试中写这样的代码,结果只能是给自己挖坑,即使是准备面试,你写的每一行代码也都是需要经过深思熟虑并且非常明白的。

能考虑到循环引用的问题,你已经向面试官展示了你考虑问题的全面性,如果还能用WeakMap解决问题,并很明确的向面试官解释这样做的目的,那么你的代码在面试官眼里应该算是合格了。

性能优化

在上面的代码中,我们遍历数组和对象都使用了for in这种方式,实际上for in在遍历时效率是非常低的,我们来对比下常见的三种循环for、while、for in的执行效率:

可以看到,while的效率是最好的,所以,我们可以想办法把for in遍历改变为while遍历。

我们先使用while来实现一个通用的forEach遍历,iteratee是遍历的回掉函数,他可以接收每次遍历的valueindex两个参数:

function forEach(array, iteratee) {
    let index = -1;
    const length = array.length;
    while (++index < length) {
        iteratee(array[index], index);
    }
    return array;
}

下面对我们的cloen函数进行改写:当遍历数组时,直接使用forEach进行遍历,当遍历对象时,使用Object.keys取出所有的key进行遍历,然后在遍历时把forEach会调函数的value当作key使用:

function clone(target, map = new WeakMap()) {
    if (typeof target === 'object') {
        const isArray = Array.isArray(target);
        let cloneTarget = isArray ? [] : {};

        if (map.get(target)) {
            return map.get(target);
        }
        map.set(target, cloneTarget);

        const keys = isArray ? undefined : Object.keys(target);
        forEach(keys || target, (value, key) => {
            if (keys) {
                key = value;
            }
            cloneTarget[key] = clone2(target[key], map);
        });

        return cloneTarget;
    } else {
        return target;
    }
}

下面,我们执行clone4.test.js分别对上一个克隆函数和改写后的克隆函数进行测试:

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8],
    f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: {} } } } } } } } } } } },
};

target.target = target;

console.time();
const result = clone1(target);
console.timeEnd();

console.time();
const result2 = clone2(target);
console.timeEnd();

执行结果:

很明显,我们的性能优化是有效的。

到这里,你已经向面试官展示了,在写代码的时候你会考虑程序的运行效率,并且你具有通用函数的抽象能力。

其他数据类型

在上面的代码中,我们其实只考虑了普通的objectarray两种数据类型,实际上所有的引用类型远远不止这两个,还有很多,下面我们先尝试获取对象准确的类型。

合理的判断引用类型

首先,判断是否为引用类型,我们还需要考虑functionnull两种特殊的数据类型:

function isObject(target) {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
}
    if (!isObject(target)) {
        return target;
    }
    // ...

获取数据类型

我们可以使用toString来获取准确的引用类型:

每一个引用类型都有toString方法,默认情况下,toString()方法被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中type是对象的类型。

注意,上面提到了如果此方法在自定义对象中未被覆盖,toString才会达到预想的效果,事实上,大部分引用类型比如Array、Date、RegExp等都重写了toString方法。

我们可以直接调用Object原型上未被覆盖的toString()方法,使用call来改变this指向来达到我们想要的效果。

function getType(target) {
    return Object.prototype.toString.call(target);
}

下面我们抽离出一些常用的数据类型以便后面使用:

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';

在上面的集中类型中,我们简单将他们分为两类:

  • 可以继续遍历的类型
  • 不可以继续遍历的类型

我们分别为它们做不同的拷贝。

可继续遍历的类型

上面我们已经考虑的objectarray都属于可以继续遍历的类型,因为它们内存都还可以存储其他数据类型的数据,另外还有MapSet等都是可以继续遍历的类型,这里我们只考虑这四种,如果你有兴趣可以继续探索其他类型。

有序这几种类型还需要继续进行递归,我们首先需要获取它们的初始化数据,例如上面的[]{},我们可以通过拿到constructor的方式来通用的获取。

例如:const target = {}就是const target = new Object()的语法糖。另外这种方法还有一个好处:因为我们还使用了原对象的构造方法,所以它可以保留对象原型上的数据,如果直接使用普通的{},那么原型必然是丢失了的。

function getInit(target) {
    const Ctor = target.constructor;
    return new Ctor();
}

下面,我们改写clone函数,对可继续遍历的数据类型进行处理:

function clone(target, map = new WeakMap()) {

    // 克隆原始类型
    if (!isObject(target)) {
        return target;
    }

    // 初始化
    const type = getType(target);
    let cloneTarget;
    if (deepTag.includes(type)) {
        cloneTarget = getInit(target, type);
    }

    // 防止循环引用
    if (map.get(target)) {
        return map.get(target);
    }
    map.set(target, cloneTarget);

    // 克隆set
    if (type === setTag) {
        target.forEach(value => {
            cloneTarget.add(clone(value,map));
        });
        return cloneTarget;
    }

    // 克隆map
    if (type === mapTag) {
        target.forEach((value, key) => {
            cloneTarget.set(key, clone(value,map));
        });
        return cloneTarget;
    }

    // 克隆对象和数组
    const keys = type === arrayTag ? undefined : Object.keys(target);
    forEach(keys || target, (value, key) => {
        if (keys) {
            key = value;
        }
        cloneTarget[key] = clone(target[key], map);
    });

    return cloneTarget;
}

我们执行clone5.test.js对下面的测试用例进行测试:

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8],
    empty: null,
    map,
    set,
};

执行结果:

没有问题,里大功告成又进一步,下面我们继续处理其他类型:

不可继续遍历的类型

其他剩余的类型我们把它们统一归类成不可处理的数据类型,我们依次进行处理:

BoolNumberStringStringDateError这几种类型我们都可以直接用构造函数和原始数据创建一个新对象:

function cloneOtherType(targe, type) {
    const Ctor = targe.constructor;
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
            return new Ctor(targe);
        case regexpTag:
            return cloneReg(targe);
        case symbolTag:
            return cloneSymbol(targe);
        default:
            return null;
    }
}

克隆Symbol类型:

function cloneSymbol(targe) {
    return Object(Symbol.prototype.valueOf.call(targe));
}

克隆正则:

function cloneReg(targe) {
    const reFlags = /\w*$/;
    const result = new targe.constructor(targe.source, reFlags.exec(targe));
    result.lastIndex = targe.lastIndex;
    return result;
}

实际上还有很多数据类型我这里没有写到,有兴趣的话可以继续探索实现一下。

能写到这里,面试官已经看到了你考虑问题的严谨性,你对变量和类型的理解,对JS API的熟练程度,相信面试官已经开始对你刮目相看了。

克隆函数

最后,我把克隆函数单独拎出来了,实际上克隆函数是没有实际应用场景的,两个对象使用一个在内存中处于同一个地址的函数也是没有任何问题的,我特意看了下lodash对函数的处理:

 const isFunc = typeof value == 'function'
 if (isFunc || !cloneableTags[tag]) {
        return object ? value : {}
 }

可见这里如果发现是函数的话就会直接返回了,没有做特殊的处理,但是我发现不少面试官还是热衷于问这个问题的,而且据我了解能写出来的少之又少。。。

实际上这个方法并没有什么难度,主要就是考察你对基础的掌握扎实不扎实。

首先,我们可以通过prototype来区分下箭头函数和普通函数,箭头函数是没有prototype的。

我们可以直接使用eval和函数字符串来重新生成一个箭头函数,注意这种方法是不适用于普通函数的。

我们可以使用正则来处理普通函数:

分别使用正则取出函数体和函数参数,然后使用new Function ([arg1[, arg2[, ...argN]],] functionBody)构造函数重新构造一个新的函数:

function cloneFunction(func) {
    const bodyReg = /(?<={)(.|\n)+(?=})/m;
    const paramReg = /(?<=\().+(?=\)\s+{)/;
    const funcString = func.toString();
    if (func.prototype) {
        console.log('普通函数');
        const param = paramReg.exec(funcString);
        const body = bodyReg.exec(funcString);
        if (body) {
            console.log('匹配到函数体:', body[0]);
            if (param) {
                const paramArr = param[0].split(',');
                console.log('匹配到参数:', paramArr);
                return new Function(...paramArr, body[0]);
            } else {
                return new Function(body[0]);
            }
        } else {
            return null;
        }
    } else {
        return eval(funcString);
    }
}

最后,我们再来执行clone6.test.js对下面的测试用例进行测试:

const map = new Map();
map.set('key', 'value');
map.set('ConardLi', 'code秘密花园');

const set = new Set();
set.add('ConardLi');
set.add('code秘密花园');

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8],
    empty: null,
    map,
    set,
    bool: new Boolean(true),
    num: new Number(2),
    str: new String(2),
    symbol: Object(Symbol(1)),
    date: new Date(),
    reg: /\d+/,
    error: new Error(),
    func1: () => {
        console.log('code秘密花园');
    },
    func2: function (a, b) {
        return a + b;
    }
};

执行结果:

最后

为了更好的阅读,我们用一张图来展示上面所有的代码:

完整代码:https://github.com/ConardLi/C...

可见,一个小小的深拷贝还是隐藏了很多的知识点的。

千万不要以最低的要求来要求自己,如果你只是为了应付面试中的一个题目,那么你可能只会去准备上面最简陋的深拷贝的方法。

但是面试官考察你的目的是全方位的考察你的思维能力,如果你写出上面的代码,可以体现你多方位的能力:

  • 基本实现

    • 递归能力
  • 循环引用

    • 考虑问题的全面性
    • 理解weakmap的真正意义
  • 多种类型

    • 考虑问题的严谨性
    • 创建各种引用类型的方法,JS API的熟练程度
    • 准确的判断数据类型,对数据类型的理解程度
  • 通用遍历:

    • 写代码可以考虑性能优化
    • 了解集中遍历的效率
    • 代码抽象能力
  • 拷贝函数:

    • 箭头函数和普通函数的区别
    • 正则表达式熟练程度

看吧,一个小小的深拷贝能考察你这么多的能力,如果面试官看到这样的代码,怎么能够不惊艳呢?

其实面试官出的所有题目你都可以用这样的思路去考虑。不要为了应付面试而去背一些代码,这样在有经验的面试官面前会都会暴露出来。你写的每一段代码都要经过深思熟虑,为什么要这样用,还能怎么优化...这样才能给面试官展现一个最好的你。

参考

小结

希望看完本篇文章能对你有如下帮助:

  • 理解深浅拷贝的真正意义
  • 能整我深拷贝的各个要点,对问题进行深入分析
  • 可以手写一个比较完整的深拷贝

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。

想阅读更多优质文章、可关注我的github博客,你的star✨、点赞和关注是我持续创作的动力!

推荐关注我的微信公众号【code秘密花园】,每天推送高质量文章,我们一起交流成长。

图片描述

查看原文

崔学森 收藏了文章 · 10月14日

前端该如何准备数据结构和算法?

一、导读

据我了解,前端程序员有相当一部分对“数据结构”和“算法”的基础概念都不是很清晰,这直接导致很多人在看到有关这部分的内容就会望而却步。

实际上,当你了解了“数据结构”和“算法”存在的真正意义,以及一些实际的应用场景,对它有了一个整体的认知之后,你可能会对它产生强烈的兴趣。当然,它带将带给你的收益也是相当可观的。

很多前端同学在看到“数据结构”和“算法”后会有一定的抵触心理,或者尝试去练习,但是被难倒,从而放弃。

这很大一部分原因是因为你还不够了解学习他们的意义,或者没有掌握合理的练习方法。

实际上,当你有了一定的目的性,并且有了合理的练习方法,再来学习这部分内容会变得得心应手。
å
在本文中,我就来分享一下我学习“数据结构”和“算法”的一些经验和方法。

后面我也会针对所有常见的数据结构和算法分类,进行全方位的梳理。

1.1 类别说明

数据结构和算法的种类非常之多,拿树举例,树的种类包括:二叉树、B树、B+树、Trie树、红黑树等等,本文只选择了二叉树。

对前端来讲,没有必要对某些比较偏的类型和解法多做了解,一是浪费宝贵的时间,二是应用的不多。

本文选择的数据结构和算法的类别均是出现频率最高,以及应用最广的类别。

1.2 题目说明

另外,做题时找对典型题目非常重要,可以让你更快速更高效的掌握知识,本文后面也会给出每种类型的典型题目供大家参考。

题目来源:

  • awesome-coding-js:我的前端算法开源项目,包括我做过的题目以及详细解析
  • leetcode
  • 剑指offer

另外,我会在后面长期更新一个前端算法的专栏,对每类数据结构和算法进行详细的讲解,敬请期待。

二、为什么要学习数据结构和算法

在学习某块内容之前,我们一定要首先明确为什么要学,而不是盲目的跟风。

这将更有利于你从学习的过程中获得收益,而且会为你的学习带来动力。

首先明确一点,学习数据结构和算法不一定就是记住二叉树、堆、栈、队列等的解题方法也不是死记硬背一些题目,如果你仅仅停留在这样的表面思想,那么你学习起来会非常痛苦。

2.1 解决问题的思想

计算机只是一个很冰冷的机器,你给他下发什么样的指令,它就能作出什么样的反应。

而开发工程师要做的是如何把实际的问题转化成计算机的指令,如何转化,来看看《数据结构》的经典说法:

设计出数据结构, 在施加以算法就行了。

所以,很重要的一点,数据结构和算法对建立解决问题的思想非常重要。

如果说 Java 是自动档轿车,C 就是手动档吉普。数据结构呢?是变速箱的工作原理。你完全可以不知道变速箱怎样工作,就把自动档的车子从 A 开到 B,而且未必就比懂得的人慢。写程序这件事,和开车一样,经验可以起到很大作用,但如果你不知道底层是怎么工作的,就永远只能开车,既不会修车,也不能造车。如果你对这两件事都不感兴趣也就罢了,数据结构懂得用就好。但若你此生在编程领域还有点更高的追求,数据结构是绕不开的课题。

2.2 面试

这是非常现实的一点,也是很多前端学习数据结构和算法的原因。

一般对待算法的态度会分为以下几类:

GoogleMicrosoft等知名外企在面试工程师时,算法是起决定性因素的,前端工程师也是一样,基本是每一轮都会考察,即使你有非常强的背景,也有可能因为一两道算法答的不好而与这样的企业失之交臂。

第二类,算法占重要因素的,国内的某些大厂在面试时,也会把数据结构和算法作为重要的参考因素,基本是面试必考,如果你达不到一定的要求,会直接挂掉。

第三类,起加分作用,很多公司不会把数据结构和算法作为硬性要求,但是也会象征性的出一些题目,当你把一道算法题答的很漂亮,这绝对是加分项。

可见,学好数据结构和算法对你跳槽更好的公司或者拿到更高的薪水,是非常重要的。

三、如何准备

了解了数据结构和算法的重要性,那么究竟该用什么样的方法去准备呢?

3.1 全方位了解

在学习和练习之前,你一定要对数据结构和算法做一个全方位的了解,对数据结构和算法的定义、分类做一个全面的理解,如果这部分做的不好,你在做题时将完全不知道你在做什么,从而陷入盲目寻找答案的过程,这个过程非常痛苦,而且往往收益甚微。

本文后面的章节,我会对常见的数据结构和算法做一个全方位的梳理。

3.2 分类练习

当你对数据结构和算法有了一个整体的认知之后,就可以开始练习了。

注意,一定是分类练习!分类练习!分类练习!重要的事情说三遍。

我曾见过非常多的同学带着一腔热血就开始刷题了,从leetcode第一题开始,刚开始往往非常有动力,可能还会发个朋友圈或者沸点什么的😅,然后就没有然后了。

因为前几题非常简单,可能会给你一定的自信,但是,按序号来的话,很快就会遇到hard。或者有的人,干脆只刷简单,先把所有的简单刷完。

但是,这样盲目的刷题,效果是非常差的,有可能你坚持下来,刷了几百道,也能有点效果,但是整个过程可能非常慢,而且效果远远没有分类练习要好。

所谓分类练习,即按每种类别练习,例如:这段时间只练习二叉树的题目,后面开始练习回溯算法的题目。

在开始练习之前,你往往还需要对这种具体的类别进行一个详细的了解,对其具体的定义、相关的概念和应用、可能出现的题目类型进行梳理,然后再开始。

3.3 定期回顾和总结

在对一个类型针对练习一些题目之后,你就可以发现一定的规律,某一些题目是这样解,另一些题目是那样解...这是一个很正常的现象,每种类型的题目肯定是存在一定规律的。

这时候就可以开始对此类题目进行总结了,针对此类问题,以及其典型的题目,发现的解题方法,进行总结。当下次你再遇到这种类型的题目,你就能很快想到解题思路,从而很快的解答。

所以,当你看到一个题目,首先你要想到它属于哪种数据结构或算法,然后要想到这是一个什么类型的问题,然后是此类问题的解决方法。

如果你看到一个新的问题还不能做到上面这样,那说明你对此类题目的掌握程度还不够,你还要多花一些经历来进行练习。

当然,后面我会把我在这部分的总结分享出来,帮助大家少走一些弯路。

3.4 题目的选择

关于题目来源,这里我推荐先看《剑指offer》,然后是leetcode,《剑指offer》上能找到非常多的典型题目,这对你发现和总结规律非常重要。看完再去刷leetcode你会发现更加轻松。

关于难度的选择, 这里我建议leetcode简单、中等难度即可,因为我们要做的是寻找规律,即掌握典型题目即可,当你掌握了这些规律,再去解一些hard的问题,也是可以的,只是多花些时间的问题。切忌不要一开始就在很多刁钻古怪的问题上耗费太多时间。

经过上面的方法,我在练习一段时间后,基本leetcode中等难度的问题可以在20minAC,另外在最近跳槽的过程中,基本所有的算法问题我都能很快的手写出来,或者很快的想到解题思路。希望大家在看到我的经验和方法后也能达到这样的效果,或者做的比我更好。

四、时间复杂度和空间复杂度

在开始学习之前,我们首先要搞懂时间复杂度和空间复杂度的概念,它们的高低共同决定着一段代码质量的好坏:

4.1 时间复杂度

一个算法的时间复杂度反映了程序运行从开始到结束所需要的时间。把算法中基本操作重复执行的次数(频度)作为算法的时间复杂度。

没有循环语句,记作O(1),也称为常数阶。只有一重循环,则算法的基本操作的执行频度与问题规模n呈线性增大关系,记作O(n),也叫线性阶。

常见的时间复杂度有:

  • O(1): Constant Complexity: Constant 常数复杂度
  • O(log n): Logarithmic Complexity: 对数复杂度
  • O(n): Linear Complexity: 线性时间复杂度
  • O(n^2): N square Complexity 平⽅方
  • O(n^3): N square Complexity ⽴立⽅方
  • O(2^n): Exponential Growth 指数
  • O(n!): Factorial 阶乘

4.2 空间复杂度

一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。

一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。

五、数据结构

数据结构这个词相信大家都不陌生,在很多场景下可能都听过,但你有没有考虑过“数据结构”究竟是一个什么东西呢?

数据结构即数据元素相互之间存在的一种和多种特定的关系集合。

一般你可以从两个维度来理解它,逻辑结构和存储结构。

5.1 逻辑结构

简单的来说逻辑结构就是数据之间的关系,逻辑结构大概统一的可以分成两种:线性结构、非线性结构。

线性结构:是一个有序数据元素的集合。 其中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的。

常用的线性结构有: 栈,队列,链表,线性表。

—非线性结构:各个数据元素不再保持在一个线性序列中,每个数据元素可能与零个或者多个其他数据元素发生联系。

常见的非线性结构有 二维数组,树等。

5.2 存储结构

逻辑结构指的是数据间的关系,而存储结构是逻辑结构用计算机语言的实现。常见的存储结构有顺序存储、链式存储、索引存储以及散列存储。

例如:数组在内存中的位置是连续的,它就属于顺序存储;链表是主动建立数据间的关联关系的,在内存中却不一定是连续的,它属于链式存储;还有顺序和逻辑上都不存在顺序关系,但是你可以通过一定的方式去放问它的哈希表,数据散列存储。

5.3 数据结构-二叉树

树是用来模拟具有树状结构性质的数据集合。根据它的特性可以分为非常多的种类,对于我们来讲,掌握二叉树这种结构就足够了,它也是树最简单、应用最广泛的种类。

二叉树是一种典型的树树状结构。如它名字所描述的那样,二叉树是每个节点最多有两个子树的树结构,通常子树被称作“左子树”和“右子树”。

5.3.1 二叉树遍历

重点中的重点,最好同时掌握递归和非递归版本,递归版本很容易书写,但是真正考察基本功的是非递归版本。
根据前序遍历和中序遍历的特点重建二叉树,逆向思维,很有意思的题目

5.3.2 二叉树的对称性

5.3.3 二叉搜索树

二叉搜索树是特殊的二叉树,考察二叉搜索树的题目一般都是考察二叉搜索树的特性,所以掌握好它的特性很重要。
  1. 若任意节点的左⼦子树不不空,则左⼦子树上所有结点的值均⼩小于它的 根结点的值;
  2. 若任意节点的右⼦子树不不空,则右⼦子树上所有结点的值均⼤大于它的 根结点的值;
  3. 任意节点的左、右⼦子树也分别为⼆二叉查找树。

5.3.4 二叉树的深度

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

平衡二叉树:左右子树深度之差大于1

5.4 数据结构-链表

用一组任意存储的单元来存储线性表的数据元素。一个对象存储着本身的值和下一个元素的地址。

  • 需要遍历才能查询到元素,查询慢。
  • 插入元素只需断开连接重新赋值,插入快。

链表在开发中也是经常用到的数据结构,React16Fiber Node连接起来形成的Fiber Tree, 就是个单链表结构。

5.4.1 基本应用

主要是对链表基本概念和特性的应用,如果基础概念掌握牢靠,此类问题即可迎刃而解

5.4.2 环类题目

环类题目即从判断一个单链表是否存在循环而扩展衍生的问题

5.4.3 双指针

双指针的思想在链表和数组中的题目都经常会用到,主要是利用两个或多个不同位置的指针,通过速度和方向的变换解决问题。
  • 两个指针从不同位置出发:一个从始端开始,另一个从末端开始;
  • 两个指针以不同速度移动:一个指针快一些,另一个指针慢一些。

对于单链表,因为我们只能在一个方向上遍历链表,所以第一种情景可能无法工作。然而,第二种情景,也被称为慢指针和快指针技巧,是非常有用的。

5.4.4 双向链表

双链还有一个引用字段,称为prev字段。有了这个额外的字段,您就能够知道当前结点的前一个结点。

5.5 数据结构-数组

数组是我们在开发中最常见到的数据结构了,用于按顺序存储元素的集合。但是元素可以随机存取,因为数组中的每个元素都可以通过数组索引来识别。插入和删除时要移动后续元素,还要考虑扩容问题,插入慢。

数组与日常的业务开发联系非常紧密,如何巧妙的用好数组是我们能否开发出高质量代码的关键。

5.5.1 双指针

上面链表中提到的一类题目,主要是利用两个或多个不同位置的指针,通过速度和方向的变换解决问题。注意这种技巧经常在排序数组中使用。

5.5.2 N数之和问题

非常常见的问题,基本上都是一个套路,主要考虑如何比暴利法降低时间复杂度,而且也会用到上面的双指针技巧

5.5.3 二维数组

建立一定的抽象建模能力,将实际中的很多问题进行抽象

5.5.4 数据统计

数组少不了的就是统计和计算,此类问题考察如何用更高效的方法对数组进行统计计算。

5.6 数据结构-栈和队列

在上面的数组中,我们可以通过索引随机访问元素,但是在某些情况下,我们可能要限制数据的访问顺序,于是有了两种限制访问顺序的数据结构:栈(后进后出)、队列(先进先出)

5.7 数据结构-哈希表

哈希的基本原理是将给定的键值转换为偏移地址来检索记录。

键转换为地址是通过一种关系(公式)来完成的,这就是哈希(散列)函数。

虽然哈希表是一种有效的搜索技术,但是它还有些缺点。两个不同的关键字,由于哈希函数值相同,因而被映射到同一表位置上。该现象称为冲突。发生冲突的两个关键字称为该哈希函数的同义词。

如何设计哈希函数以及如何避免冲突就是哈希表的常见问题。
好的哈希函数的选择有两条标准:
  • 1.简单并且能够快速计算
  • 2.能够在址空间中获取键的均匀人分布

例如下面的题目:

当用到哈希表时我们通常是要开辟一个额外空间来记录一些计算过的值,同时我们又要在下一次计算的过程中快速检索到它们,例如上面提到的两数之和、三数之和等都利用了这种思想。

5.8 数据结构-堆

堆的底层实际上是一棵完全二叉树,可以用数组实现

  • 每个的节点元素值不小于其子节点 - 最大堆
  • 每个的节点元素值不大于其子节点 - 最小堆
堆在处理某些特殊场景时可以大大降低代码的时间复杂度,例如在庞大的数据中找到最大的几个数或者最小的几个数,可以借助堆来完成这个过程。

六、算法

6.1 排序

排序或许是前端接触最多的算法了,很多人的算法之路是从一个冒泡排序开始的,排序的方法有非常多中,它们各自有各自的应用场景和优缺点,这里我推荐如下6种应用最多的排序方法,如果你有兴趣也可以研究下其他几种。

选择一个目标值,比目标值小的放左边,比目标值大的放右边,目标值的位置已排好,将左右两侧再进行快排。
将大序列二分成小序列,将小序列排序后再将排序后的小序列归并成大序列。
每次排序取一个最大或最小的数字放到前面的有序序列中。
将左侧序列看成一个有序序列,每次将一个数字插入该有序序列。插入时,从有序序列最右侧开始比较,若比较的数较大,后移一位。
循环数组,比较当前元素和下一个元素,如果当前元素比下一个元素大,向上冒泡。下一次循环继续上面的操作,不循环已经排序好的数。
创建一个大顶堆,大顶堆的堆顶一定是最大的元素。交换第一个元素和最后一个元素,让剩余的元素继续调整为大顶堆。从后往前以此和第一个元素交换并重新构建,排序完成。

6.2 二分查找

查找是计算机中最基本也是最有用的算法之一。 它描述了在有序集合中搜索特定值的过程。

二分查找维护查找空间的左、右和中间指示符,并比较查找目标或将查找条件应用于集合的中间值;如果条件不满足或值不相等,则清除目标不可能存在的那一半,并在剩下的一半上继续查找,直到成功为止。如果查以空的一半结束,则无法满足条件,并且无法找到目标。

6.3 递归

递归是一种解决问题的有效方法,在递归过程中,函数将自身作为子例程调用。

你可能想知道如何实现调用自身的函数。诀窍在于,每当递归函数调用自身时,它都会将给定的问题拆解为子问题。递归调用继续进行,直到到子问题无需进一步递归就可以解决的地步。

为了确保递归函数不会导致无限循环,它应具有以下属性:

  • 一个简单的基本案例 —— 能够不使用递归来产生答案的终止方案。
  • 一组规则,也称作递推关系,可将所有其他情况拆分到基本案例。

6.3.1 重复计算

一些问题使用递归考虑,思路是非常清晰的,但是却不推荐使用递归,例如下面的几个问题:

这几个问题使用递归都有一个共同的缺点,那就是包含大量的重复计算,如果递归层次比较深的话,直接会导致JS进程崩溃。

你可以使用记忆化的方法来避免重复计算,即开辟一个额外空间来存储已经计算过的值,但是这样又会浪费一定的内存空间。因此上面的问题一般会使用动态规划求解。

所以,在使用递归之前,一定要判断代码是否含有重复计算,如果有的话,不推荐使用递归。

递归是一种思想,而非一个类型,很多经典算法都是以递归为基础,因此这里就不再给出更多问题。

6.4 广度优先搜索

广度优先搜索(BFS)是一种遍历或搜索数据结构(如树或图)的算法,也可以在更抽象的场景中使用。

它的特点是越是接近根结点的结点将越早地遍历。

例如,我们可以使用 BFS 找到从起始结点到目标结点的路径,特别是最短路径。

BFS中,结点的处理顺序与它们添加到队列的顺序是完全相同的顺序,即先进先出,所以广度优先搜索一般使用队列实现。

6.5 深度优先搜索

和广度优先搜索一样,深度优先搜索(DFS)是用于在树/图中遍历/搜索的一种重要算法。

BFS 不同,更早访问的结点可能不是更靠近根结点的结点。因此,你在DFS 中找到的第一条路径可能不是最短路径。

DFS中,结点的处理顺序是完全相反的顺序,就像它们被添加到栈中一样,它是后进先出。所以深度优先搜索一般使用栈实现。

6.6 回溯算法

从解决问题每一步的所有可能选项里系统选择出一个可行的解决方案。

在某一步选择一个选项后,进入下一步,然后面临新的选项。重复选择,直至达到最终状态。

回溯法解决的问题的所有选项可以用树状结构表示。

  • 在某一步有n个可能的选项,该步骤可看作树中一个节点。
  • 节点每个选项看成节点连线,到达它的n个子节点。
  • 叶节点对应终结状态。
  • 叶节点满足约束条件,则为一个可行的解决方案。
  • 叶节点不满足约束条件,回溯到上一个节点,并尝试其他叶子节点。
  • 节点所有子节点均不满足条件,再回溯到上一个节点。
  • 所有状态均不能满足条件,问题无解。

回溯算法适合由多个步骤组成的问题,并且每个步骤都有多个选项。

6.7 动态规划

动态规划往往是最能有效考察算法和设计能力的题目类型,面对这类题目最重要的是抓住问题的阶段,了解每个阶段的状态,从而分析阶段之间的关系转化。

适用于动态规划的问题,需要满足最优子结构和无后效性,动态规划的求解过程,在于找到状态转移方程,进行自底向上的求解。

自底向上的求解,可以帮你省略大量的复杂计算,例如上面的斐波拉契数列,使用递归的话时间复杂度会呈指数型增长,而动态规划则让此算法的时间复杂度保持在O(n)

6.7.1 路径问题

6.7.2 买卖股票类问题

子序列问题

6.8 贪心算法

贪心算法:对问题求解的时候,总是做出在当前看来是最好的做法。

适用贪心算法的场景:问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。这种子问题最优解成为最优子结构

6.8.1 买卖股票类问题

6.8.2 货币选择问题

6.9 贪心算法、动态规划、回溯的区别

贪心算法与动态规划的不同在于它对每个子问题的解决方案都作出选择,不能回退,动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能,而回溯算法就是大量的重复计算来获得最优解。

有很多算法题目都是可以用这三种思想同时解答的,但是总有一种最适合的解法,这就需要不断的练习和总结来进行深入的理解才能更好的选择解决办法。

七、前端编码能力

这部分是与前端开发贴近最紧密的一部分了,在写业务代码的同时,我们也应该关心一些类库或框架的内部实现。

大多数情况下,我们在写业务的时候不需要手动实现这些轮子,但是它们非常考察一个前端程序员的编码功底,如果你有一定的算法和数据结构基础,很多源码看起来就非常简单。

下面我拣选了一些问题:

八、小结

本文的部分图片来源于网络,如有侵权,请联系我删除,谢谢。

本文并没有对每个点进行深入的分析,而是从为什么、怎么做、做什么的角度对数据结构和算法进行的全面分析(针对前端角度),希望看完本片文章能对你有如下帮助:

  • 对数据结构和算法建立一个较全面的认知体系
  • 掌握快速学习数据结构和算法的方法
  • 了解数据结构和算法的重要分类和经典题型

如果你还想更深入的学习数据结构和算法,请关注我的后续文章。

推荐我的算法总结:awesome-coding-jshttps://github.com/ConardLi/a...

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。

想阅读更多优质文章、可关注我的github博客,你的star✨、点赞和关注是我持续创作的动力!

推荐关注我的微信公众号【code秘密花园】,每天推送高质量文章,我们一起交流成长。

查看原文

崔学森 收藏了文章 · 10月9日

2019前端校招经历

  我是一个想要找前端工作的妹纸,最近电话面了挺多企业,在每次面试中都会发现自己的不足或者没涉及到的方面,一边被拒一边学习新知识。想要在这里记录、分享前端校招遇到的问题,希望每一次都有提升,下一次不会再犯上一次同样的错误。

持续更新哦 ^^

总结(秋招结束,写在最后)

百度(offer)
oppo(offer)
顺丰(offer)
海康(offer)
航信(offer)
美团(二面挂)
大华(终面完无offer)
中兴(终面无offer)
小米(电话面挂)
阿里(三次电话面挂)
京东(未去面试,遗憾)
网易(两次面试机会未去,遗憾)

以上是有了面试机会的概览。在秋招的过程中,有碰壁,有肯定,我也是在不断的面试中成长的,在面试中知道自己的不足,确定自己的方向,不断学习。结束秋招的最后一面的是百度的终面,如我所愿,作为一个通信非科班生,第一份工作能进互联网公司做我喜欢的前端,这几个月的努力也算是有所收获。

6.27 完美世界(重庆)实习电话面

  1. 自我介绍
  2. 项目:项目中到了什么技术,遇到过什么问题,从中学到了什么
  3. 为什么学习nodejs
  4. html标签的语义化是什么意思
  5. p标签、b标签、strong标签是什么意思?b和strong标签浏览器呈现效果都是一样的,它们有什么区别?
  6. localStorage是什么?localStage的内存有限制吗,一般浏览器内存是多少
  7. 在项目中css3或者css2你用到了哪些特性学到了哪些东西?CSS3动画是怎么实现?
  8. js原型继承

7.12 今日头条实习电话面

  1. this出现在那些场景
  2. TCP、UDP是什么?它们的实现过程
  3. 解释promise
  4. let和var的区别
  5. position都有哪些属性,具体作用
  6. 盒模型
  7. 解释flex布局
  8. js原型链

7.17 阿里巴巴实习电话面

  1. 如何实现轮播图
  2. 如何实现动画
  3. flex布局
  4. CSS选择器权重
  5. AJAX的原理
  6. 平时用了什么开发工具
  7. 了解什么开源项目,读过什么开源代码

  最后要了我的github地址、博客地址和作品集展示。

7.26 CVTE提前批电话面

之前做过线上笔试,除了选择题还做了两道编程题,附编程题目:

题目1:实现一个函数,输入参数为一个长度是2n的整数数组,以两个整数一组的方式将数据分组,并使每组数组最小值相加为最大值,输出这个最大值。

题目2:用flex实现三列布局,左右两列定宽、不伸缩分别为200px、120px,中间一列自适应。(题目大概是这样)

还有一些选择题中我不懂的知识点:CORS、FileReader

  1. 最近做什么项目,负责什么模块,遇到什么问题?
  2. 在项目中遇到过最棘手,花时间最多的问题是什么?
  3. js原型链
  4. 闭包是什么,作用和缺点
  5. Jsonp原理,优缺点
  6. 知道哪些http状态码
  7. 实现垂直居中的效果有哪些方法
  8. 了解前端优化吗?
  9. 冒泡排序,时间复杂度
  10. let、const作用、特性、区别
  11. 用过前端自动化构建的工具吗?webpack、gulp
  12. nodejs中express的作用
  13. 箭头函数?其中this

7.27 阿里巴巴提前批电话面

之前在线上做了编程测试,只有一道题,三十分钟。

题目:实现mergePromise函数,把传进去的数组顺序先后执行,并且把返回的数据先后放到数组data中.
  1. 从本科起介绍自己的学习经历
  2. 在做的项目中主要负责什么?项目选择的技术栈是什么?
    (其实自学小白一枚,问项目中的技术栈很痛苦的)
  3. 了解过react、vue吗?
  4. 用过什么前端自动化构建工具,前端自动化构建是什么原理?
  5. jsonp如何实现,解决什么问题
  6. js中浮点值精度问题:为什么0.1+0.2不等于0.3?在什么场景下遇到这个问题,如何解决?
  7. 如何获取0-9的随机数。
  8. mouseover和mouseenter的区别

  给我的建议就是,多了解下前端当前的MVVM流行框架,自动化构建工具这些,只用原生的东西,开发难度和开发效率都会大打折扣,而且现在公司也都有自己的技术栈,如果什么都不懂一切从零开始还是有一定的难度。

9.12 阿里秋招电话面

  1. 自我介绍
  2. 介绍做的项目
  3. bootstrap栅格布局实现原理
  4. vue数据双向绑定原理
  5. vue Dep
  6. vue父子组件之间的传值
  7. vue中props、data、computed的加载顺序
  8. vue中watcher观察者有哪些类型(data、watch、method)
  9. 开发中有用到vue-cli吗
  10. webpack配置中plugins和module节点原理
  11. vue-loader解析.vue的原理
  12. 知道哪几种图片格式
  13. webp的优势
  14. 图片懒加载的时候什么时间点把图片放到页面
  15. 前端优化有哪些方法
  16. 有移动端开发经验吗

  最后我问了面试官,应届生要具备哪些技能或者特质才能受到阿里hr的青睐,他给我列了一些技能:

  1. 有移动端开发经验(混合开发)
  2. 熟悉热门框架,并能了解其原理
  3. 前端优化技能
  4. 前端工程化开发:比如webpack
  5. 并且自己动手写过loader、plugin更好

9.13 鼎桥一面、群面、测评(非前端)

  校招,笔试做的是java卷,用JavaScript写的编程题竟然通过了。
  一面的时候好像也不在乎是不是java写的,主要看中个人能力,面试官是专门面java的,或许是没有想到跑进来一个做前端的,本来想把我带到了解前端的面试官那去,但是聊得很愉快,他照着我简历上的技能问了我一些问题,觉得我不错,就给通过了。
  二面是群面,不要求你表现特别突出,只要把想说的都说了,并且思维逻辑清晰,有条理,就过了。测评是纯主管题的性格测评。喜欢这种一天搞定的公司。等通知……

9.14 小米内推简历电话面

  最近面试比较多,记得不是很清晰了,尽量回忆……

  1. 自我介绍
  2. position定位有哪些属性
  3. 跨域是什么,怎么解决跨域问题
  4. 页面优化
  5. BFC

……
好像忘记了,没有立即写面经,印象模糊了……T-T

9.14、15 中兴一面、二面

  一面聊简历上的东西,由于是前端技术栈,感觉他们可问的也很少,问了vue和jQuery的区别,ajax是什么之类的概念性问题,闻不了深入的东西,主要是自己要展现自己有的和做的,让人觉得有用武之地。
  二面主要聊聊天,让我用英文介绍了自己的学校,但是其中一个面试官好像对我的技术不太满意,要求复查技术,但他也不懂前端,问我会不会java,软件测试,我都说不会;另一个面试官说一轮面已经面了技术,不用再面。之后我问了一些公司内部岗位调配的问题,然后就结束了,好像对我不是很感兴趣。等通知吧……

9.16 中国航信重庆研发中心(web前端)

  1. 创建对象有哪些方法
  2. js的继承方式有哪些
  3. js中的变量提升(说了var和let)
  4. 箭头函数和普通函数的区别
  5. 如何去抖动(说了定时器)
  6. 平时用到H5了吗
  7. 是只学习了vue还是用到了vue的整个技术栈
  8. vueX的原理(这个说了只会用不懂其间的原理)
  9. 数据的双向绑定用什么(v-model,应该主动说下双向绑定原理的TT)
  10. 子组件传值给父组件
  11. 闭包是什么?闭包的缺点?怎么解决内存溢出?

9.17 海康威视前端一面

  1. 自我介绍
  2. 介绍觉得最骄傲的项目吧(介绍了下用vue写的webAPP)
  3. vue有哪些优势(然后面试官说不管框架多好用原生的js还是要扎实……)
  4. 说说ES6有哪些新特性(balabala说了很多)
  5. 箭头函数的this与普通函数区别
  6. promise是什么,promise.then返回的是什么(顺便说了下解决回调地狱的问题)
  7. 了解模块化开发吗(说了下AMD、commonJs和ES6的模块化开发)
  8. 让我写出ES6中怎么去定义和引用模块
  9. webpack的作用
  10. CSS3的新特性有哪些(balabala)
  11. 说到css3的animation,问我能做过什么
  12. 让我写c3的web字体怎么定义
  13. 写更换图片的多种方法(可能主要考察对于基本的DOM操作的熟练程度)
  14. 因为简历上写了:对前后端联合开发的原理有较深的认识,问:解释下前后端联合开发
  15. git都会哪些操作(我说了很基本的push、pull、创建、查看、删除branch,diff),之后问我有没有遇到过分支冲突的情况,怎么解决……(没有遇到过,纯粹是自己用着玩,没有与人合作开发用git)
  16. 最后问我还有什么我准备了但是她还没问到的(TT我说了好多,SEO、前端页面优化、web安全……,感觉面试官都听疲倦了……可能死于话多)

9.18 百度前端一面(凉)

  感觉百度的一面主要考察基础,面了将近80分钟真的很长了……

  1. 介绍自己和一个项目
  2. 项目中遇到过什么问题,怎么解决的
  3. 说下自适应布局的原理(我提到@media媒体查询,又问媒体查询如何实现自适应布局)
  4. ajax的工作原理,以及原生js如何实现
  5. 了解ES6的哪些特性
  6. 箭头函数与普通函数的区别
  7. this有哪些指向
  8. 除了call、apply还有哪些改变函数this指向的方式(bind、with)
  9. call、apply、bind的区别
  10. 模板字符串可以换行吗,怎么换行
  11. http有哪些状态码,什么情况下返回304
  12. js代码可以放在哪些位置,它们的区别是什么,放在头部有哪些影响
  13. 闭包是什么,手写一个闭包,它的优缺点是什么,怎么解决闭包的内存溢出
  14. 了解哪些代理模式,说明它们是怎么实现的(单例模式、代理模式、发布订阅者模式……)
  15. 手写单例模式
  16. 说明原型链继承的原理
  17. 在知道宽高和不知道宽高的前提下元素的水平垂直居中如何实现,写出不同的方法
  18. button、和input中type为submit提交表单的区别是什么
  19. 知道正则表达式吗,说说"."、"+"、"?"、"*"、"w"都是什么意思;中括号[]是什么意思;
  20. 正则中哪些符号需要转义
  21. 写出数组去重的方法,不用数组的API该怎么实现?

  最后问了下hr小姐姐,百度在应届生中招前端主要看中什么特质或者技能,小姐姐说:首先得懂一些前端的基本知识,其次是一个人思考问题的深度,主要看的是这个人的能力。
  刚收到二面通知……居然没有凉。

9.19 百度前端二面

面了将近90分钟,全程手写代码,有将fs.readFile改写为promise、如何统计整个页面的标签及个数、用js和css画统计图……其中涉及到js和css及页面布局的问题,主要看面试官怎么面。

9.21 浙江大华一、二、hr面

不走心的面经:一面问了一些前端基本知识和项目中遇到的难点,二面让我介绍一个项目面试官从中引出问题,hr面让我对比大华和海康(因为他问我之前面了什么公司我提到了海康威视),自己给自己挖了坑,一直在问,有什么区别,最大的感受是什么,了解他们公司吗,如果同时收到海康和大华的offer你要怎么选择……

9.25 oppo一面

9.26 oppo二面、hr面

9.26 美团一面、二面(挂)

9.27 顺丰一面、hr面

9.27 百度三面

因为二十几号那几天强度真的很大,不停的面试不停的到处奔波,所以很多都记不起来了。但是,总觉得这一面挺重要的,尽量回忆下。

  1. 拿一个功能最完善的项目介绍
  2. 介绍项目中的难点和闪光点
  3. 面试官拿出一个手机百度专门为宝马设计的卡片让我分析
  4. 在这个宝马项目中前端要做的是什么
  5. 如何统计用户的点击量(302临时重定向)
  6. 平时如何自学前端
  7. 平时常去什么网站
  8. 觉得自己有什么不足,有哪些改进的方法
  9. 未来规划
  10. 一些常规问题,有拿到什么offer,去北京还是上海等等
查看原文

崔学森 收藏了文章 · 10月8日

要去大厂应该把这些面试题搞(bei)懂(hui)

阳春召我以烟景,大块假我以文章,掘金予我以面经 - 李白 春夜宴从弟桃李园序

这是我自己在仓库中的总结:https://github.com/shfshanyue...

高赞文章收藏

如果这些还不够,我在 github 上新建了一个仓库 日问,每天一道面试题,有关前端,后端,devops以及软技能,促进职业成长,敲开大厂之门,欢迎交流

勤学如春起之苗,不见其长,日有所增

并且记录我的面试经验

我是山月,可以加我微信 shanyue94 与我交流,备注交流。另外可以关注我的公众号【全栈成长之路】

如果你对全栈面试,前端工程化,graphql,devops,个人服务器运维以及微服务感兴趣的话,可以关注我

查看原文

认证与成就

  • 获得 0 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 9月15日
个人主页被 156 人浏览