冴羽

冴羽 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织 github.com/mqyqingfeng 编辑
编辑

JavaScript深入系列 15 篇已完结。

JavaScript专题系列 20 篇已完结。

underscore 系列 8 篇已完结。

ES6 系列 20 篇已完结。

现在写起了各种单篇……

React 系列还在懒惰中……

公众号:冴羽的JavaScript博客

个人动态

冴羽 发布了文章 · 2020-05-07

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

前言

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

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

1. 业务前端的困境

1.1 业务前端“好忙”

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

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

1.2 业务前端是“资源”?

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

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

1.3 业务前端想突破

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

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

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

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

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

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

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

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

2. 了解业务

2.1 业务和需求

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

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

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

2.2 前端为什么要学习业务

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

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

2.3 你了解业务吗?

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

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

2.4 如何学习业务?

2.4.1 业务领域知识的阅读

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

2.4.2 了解业务背景和规划

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

2.4.3 多交流

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

2.4.4 谨记数字

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

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

2.4.5 从日常需求入手

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

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

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

2.4.6 坚持

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

3. 助力业务

3.1 思考

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

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

3.2 沟通

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

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

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

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

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

3.3 技术规划

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

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

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

3.4 站在巨人的肩膀上

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

4. 技术深度

4.1 技术知识与技术能力

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

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

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

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

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

4.2 培养技术视野

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

4.3 技术深度

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

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

4.4 技术与业务共赢

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

5. 给你更多体感

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

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

查看原文

赞 45 收藏 19 评论 2

冴羽 发布了文章 · 2020-04-14

淘系前端校招负责人直播答疑文字实录

前言

3 月 25 日晚,面向 21 届学生,淘系前端团队举办直播活动,由淘系前端技术专家大果带来「淘系前端技术体系揭秘」和「校招问题答疑」,答疑环节更是邀请了淘系前端校招负责人、淘系高级技术专家元彦,直播回答问题,其中干货满满,冴羽辛苦的对答疑部分进行了文字整理,分享给大家,如果要观看这场直播,可以搜索「yayujs」,关注「冴羽的JavaScript博客」公众号,回复 "宝典" 获取视频回放。

1. flutter 在手淘有很多应用场景吗?

元彦:阿里一直在发展 flutter,像以闲鱼的同学为代表,就一直在社区布道和宣传,这是阿里在新技术上的态度,会一直做推进,也一起谋求共建。这是当下 flutter 在闲鱼的落地,而手淘这边,还是大量的处在探索阶段,这是因为手淘和闲鱼客户端并不在一个体量上,我相信今天大家也看到了很多手淘的问题,我觉得这个是客观的存在,所以我们在新技术的探索会做一些保守的策略,所以手淘我们会做很多新的探索,但当下并没有大量的落地。

2. 校招侧重哪些能力的考察?

元彦:对人的考察往往是因人而异的,毕竟每个人的背景、学校、经历都是不同的,比方说有些人非科班,可能到了大二大三的时候,突然发现前端很有意思,然后才去学习,而有些科班的同学,可能从一开始就已经接触了前端,在社团里也承担了前端相关的工作,所以每个人的差异其实是很大的。

但从整体来说,我们其实有一些基本的考量标准,这个可能是我们站在淘系前端的角度,第一次对外宣扬这个标准,不过我觉得这个标准其实也不应该是什么秘密,可能有些人还会觉得这是正确的废话,但是这就是我们评判一个人的标准,大家可以做一个参考。我觉得主要是三个点。

第一是基础,即基础扎不扎实,这个基础就是指计算机理论的基础,很多渠道都可以学习,比如学校的课程,非科班的同学也可以去选修 CS 相关的课程,当然如果是科班的同学,我们还希望他能把基础知识学扎实,尤其是数据结构、操作系统原理等这些最基本的东西,在未来工作中也是一定会用到的内容,这是一个最基础的事情,当然,有些同学会说,我要去学习 AI 人工智能,然后我觉得这是一个加分项,至少对前端这个岗位来说。因为当前的 AI 其实更多的还是在探索阶段,并没有在一个收割的阶段。我们见到很多学生,会去研究这些,这个是我们希望看到的,但是最基本的还是,基础一定要扎实。

第二个是热情。只关心自己的基础,肯定是不够的,至少你还要对社区的发展有观察或者说有参与。观察就比如说经常逛社区,这是一种热情的体现,今天在直播背后的很多同学,我相信很多人还会逛 Github,在 Github 里面大家有没有经常去参与一些讨论和建设,我觉得这个也是热情很重要的体现,我们也希望看到很多人说,对知名的开源项目提交过 PR,像我昨天面试的一个同学,他去年有为 ICE 这个开源项目提交过 PR,而且 PR 质量也很高,我觉得这个就是一个不错的亮点,也很能打动评委,说明今天你参与了一个大公司的开源项目或者社区知名开源项目,并且受到认可。当然我不是说让大家去提交一个文本错误之类的 PR,虽然这种也是好事情,的确体现了热情,但是可能没法去证明自己的实力,这个希望大家有一个正确的判断。所以,当你有去参与开源项目的时候,我们会认为这是一个非常有热情的的同学,这是我们希望看到的。

当然,最重要的,我觉得还是成果,成果就是你在这个领域做出的一些关键的结果,比如说,有些人将一些技术沉淀下来,出了一本书,或者,你写了一个库,大家都在使用它,而且你也一直在经营它,我觉得这是一个非常有意思的体验和成果,当然我觉得这些也离不开你在之前说的基础和热情,这三者也是相辅相成的。

所以总结来说,其实我们看重的就三个指标,一个是基础,就是你的基础知识,第二个是你的热情,你对技术上的热情,包括你对这个社区的热情,这都是热情。第三个是成果,就是你有没有一些作品。

所以每个人在写简历的时候,一定要想象自己的作品是什么,不过这个作品不是老师布置的作业,作品是自己定义的问题,自己提出了解决方案,作业是老师定义的问题,你提交了一个作业。这两者是有很大的区别的。

3. 如何系统的学习前端然后如何从前端开发晋升至前端专家?

大果:我结合着自己的经验来讲一下,首先是如何系统的学习前端,刚才元彦讲的那些标准,其实对不管是实习生还是社招,都是一样的。就是看你这个人的一些特质:你的基础能力,你对这个方向有没有热情,你有没有一些作品,这些都很重要。

这些特质其实回过头来看,在我 13 年应聘的时候,这三个是不可能都达到的,可能就只达到了一个热情,当时整个实习面试也是非常的坎坷,最后也是到了大三的暑假了,然后在家里才通过一次补招,最后拿到阿里实习的 offer。

不过今天,我们对于实习生的要求,其实已经比我当年高很多了,因为不管是大家的学习能力,还是社区的发展,都一直往前走。那怎么系统的学习呢,我觉得首先还是基础能力这一块,大家千万不要去忽视。很多人也在说,今天前端领域很多,我怎么去把一个领域做深,所以首先第一个是,我们得有这样一个想法,就是我今天不求说,所有的东西都要会,但我一定要能说,我们在某个领域是足够深入的,基础也就是指这一块。基础是你往后发展的一个基石,如果这一块的东西你不打扎实的话,你未来在任何的一个领域,其实都很难去做深的,因为你很容易就遇到一些瓶颈。有很多同学说,Flutter 出来了,我可能比较了解 Flutter,那我会就会问,你做了什么呢?然而很多人可能就跑了一个 Demo,打印个 hello world,然后模拟器跑起来,那这个显然是不符合我们的要求,所以对于基础这一块,大家一定要去关注。

然后第二个就是作品, 不过今天很多可能都是校招生,可能还没有一些业务的驱动,对于作品这个概念,可能不是很好理解,那我觉得学校里,比如说实验室的老师让我们去做的一些项目,其实就是很好的去展示自己作品的机会。

然后另一块就是今天整个前端开源社区,其实也是非常开放的,发展也很快,大家可以对开源社区多去做一些关注,了解别人在做些什么,社区在发展什么,然后慢慢的参与进去,这个我觉得也是一个作品。所以作品不一定说我要做一个 React,我要做一个 Vue 。我对 React 提交一个 PR,这些也都是作品。

然后怎么晋升至前端专家,晋升其实是一个水到渠成的过程,没有说今天我给你安排好一条道路,你就按照这样的道路去走,你就能晋升,那显然不是的。刚开始我可能会做业务,那在做业务的阶段里面,热情很重要,我要有自己的热情和积极性,可以去看到业务的问题,看到架构的问题,虽然可能这个架构现在还不是我在做,那可能是其他团队的任务,但是我在里面可以看到他们的问题,并且逐渐的去给他解决问题,那这中间就是我自己去发挥的一个能力,也能让别人认识到我有这个能力,那慢慢的,你可能就会做一些相关的事情,逐渐的,整个技术深度包括你对业务的一些理解,都会逐渐成长,最后到晋升,其实都是一个水到渠成的过程,所以这里面热情、积极性很重要。

然后另一块就是要多去输出,不管是学生,还是说今天已经工作的同学,很多时候我们都在被输入这个,输入那个,比如说业务给我们输入什么,整个社区给我们输入什么,老板在跟我们输入什么,他们告诉我们,要去做什么什么样的事情。但很多输入可能我们自己并没有去消化,把他沉淀成自己的能力,所以我们要通过输出去把这些东西转换成自己能力。所以我面试的时候经常会问,有写一些博客或者去做一些开源项目之类的分享之类的事情嘛,我觉得这些都是一些很好的输出的场子。通过每次的输出,也是对自己这一块的技术体系的一个沉淀和抽象,把它真正的转换成自己所理解的东西。

4. 前端对非计算机专业有歧视吗?

大果:这个问题还是要抽象一下,应该是说,今天我们对于计算机或者非计算机专业的同学,有没有区别对待之类的?那这个问题元彦来回答吧。

元彦:这个答案肯定是没有的。其实我个人在面试过程中,对于非科班的毕业生更有好感。为什么这么说,因为今天你作为非科班,你去应聘技术岗位的时候,你相关的学习都是自驱的,你是基于兴趣去学习的,不是因为学校里有这些课程才去学习的,你学习的过程是完全自己驱动的,未来在工作中,更多的事情也不是自上而下布置的,很多事情都是要靠你自己主动去学习的,我觉得一旦你养成了这个学习习惯,尤其在工作中应用到,一定会把工作做的更好,所以我才会对非科班的学生更有好感。

5. 面试的时候不会的问题,应该怎么回答?

元彦:面试中遇到问题不会回答,这是非常正常的,毕竟大家在学习过程中遇到的问题,跟我们在工作中遇到的问题是非常不一样的,除此之外,我们看问题的角度,也是有差别很大的。

举个例子,很多人都在看 React Fiber,但是如果问你们,为什么要有 Fiber 这个东西,可能很多人都回答不上来,因为大家只在社区里面看到说,Fiber 是怎么工作的啊,但是为什么要有 Fiber?如果没有会怎样,而这种思考,其实是非常底层的。我相信,很多人可能都没有思考过。

那如果遇到的问题不会,你可以先选择不会,说我觉得自己没有信心能答出这个问题,当然我更希望听到你说,我现在不会,但是你给我两分钟思考一下,我想一下这个问题有没有答案?我觉得这是一个很好的思考习惯,首先你在面对不会的东西是坦然的,我当下不会,很正常,我之前没有思考过这个问题嘛,那我们再花两分钟思考一下,给一个初步的答案。

所以说遇到问题,也不用不会就不会,也可以有一个积极的方式回应。

6. 前端实习生一般做什么工作?

大果:我自己也是实习过来的,在我们进来之前,每个人就会分配一个师兄,师兄可能会提前跟你联系,帮助你解决来杭州、租房之类的问题,入职之后,师兄会带着你去了解整个工作中依赖的一些东西,就比如说我刚才讲的一些工程的东西,可能需要去学习和了解的一些东西,那在这个阶段过去之后呢,接下来就是分配一些实质性的内容,我们给到实习生的工作内容也是非常正式的。因为我们其实是希望实习生能承担起跟正式员工一样的工作,就没有说今天你是一个实习生,就给你分配一些简单的工作。接下来实习生实习了一段时间之后,还有实习转正,review 你在整个实习过程中做的事情,包括你自己在里面的思考。

元彦:我再补充几点,我个人在两家公司做过实习生,这一家是在阿里,上一家是在深圳的一个公司,我觉得这两家公司的实习还是有很大的差别的。这个我实习的时候有点久远,可能差不多快九年多了,但我觉得这两家公司给我的感觉,还是很不太一样的。

阿里更多是把实习生当作一个正式员工来看待,会倾心的把所有知识交给你,把正式员工遇到的问题交给你。这个有好处,有坏处。好处是今天你跟大家一样,都是在一个水平线上做挑战,不是说今天你是一个实习生,就拿一些不重要的事情让你做。我确实听过很多人说,今天实习生过来只是个打杂的,其实很多实习生在很多公司真的学不到东西或者说学不到非常深入的东西。为什么学不到,可能是因为你面对的问题不是一个难题啊,只是说是一个资源的问题,这跟阿里确实有很大的差别。

另一个点,对于一个可能没有经验的人,他压力会比较大,这就是为什么会有师兄存在的原因。我们希望你遇到困难的时候,更多的去找人沟通、讨论,让师兄帮助你。而且你来到阿里,师兄很可能就是像大果这样的专家,甚至可能是高级专家去帮助你。

7. 前端应用面太广,怎么选择一项深入呢?

元彦:刚才我在看这些问题,有人提 Flutter、游戏、gcanvas、框架之类的,的确前端的技术非常多,但其实这些技术核心的点都是围绕体验的。

那技术这么多,怎么办?我觉得这是客观问题,当然一定要从兴趣出发,就是今天你喜欢做什么,有些人说我喜欢做游戏,那我觉得你就可以专门去选择学习这样的技术和领域。

所以围绕技术做出的选择永远是基于自身的倾向来的。多说一点就是今天我以个人的角度来看这个问题,其实我发现我个人的喜好是有变化的。我以前喜欢做工程,但我做着做着,转移了我的兴趣,所以我觉得人还是善变的,兴趣是会偏移的,我现在主要是做基础架构,工程上的事情就很少做了,但是工程是我以前喜欢做的,所以人的兴趣是一个变化的过程,并不是一成不变的,所以你今天什么都学一点,也不会一件坏事情,但我建议还是围绕前端,围绕体验的技术展开,不是说今天我学前端,我可能去选一个数据库方面的技术,当然 SQL 是基础的,这个也不是一个关键的技能,只要能用就行。但比如说存储方面,你说分库分表的优化怎么做?我觉得这不是我们关心的,我们也不是特别推荐说,你一定要往这个方面去深入,当然你懂、你有兴趣,你有这个好奇说我要去了解一下,我觉得这也是好的点。

所以总结来说,当下,你可以选择一个感兴趣的点持续深入,不过我不会觉得你会一直在这个点上,你未来也可能会转移你的兴趣。

8. 前端更新快,怎么看待?

元彦:这个问题我们拓展来看,其实不止前端技术发展快,整个技术发展都更新都很快,比方说今天后端体系,你回到两三年前,可能都很少去谈云原生这个概念,但当下,云原生、Serverless 在服务端大行其道,所以技术的发展一直是很快的,这里面包括前端,我觉得这是从宏观层面来看,不是前端发展的快,其实恰恰我觉得前端这些年的发展有点慢。为什么这么说,你们现在用的技术,其实好多年前就已经开始使用,大部分的技术,其实并没有太大变化,我从我的视角来看,前端的技术已经大概有四年左右时间没有什么大的发展。这是我对前端发展的一个看法。

我个人觉得前端未来如果能发展的更快,有更好的迭代,那一定是在新的问题和新的领域,比如说我们在 IOT 领域上的一些发展,包括有些同学说的,我们做一些 D2C,用智能的方式去提升我们整个的研发效率,那我们很多定义的问题都是新的领域,而且这些新的领域其实跟已有的技术是不冲突的,比如 D2C,生成的依然是 Rax 的技术体系,这个体系也存在三到四年了。

9. 什么样的作品能让面试官感兴趣?

大果:这里没有一个绝对的标准,我们去看待学生的作品或者说社招同学的作品,核心还是说是第一个是你在这个产品层面有没有自己的思考。我今天做一个产品、做一个作品,这个作品我是怎么思考的?为什么要去做?而不是说我今天可能就仿照饿了么做个小的网站,或者搞个什么 stackoverflow,这个显然在产品层面上是缺少思考的,就是没有一个,我要去做一个什么样的事情?为什么要去做?这个事情的价值是什么?的思考。

另一块就是整个技术深度的挑战,就比如说我刚才说的仿照做的网站,那这个可能产品层面确实没有什么思考,但如果说我在这个作品里面有很多技术上的挑战,那我们也是认可的。

所以我觉得核心就两个点,一个是你在产品方面有没有自己的一些思考想法,另外一块就是你做的事情,在技术上有没有一些挑战。其实也是核心考察两个点,一个是人的思考能力,一个是人的技术能力。

10. 实习生是不是基本都会转正?

大果:这个不一定,我当时差点就没有实习转正。所以也不是说百分之百转正。所以在整个实习过程中,大家还是要保持一个好奇、热情、有产出这样一个心态。其实我自己在实习前,前端基础是很差的,因为我就是刚才说的非科班,大三的时候才开始接触前端,以前都是搞其他语言,进来的时候整个基础就很差,但我觉得实习的那三四个月,参与到公司一些产品业务里的建设,对于我的技术能力、做事方式的成长是非常非常快的,所以,大家对实习这个过程一定要珍惜,努力争取这样的机会。

冴羽补充一句:淘系去年的实习生其实是都转正的

其他

最近新建了公众号,搜索「冴羽的JavaScript博客」或者 「yayujs」,文章也会第一时间发送,收到推送后依然建议到各平台阅读。

冴羽的公众号

系列博客

JavaScript 系列目录地址:https://github.com/mqyqingfen...

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

查看原文

赞 11 收藏 2 评论 1

冴羽 发布了文章 · 2020-04-10

JavaScript 专题之花式表示 26 个字母

先看效果

先来个思考题:

// 下面这一句会打印什么呢?
[+[][0] + []][0][1]

我们直接看效果:

字母

如果觉得打印一个字母不过瘾的话,打印一句话呢?

// 注意,在Chrome浏览器中打印

[[][0] + []][0][5]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][8]+[[[] == []][0] + []][0][2]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][6]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]]+[]][0][23]+[[][0] + []][0][3]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][8]+[+[1 + [[][0] + []][0][3] +309][0] + []][0][7]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][6]+[[][0] + []][0][0]

再来看看效果:

i love you

基础测验

如果想了解以上是怎么实现的,先来检测下自己对 JavaScript 隐式类型转换的理解程度:

// 下面这些值都会打印什么?
console.log(+[]);
console.log(1 + []);
console.log(+undefined);
console.log([] == []);
console.log(+[] == +[]);

如果不能准确的说出以上的结果,或者是想更深入的理解以上的转换过程,推荐先看两篇文章:

JavaScript专题之头疼的类型转换(上).md

JavaScript专题之头疼的类型转换(下).md

开始解密

第一个效果:

[][0]

因为空数组并不存在第一个元素,所以会打印 undefined

第二个效果:

undefined + []

undefined + [] 相当于 undefined + "" 结果为"undefined"字符串。

这时候已经获得一个 undefined 字符串了,我们只用通过下标就可以取到对应的字母了。

然而,如果我们不用括号,我们怎么取值呢?这时候,我们就需要利用一个技巧:

第三个效果:

['undefined'][0][0]

这时候我们就获得了"u"字母,通过改变下标,我们可以获取u、n、d、e、f、i 共6个字母

是不是很有意思,然而这才只是个开始。

NaN

第一个效果:

+undefined

相当于 Number(undefined),结果是 NaN

第二个效果:

NaN + []

相当于 NaN + "" 结果为NaN字符串

第三个效果:

[NaN][0][1]

通过这种方式我们可以取到字母 a。

false

第一个效果:

[] == []

结果自然是 false

注意,因为之前两个例子的铺垫,或许大家已经渐渐的明白当取出一个值的时候,如果转成字符串,如果取下标的字母了

第二个效果:

// 通过 value + []转成字符串
false + []

第三个效果:

// 通过 [value][0][n] 取字母
['false'][0][0]

我们就可以取出 f 字母

通过这种方式,我们可以取出 "f"、"a"、"l"、"s"、"e"五个字母

true

直接看核心步骤:

+[] == +[]

相当于比较 "" == "",结果自然为 true

剩下的想必大家已经轻车熟路了。

通过以上 4 种方法取到的字母依然有限,我们需要一些其他的方法来获得更多的字母。

Infinity

注意:在前面我们已经取到了字母 e。

+("1e309")

转成数字后,相当于 1 乘以 10 的 309 次方,大于 JavaScript 最大的数,所以结果会是 Infinity,剩下的步骤与上面的相同,以后就不赘述了。

我们可以从中取出 t 和 y

function

注意:到此为止,我们已经获得了 u n d e f i t r f a l s t y,从中我们可以拼成"find"字符串。

[]["find"]

会显示数组的find函数,结果为:

function find() { [native code] }

通过这种方法,我们可以取出 c o v。

不过注意:通过这种方式取字母 v 会有兼容性问题!!!

神奇的constructor

注意,我们已经有了 17 个字母了,我们现在可以拼出"constructor"!

constructor 可是一个神奇的属性,因为通过它,我们可以获得各种类型的值对象的构造函数!

0["constructor"] // function Number() { [native code] }

""["constructor"] // function String() { [native code] }

...

通过以上方式,我们可以取 m、g

也许我们会疑问,"" 如何表示呢?

[] + [] === "" // true

name

有了 m,我们现在可以拼出 name,可是 name 有什么用呢?

"to" + ""["constructor"]["name"] // "toString"

我们最终的目的是拼出万能的"toString"字符串

万能的 toString

我们之所以拼出 toString,是因为利用 toString 这个方法可以表示出 26个 字母!

这时候,就要隆重介绍下这个平时看起来不起眼,但是在这里确实最终主角的 toString 方法!

以下引自 W3C school:

作用:

toString() 方法可把一个 Number 对象转换为一个字符串,并返回结果。

用法:

NumberObject.toString(radix)

参数解释:

radix:表示数字的基数,使 2 ~ 36 之间的整数。若省略该参数,则使用基数 10。但是要注意,如果该参数是 10 以外的其他值,则 ECMAScript 标准允许实现返回任意值

举个例子:

var number = new Number(10);
number.toString('16');

就是将10用16进制进行表示,上面的例子打印的结果是"a"。

注意,radix最大可以表示36!!!

var number = new Number(35);
number.toString('36');

打印的字母是 "z"! 用这种方法,我们可以表示剩下的所有字母!

但是我们怎么利用这个 toString 方法呢?准确的说,我们该怎么生成一个 number 对象呢?还要拼出 new Number 吗?

其实都不用!这个时候,就彰显出了 JavaScript 隐式类型转换的优秀之处:

35["toString"](36) // z

注意:到了这个时候,我们也不得不使用()了!

到此为止,我们已经可以表示出所有的字母了,有的很轻松的就表示出来,有的则有些麻烦,而且显示也很长,比如字母 p:

25[[[+[] == +[]][0] + []][0][0]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][6] + [[] + []][0][[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][3]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][6]+[[][0] + []][0][1]+[[[] == []][0] + []][0][3]+[[+[] == +[]][0] + []][0][0]+[[+[] == +[]][0] + []][0][1]+[[][0] + []][0][0]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][3]+[[+[] == +[]][0] + []][0][0]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][6]+[[+[] == +[]][0] + []][0][1]][[[][0] + []][0][1]+[+[][0] + []][0][1]+[0[[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][3]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][6]+[[][0] + []][0][1]+[[[] == []][0] + []][0][3]+[[+[] == +[]][0] + []][0][0]+[[+[] == +[]][0] + []][0][1]+[[][0] + []][0][0]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][3]+[[+[] == +[]][0] + []][0][0]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][6]+[[+[] == +[]][0] + []][0][1]]+[]][0][11]+[[][0] + []][0][3]]](27)

其他

最近新建了公众号,搜索「冴羽的JavaScript博客」或者 「yayujs」,文章也会第一时间发送,收到推送后依然建议到各平台阅读。

冴羽的公众号

系列博客

JavaScript 系列目录地址:https://github.com/mqyqingfen...

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

查看原文

赞 13 收藏 7 评论 6

冴羽 发布了文章 · 2020-04-08

JavaScript深入之头疼的类型转换(下)

前言

举个例子:

console.log(1 + '1')

在 JavaScript 中,这是完全可以运行的,不过你有没有好奇,为什么 1 和 '1' 分属不同的数据类型,为什么就可以进行运算呢?

这其实是因为 JavaScript 自动的将数据类型进行了转换,我们通常称为隐式类型转换。但是我们都知道,+运算符既可以用于数字加法,也能用于字符串拼接,那在这个例子中,是将数字 1 转成字符串 '1',进行拼接运算?还是将字符串 '1' 转成数字 1,进行加法运算呢?

先卖个关子,虽然估计你也知道答案。今天,我们就常见的隐式类型转化的场景进行介绍。

一元操作符 +

console.log(+'1');

当 + 运算符作为一元操作符的时候,查看 ES5规范1.4.6,会调用 ToNumber 处理该值,相当于 Number('1'),最终结果返回数字 1

那么下面的这些结果呢?

console.log(+[]);
console.log(+['1']);
console.log(+['1', '2', '3']);
console.log(+{});

既然是调用 ToNumber 方法,回想《JavaScript 深入之头疼的类型转换(上)》中的内容,当输入的值是对象的时候,先调用 ToPrimitive(input, Number) 方法,执行的步骤是:

  1. 如果 obj 为基本类型,直接返回
  2. 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
  3. 否则,调用 toString 方法,如果返回一个原始值,则JavaScript 将其返回。
  4. 否则,JavaScript 抛出一个类型错误异常。

+[] 为例,[] 调用 valueOf 方法,返回一个空数组,因为不是原始值,调用 toString 方法,返回 ""

得到返回值后,然后再调用 ToNumber 方法,"" 对应的返回值是 0,所以最终返回 0

剩下的例子以此类推。结果是:

console.log(+['1']); // 1
console.log(+['1', '2', '3']); // NaN
console.log(+{}); // NaN

二元操作符 +

规范

现在 + 运算符又变成了二元操作符,毕竟它也是加减乘除中的加号

1 + '1' 我们知道答案是 '11',那 null + 1[] + [][] + {}{} + {} 呢?

如果要了解这些运算的结果,不可避免的我们要从规范下手。

规范地址:http://es5.github.io/#x11.6.1

不过这次就不直接大段大段的引用规范了,直接给大家讲简化后的内容。

到底当执行 + 运算的时候,会执行怎样的步骤呢?让我们根据规范11.6.1 来捋一捋:

当计算 value1 + value2时:

  1. lprim = ToPrimitive(value1)
  2. rprim = ToPrimitive(value2)
  3. 如果 lprim 是字符串或者 rprim 是字符串,那么返回 ToString(lprim) 和 ToString(rprim)的拼接结果
  4. 返回 ToNumber(lprim) 和 ToNumber(rprim)的运算结果

规范的内容就这样结束了。没有什么新的内容,ToStringToNumberToPrimitive都是在《JavaScript 深入之头疼的类型转换(上)》中讲到过的内容,所以我们直接进分析阶段:

让我们来举几个例子:

1.Null 与数字

console.log(null + 1);

按照规范的步骤进行分析:

  1. lprim = ToPrimitive(null) 因为null是基本类型,直接返回,所以 lprim = null
  2. rprim = ToPrimitive(1) 因为 1 是基本类型,直接返回,所以 rprim = null
  3. lprim 和 rprim 都不是字符串
  4. 返回 ToNumber(null) 和 ToNumber(1) 的运算结果

接下来:

ToNumber(null) 的结果为0,(回想上篇 Number(null)),ToNumber(1) 的结果为 1

所以,null + 1 相当于 0 + 1,最终的结果为数字 1

这个还算简单,看些稍微复杂的:

2.数组与数组

console.log([] + []);

依然按照规范:

  1. lprim = ToPrimitive([]),[]是数组,相当于ToPrimitive([], Number),先调用valueOf方法,返回对象本身,因为不是原始值,调用toString方法,返回空字符串""
  2. rprim类似。
  3. lprim和rprim都是字符串,执行拼接操作

所以,[] + []相当于 "" + "",最终的结果是空字符串""

看个更复杂的:

3.数组与对象

// 两者结果一致
console.log([] + {});
console.log({} + []);

按照规范:

  1. lprim = ToPrimitive([]),lprim = ""
  2. rprim = ToPrimitive({}),相当于调用 ToPrimitive({}, Number),先调用 valueOf 方法,返回对象本身,因为不是原始值,调用 toString 方法,返回 "[object Object]"
  3. lprim 和 rprim 都是字符串,执行拼接操作

所以,[] + {} 相当于 "" + "[object Object]",最终的结果是 "[object Object]"。

下面的例子,可以按照示例类推出结果:

console.log(1 + true);
console.log({} + {});
console.log(new Date(2017, 04, 21) + 1) // 这个知道是数字还是字符串类型就行

结果是:

console.log(1 + true); // 2
console.log({} + {}); // "[object Object][object Object]"
console.log(new Date(2017, 04, 21) + 1) // "Sun May 21 2017 00:00:00 GMT+0800 (CST)1"

注意

以上的运算都是在 console.log 中进行,如果你直接在 Chrome 或者 Firebug 开发工具中的命令行直接输入,你也许会惊讶的看到一些结果的不同,比如:

type1

我们刚才才说过 {} + [] 的结果是 "[object Object]" 呐,这怎么变成了 0 了?

不急,我们尝试着加一个括号:

type2

结果又变成了正确的值,这是为什么呢?

其实,在不加括号的时候,{} 被当成了一个独立的空代码块,所以 {} + [] 变成了 +[],结果就变成了 0

同样的问题还出现在 {} + {} 上,而且火狐和谷歌的结果还不一样:

> {} + {}
// 火狐: NaN
// 谷歌: "[object Object][object Object]"

如果 {} 被当成一个独立的代码块,那么这句话相当于 +{},相当于 Number({}),结果自然是 NaN,可是 Chrome 却在这里返回了正确的值。

那为什么这里就返回了正确的值呢?我也不知道,欢迎解答~

== 相等

规范

"==" 用于比较两个值是否相等,当要比较的两个值类型不一样的时候,就会发生类型的转换。

关于使用"=="进行比较的时候,具体步骤可以查看规范11.9.5

当执行x == y 时:

  1. 如果x与y是同一类型:

    1. x是Undefined,返回true
    2. x是Null,返回true
    3. x是数字:

      1. x是NaN,返回false
      2. y是NaN,返回false
      3. x与y相等,返回true
      4. x是+0,y是-0,返回true
      5. x是-0,y是+0,返回true
      6. 返回false
    4. x是字符串,完全相等返回true,否则返回false
    5. x是布尔值,x和y都是true或者false,返回true,否则返回false
    6. x和y指向同一个对象,返回true,否则返回false
  2. x是null并且y是undefined,返回true
  3. x是undefined并且y是null,返回true
  4. x是数字,y是字符串,判断x == ToNumber(y)
  5. x是字符串,y是数字,判断ToNumber(x) == y
  6. x是布尔值,判断ToNumber(x) == y
  7. y是布尔值,判断x ==ToNumber(y)
  8. x不是字符串或者数字,y是对象,判断x == ToPrimitive(y)
  9. x是对象,y不是字符串或者数字,判断ToPrimitive(x) == y
  10. 返回false

觉得看规范判断太复杂?我们来分几种情况来看:

1. null和undefined

console.log(null == undefined);

看规范第2、3步:

  1. x是null并且y是undefined,返回true
  2. x是undefined并且y是null,返回true

所以例子的结果自然为 true

这时候,我们可以回想在《JavaScript专题之类型判断(上)》中见过的一段 demo,就是编写判断对象的类型 type 函数时,如果输入值是 undefined,就返回字符串 undefined,如果是 null,就返回字符串 null

如果是你,你会怎么写呢?

下面是 jQuery 的写法:

function type(obj) {
    if (obj == null) {
        return obj + '';
    }
    ...
}

2. 字符串与数字

console.log('1' == 1);

结果肯定是true,问题在于是字符串转化成了数字和数字比较还是数字转换成了字符串和字符串比较呢?

看规范第4、5步:

4.x是数字,y是字符串,判断x == ToNumber(y)

5.x是字符串,y是数字,判断ToNumber(x) == y

结果很明显,都是转换成数字后再进行比较

3. 布尔值和其他类型

console.log(true == '2')

当要判断的一方出现 false 的时候,往往最容易出错,比如上面这个例子,凭直觉应该是 true,毕竟 Boolean('2') 的结果可是true,但这道题的结果却是false。

归根到底,还是要看规范,规范第6、7步:

6.x是布尔值,判断ToNumber(x) == y

7.y是布尔值,判断x ==ToNumber(y)

当一方出现布尔值的时候,就会对这一方的值进行ToNumber处理,也就是说true会被转化成1,

true == '2' 就相当于 1 == '2' 就相当于 1 == 2,结果自然是 false

所以当一方是布尔值的时候,会对布尔值进行转换,因为这种特性,所以尽量少使用 xx == truexx == false 的写法。

比如:

// 不建议
if (a == true) {}

// 建议
if (a) {}
// 更好
if (!!a) {}

4. 对象与非对象

console.log( 42 == ['42'])

看规范第8、9步:

  1. x不是字符串或者数字,y是对象,判断x == ToPrimitive(y)
  2. x是对象,y不是字符串或者数字,判断ToPrimitive(x) == y

以这个例子为例,会使用 ToPrimitive 处理 ['42'],调用valueOf,返回对象本身,再调用 toString,返回 '42',所以

42 == ['42'] 相当于 42 == '42' 相当于42 == 42,结果为 true

到此为止,我们已经看完了第2、3、4、5、6、7、8、9步,其他的一概返回 false。

其他

再多举几个例子进行分析:

console.log(false == undefined)

false == undefined 相当于 0 == undefined 不符合上面的情形,执行最后一步 返回 false

console.log(false == [])

false == [] 相当于 0 == [] 相当于 0 == '' 相当于 0 == 0,结果返回 true

console.log([] == ![])

首先会执行 ![] 操作,转换成 false,相当于 [] == false 相当于 [] == 0 相当于 '' == 0 相当于 0 == 0,结果返回 true

最后再举一些会让人踩坑的例子:

console.log(false == "0")
console.log(false == 0)
console.log(false == "")

console.log("" == 0)
console.log("" == [])

console.log([] == 0)

console.log("" == [null])
console.log(0 == "\n")
console.log([] == 0)

以上均返回 true

其他

除了这两种情形之外,其实还有很多情形会发生隐式类型转换,比如if? :&&等情况,但相对来说,比较简单,就不再讲解。

深入系列

JavaScript 深入系列目录地址:https://github.com/mqyqingfen...

JavaScript 深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

再其他

现在是校招季,淘系前端正在帮助21届同学们收割大厂offer,专门创建了交流群,可以在这里提问题,交流面试经验,这里也提供了简历辅导和答疑解惑等服务,加淘小招微信邀你入群,微信号 taoxiaozhao233

查看原文

赞 9 收藏 5 评论 0

冴羽 发布了文章 · 2020-03-31

阿里前端攻城狮们又写了一份面试题答案,请查收

前言

话说两周前,我发了这样一条沸点:

于是我真的就建群收集了题目,和团队的同事一起写答案,我们也不图什么,就是想做一件有意义的事情,现在我整理了下我们的回答,有的不一定就是非常具体的回答,但也提供了思路和参考资料,大家看看是否还有什么补充的?

[字节跳动] 怎么与服务端保持连接

和服务端保持连接,最简单粗暴的方法就是通过请求轮询保持跟服务端的通信,客户端不光要花成本维护定时轮询器,还会使得服务器压力变大,所以不推荐。

还有一种可以借助请求超时的设置,将超时时间设置一个足够大的值,客户端发起连接后,只要服务端不返回消息,整个连接阶段都会受到阻塞,所以这种方式也不推荐。

最后一种是WebSocket,当服务器完成协议从HTTP到WebSocket的升级后,服务端可以主动推送信息给客户端,解决了轮询造成的同步延迟问题。由于 WebSocket 只需要一次 HTTP 握手,服务端就能一直与客户端保持通信,直到关闭连接,这样就解决了服务器需要反复解析 HTTP 协议,减少了资源的开销。

1

[字节跳动] 谈一下隐式类型转换

这个问题可以拓展讲成 JavaScript 中的类型转换,分为两类,显式类型转换和隐式类型转换,当我们用 Number() 等函数的时候,就是显式类型转换,其转换规则是当是基本类型时,参照规范中的对应表进行转换,当不是基本类型的时候,先参照规范中的 ToPrimitive 方法转换为基本类型,再按照对应表转换,当执行 ToPrimitive 的时候,又会根据情况不同,判断先执行对象的 valueOf 方法还是 toString 方法进行准换,这个可以参照 JavaScript 深入之头疼的类型转换(上),而当我们进行运算的时候,经常发生的就是隐式类型转换,比如 +== 运算符,当 + 运算符的时候,更倾向于转成字符串,而当 == 的时候,更倾向于转为数字,这个在 [JavaScript 深入之头疼的类型转换(下)]()中会讲到……但是我还在写……总之,回答类型转换的时候,最好是扯到规范中,表明你在研究这块内容的时候,还专门去看过规范。基本上回答了这里,对类型转换说明已经有很多的了解,但你可以再拓展讲一下,比如当时我在学习类型转换的时候,还专门去研究了如何花式表示 26 个字母,比如在控制台打印这样一句话:

[[][0] + []][0][5]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][8]+[[[] == []][0] + []][0][2]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][6]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]]+[]][0][23]+[[][0] + []][0][3]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][8]+[+[1 + [[][0] + []][0][3] +309][0] + []][0][7]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][6]+[[][0] + []][0][0]

花式表示字母的原理大家也可以探索一下,虽然看起来很无聊,但可以作为一个小案例,表明下你对技术的钻研和兴趣。

[各种公司] 输入url后发生了什么

这个问题的核心是在问从输入URL到页面渲染经历了哪些过程。

从耗时过程来看,可以分为DNS解析、TCP连接、HTTP请求与响应、客户端浏览器解析渲染、连接结束。其中浏览器解析渲染包含HTML词法、语法的解析、CSS解析、DOM树生成、渲染树建立、屏幕绘制。

下面针对几个较为重要的过程做下介绍。

DNS解析

当我们在浏览器中输入如www.taobao.com的时候,DNS解析充当了一个翻译的角色,把网址「翻译」成了IP地址。DNS解析的过程就是域名到IP地址的转换的过程。域名解析也叫域名指向、服务器设置、域名配置以及反向IP登记等等。说得简单点就是将好记的域名解析成IP,服务由DNS服务器完成,把域名解析到一个IP地址,然后在此IP地址的主机上将一个子目录与域名绑定。

image

TCP连接

TCP连接的重要目的,是为了保证消息的有序和不丢包,为了建立可靠的数据传输,TCP通信双方相互告知初始化序列号,并确定对方已经收到ISN的,整个链接的过程就是我们俗称的三次握手

image

HTTP请求与响应

HTTP请求它主要发生在客户端,发送HTTP请求的过程就是构建HTTP请求报文并通过TCP协议发送到服务器指定端口的过程。

还是用 https://www.taobao.com 举例子。

当在地址栏输入后,浏览器会分析这个url,并设置好请求报文发出。请求报文中包括请求行(包括请求的方法,路径和协议版本)、请求头(包含了请求的一些附加的信息,一般是以键值的形式成对存在)、空行(协议中规定请求头和请求主体间必须用一个空行隔开)、请求主体(对于post请求,所需要的参数都不会放在url中,这时候就需要一个载体了,这个载体就是请求主体)。服务端收到这个请求后,会根据url匹配到的路径做相应的处理,最后返回浏览器需要的页面资源。处理后,浏览器会收到一个响应报文,而所需要的资源就就在报文主体上。与请求报文相同,响应报文也有与之对应的起始行(响应报文的起始行同样包含了协议版本,与请求的起始行不同的是其包含的还有状态码和状态码的原因短语)、响应头(对应请求报文中的请求头,格式一致,但是各自有不同的首部)、空行、报文主体(请求所需要的资源),不同的地方在于包含的东西不一样。

HTML词法、语法解析

对我们来说HTML其实是一坨字符串,而实际上我们要面对的是"字符流"。为了把字符流解析成正确的可被浏览器识别的结构,我们需要做的事情分为两步:

词法分析:把字符流初步解析成我们可理解的"词",学名叫token。

语法分析:把开始结束标签配对、属性赋值好、父子关系连接好、构成dom树。

html结构其实不算太复杂,我们平时见到的大部分都只是标签、属性、注释、CDATA节点。

屏幕绘制

image

DOM树的生成和渲染树建立比较好理解这个就不做展开。完成了这「两棵树」的构造后,就进入屏幕绘制阶段。

在绘制的过程中,会遍历渲染树,调用由浏览器的UI组件的paint()方法在屏幕上显示对应的内容,并根据渲染树布局,计算CSS样式(即每个节点在页面中的大小和位置等几何信息)。

HTML默认是从上到下流式布局的,CSS和JS的加入会打破这种布局,改变DOM的外观样式以及大小和位置。这就引出两个非常重要的概念:replaint重绘和reflow重排。

replaint重绘,屏幕的一部分重新绘制,不影响整体布局,比如某个CSS的背景色变了,但元素的几何尺寸和位置不变。eflow重排: 意味着元件的几何尺寸变了,我们需要重新验证并计算渲染树。是渲染树的一部分或全部发生了变化。无论是重绘还是重排,对浏览器而言都是一种「消耗」,所以我们应该尽量减少这两种状态的触发。

[百词斩] CDN的原理

CDN的基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中,在用户访问网站时,利用全局负载技术将用户的访问指向距离最近的工作正常的缓存服务器上,由缓存服务器直接响应用户请求。
最简单的CDN网络由一个DNS 服务器和几台缓存服务器就可以组成,当用户输入URL按下回车,经过本地DNS系统解析,DNS系统会最终将域名的解析权交给CNAME指向的CDN专用DNS服务器,然后将得到全局负载均衡设备的IP地址,用户向全局负载均衡设备发送内容访问请求,全局负载均衡设备将实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上,使用户可就近取得所需内容,解决 Internet网络拥挤的状况,提高用户访问网站的响应速度

[百词斩] 性能监控平台是如何捕获错误的

一般从如下两个方面考虑:

1.全局捕获:

通过全局的接口,将捕获代码集中写在一个地方,可以利用的接口有:

  1. window.addEventListener(‘error’) / window.addEventListener(“unhandledrejection”) / document.addEventListener(‘click’) 等
  2. 框架级别的全局监听,例如aixos中使用interceptor进行拦截,vue、react都有自己的错误采集接口
  3. 通过对全局函数进行封装包裹,实现在在调用该函数时自动捕获异常
  4. 对实例方法重写(Patch),在原有功能基础上包裹一层,例如对console.error进行重写,在使用方法不变的情况下也可以异常捕获

2.单点捕获:

在业务代码中对单个代码块进行包裹,或在逻辑流程中打点,实现有针对性的异常捕获:

  1. try…catch
  2. 专门写一个函数来收集异常信息,在异常发生时,调用该函数
  3. 专门写一个函数来包裹其他函数,得到一个新函数,该新函数运行结果和原函数一模一样,只是在发生异常时可以捕获异常

注:详情可参见Fundebug 发表的 《前端异常监控解决方案研究》

[腾讯一面] 性能优化从哪些方面入手?

• 分屏加载,当页面需要渲染的数据较多时,先渲染首屏,下滑时再加载第二屏的数据;
• 图片大小优化,在不影响视觉效果的前提下,把图片尺寸降到最小;
• 图片懒加载,on appear时再加载图片;
• Code splitting,或者拆包,应用下的某些组件不需要立刻import,可以采用动态import的方式,打包时也可以将它们打到不同的bundle里,给index bundle瘦身;
• Chrome Devtools - Trace & Timeline等一系列强大的分析工具可以去研究一下,它们可以深入到内核分析应用的性能问题所在;

[腾讯二面] 如何加快首屏渲染,你有哪些方案?

  • 降低请求量:合并资源,减少 HTTP 请求数,minify / gzip 压缩,webP,lazyload。
  • 加快请求速度:预解析DNS,减少域名数,并行加载,CDN 分发。
  • 增加缓存:HTTP 协议缓存请求,离线缓存 manifest,离线数据缓存 localStorage、PWA。
  • 渲染优化:首屏内容最小化,JS/CSS优化,加载顺序,服务端渲染,pipeline。

[腾讯一面] webpack 针对模块化做的处理

这一块建议去读 webpack4 文档中对于 library,libraryTarget 的描述。当我们开发一个 JS 库的时候,通常最终的 npm package 需要输出的是一些组件或 api,这个时候我们需要了解webpack4所提供的模块化的打包能力。通过对libraryTarget的设置,我们可以将我们的工程打包成amd,umd,或commonJS模块。
https://webpack.js.org/config...

[腾讯一面] 概述一下 Node.js 中的进程与线程

Node.js 中的进程 Process 是一个全局对象,无需 require 直接使用,给我们提供了当前进程中的相关信息。Node.js 中进程可以使用 child_process 模块创建。

关系:

  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程(通常说的主线程)。
  • 同一进程的所有线程共享该进程的所有资源。
  • 进程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
  • 处理机分给线程,即真正在处理机上运行的是线程。
  • 线程是指进程内的一个执行单元,也是进程内的可调度实体。

区别:

  • 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。
  • 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。

[字节跳动] 鼠标滚动的时候,会触发很多次事件,如何解决的?

这里选择的方法应该是节流,可以拓展讲到防抖和节流,防抖是指连续触发的时候只会执行一次,停止触发 N 秒后才能继续执行,而节流是指如果你持续触发事件,每隔一段时间,只执行一次事件。像防止按钮多次点击就用防抖,像是监听滚动事件就用节流,函数实现都可以参照 underscore 代码中的实现,以前我写过 JavaScript专题之跟着underscore学防抖JavaScript专题之跟着 underscore 学节流 两篇文章讲述了 underscore 中的实现方式

查看原文

赞 56 收藏 40 评论 1

冴羽 发布了文章 · 2020-03-30

致2021届前端同学的一封信|砥砺前行,未来可期!

前言

本篇的作者是来自淘系 D2C 团队的泽壹,作为准入职的淘系前端开发工程师,写给师弟们的这封信有着中肯的建议和诚挚的祝福。

亲爱的2021届前端同学们:

   你好,我是准入职淘系前端开发工程师泽壹,去年春天到夏天的求职历程还记忆犹新,走过些弯路,大一是车辆工程专业,大二时候转入软件工程专业,大二下学期接触前端,大三秋招很荣幸拿到阿里淘系 offer,这一路走来说下心得体会,希望可以帮助到正在求职的同学们。

为什么选择阿里

在阿里,我认为挑战与学习并存。

曾在社交平台上看到阿里师兄对 ATA 内部技术社区赞不绝口,` 我不止一次提过 ATA 那仿佛宝库一般的内部技术社区,你发的文章以及技术评论,会收到来自集团各个 BU 的大佬一起探讨,甚至转移到钉钉 1:1 深入交流,真的是太美妙了 `,当我切身目睹 ATA 这一片知识汪洋后,确实名不虚传,你可以化身为浪里小白龙投入到知识海洋中。

前端的深水区比如涉及到计算机图形学的互动图形和可视化,算法与前端交叉融合的前端智能化,在淘系都有相应的场景,充满挑战,天花板效应在这里似乎是不存在的,同时在手淘上有你写的代码也是可以向经常逛淘宝的女朋友吹嘘的一件事。

前端技术学习

技术是安身立命之本,我想很多同学跟我一样在前端学习的道路上是独自前行的,在刚开始接触前端的时候我是根据岗位的 JD 制定学习计划,我将前端学习过程分为 3 个阶段。

1.入门阶段

所谓基础不牢,必然地动山摇。入门阶段要打好基础,《JavaScript 高级程序设计》、《ES6 标准入门》、《你不知道的 JavaScript》、《CSS 权威指南》、《CSS 揭秘》我认为都是前端必读书籍。在学习基础知识的时候,要多加练习,而入门最好的方式莫过于模仿,可以尝试模仿一些比较复杂的页面,能够很快的熟悉各方面的基础知识。比如看看别人的页面是怎么布局的,样式是怎么写的,有什么技巧等。同时可以看看 underscore 代码,学习常用的比如节流、防抖是如何实现的。

2.提升阶段    

紧接着学习前端流行框架 Vue/React, 两者的官方文档值得细品,除了快速入门以外还有些高级指引、最佳实践,如 Error Boundaries你可能不需要使用派生 state。同时大家应该在实战中成长,向大家推荐一个全栈练手项目 Spectrum,是一个用于开源社区聊天的,论坛 + 聊天室的结合体,整个技术栈我觉得不错:

前端:React/Redux/React-Router/DraftJS

后端:Nodejs/GraphQL/RethinkDB/WebSocket/Redis

从中你可以学习到很多,比如:

- 如何构建一个 GraphQL 的服务端

- React 如何和 GraphQL 交互

- 如何去用 Redux 管理复杂的应用程序状态

- 如何实现 SSR

等等。

3.深入阶段

深入阶段可以分为两个维度:技术广度和技术深度。

扩展技术广度可以通过多逛逛技术社区,那些你没听到过的技术词就是你要学习的新知识,日常工作中必定会面临一些新的场景,技术广度可以帮助你,知道某个技术能干什么,原理大致是什么,是否适合当前业务的场景等。

技术深度是对技术底层或设计理念有比较透彻的理解,可以阅读 React、Vue 源码,深入理解框架的运行机制以及背后的设计模式,并可以通过对原理的理解来优化代码,这些可以作为项目中的技术亮点。

同时在学习的过程中,一定要多实践,爱折腾,千万不要眼高手低,最后会形成“简单问题不想搞,复杂问题搞不定”,往往问题的本质都是在解决一个个问题的过程中总结出来的。同时通过折腾,逐渐沉淀出一些成熟的项目,能达到解决现实问题的更好,这些都是让面试官眼前一亮、增加面试筹码的有利资产。

前端面试经验分享

所有的面试技巧都是建立在面试者已经具备了相对合格的实力的基础之上。

首先,大家还是要先储备足够的知识,这些知识包括操作系统、计算机网络、数据结构、算法、安全等计算机基础知识、前端知识 和其他的加分项知识。

其次,可以看看同学们的面试经验分享,我认为看面经不是应试的过程,更多的是查漏补缺的过程。

然后,做过的项目要总结完善,面试官会从广度和深度来挖掘项目中的亮点,比如你的懒加载是如何实现的,知道 IntersectionObserver API 吗? 有读过它的 polyfill 吗?给面试官一个选你的理由。

最后,面试的过程中要充满自信,多表达自己的理解,展示自己解决问题的思路。

砥砺前行,未来可期

学习没有捷径可走,在学习的过程希望你能够保持对技术的好奇心,收获一份理想的 offer 固然不易,放下一些东西,专注于事情,内心有相信,行动有坚持,结果有落地,offer 往往是水到渠成的,各位加油,你也可以是个 offer 收割机。

祝:大家一切安好,求职顺利,收获理想 offer。

泽壹

2020.3.27

查看原文

赞 5 收藏 1 评论 0

冴羽 发布了文章 · 2020-03-27

JavaScript深入之头疼的类型转换(上)

在 JavaScript 中,有一部分内容,情况复杂,容易出错,饱受争议但又应用广泛,这便是类型转换。

前言

将值从一种类型转换为另一种类型通常称为类型转换。

ES6 前,JavaScript 共有六种数据类型:Undefined、Null、Boolean、Number、String、Object。

我们先捋一捋基本类型之间的转换。

原始值转布尔

我们使用 Boolean 函数将类型转换成布尔类型,在 JavaScript 中,只有 6 种值可以被转换成 false,其他都会被转换成 true。

console.log(Boolean()) // false

console.log(Boolean(false)) // false

console.log(Boolean(undefined)) // false
console.log(Boolean(null)) // false
console.log(Boolean(+0)) // false
console.log(Boolean(-0)) // false
console.log(Boolean(NaN)) // false
console.log(Boolean("")) // false

注意,当 Boolean 函数不传任何参数时,会返回 false。

原始值转数字

我们可以使用 Number 函数将类型转换成数字类型,如果参数无法被转换为数字,则返回 NaN。

在看例子之前,我们先看 ES5 规范 15.7.1.1 中关于 Number 的介绍:

1

根据规范,如果 Number 函数不传参数,返回 +0,如果有参数,调用 ToNumber(value)

注意这个 ToNumber 表示的是一个底层规范实现上的方法,并没有直接暴露出来。

ToNumber 则直接给了一个对应的结果表。表如下:

参数类型结果
UndefinedNaN
Null+0
Boolean如果参数是 true,返回 1。参数为 false,返回 +0
Number返回与之相等的值
String这段比较复杂,看例子

让我们写几个例子验证一下:

console.log(Number()) // +0

console.log(Number(undefined)) // NaN
console.log(Number(null)) // +0

console.log(Number(false)) // +0
console.log(Number(true)) // 1

console.log(Number("123")) // 123
console.log(Number("-123")) // -123
console.log(Number("1.2")) // 1.2
console.log(Number("000123")) // 123
console.log(Number("-000123")) // -123

console.log(Number("0x11")) // 17

console.log(Number("")) // 0
console.log(Number(" ")) // 0

console.log(Number("123 123")) // NaN
console.log(Number("foo")) // NaN
console.log(Number("100a")) // NaN

如果通过 Number 转换函数传入一个字符串,它会试图将其转换成一个整数或浮点数,而且会忽略所有前导的 0,如果有一个字符不是数字,结果都会返回 NaN,鉴于这种严格的判断,我们一般还会使用更加灵活的 parseInt 和 parseFloat 进行转换。

parseInt 只解析整数,parseFloat 则可以解析整数和浮点数,如果字符串前缀是 "0x" 或者"0X",parseInt 将其解释为十六进制数,parseInt 和 parseFloat 都会跳过任意数量的前导空格,尽可能解析更多数值字符,并忽略后面的内容。如果第一个非空格字符是非法的数字直接量,将最终返回 NaN:

console.log(parseInt("3 abc")) // 3
console.log(parseFloat("3.14 abc")) // 3.14
console.log(parseInt("-12.34")) // -12
console.log(parseInt("0xFF")) // 255
console.log(parseFloat(".1")) // 0.1
console.log(parseInt("0.1")) // 0

原始值转字符

我们使用 String 函数将类型转换成字符串类型,依然先看 规范15.5.1.1中有关 String 函数的介绍:

如果 String 函数不传参数,返回空字符串,如果有参数,调用 ToString(value),而 ToString 也给了一个对应的结果表。表如下:

参数类型结果
Undefined"undefined"
Null"null"
Boolean如果参数是 true,返回 "true"。参数为 false,返回 "false"
Number又是比较复杂,可以看例子
String返回与之相等的值

让我们写几个例子验证一下:

console.log(String()) // 空字符串

console.log(String(undefined)) // undefined
console.log(String(null)) // null

console.log(String(false)) // false
console.log(String(true)) // true

console.log(String(0)) // 0
console.log(String(-0)) // 0
console.log(String(NaN)) // NaN
console.log(String(Infinity)) // Infinity
console.log(String(-Infinity)) // -Infinity
console.log(String(1)) // 1

注意这里的 ToString 和上一节的 ToNumber 都是底层规范实现的方法,并没有直接暴露出来。

原始值转对象

原始值到对象的转换非常简单,原始值通过调用 String()、Number() 或者 Boolean() 构造函数,转换为它们各自的包装对象。

null 和 undefined 属于例外,当将它们用在期望是一个对象的地方都会造成一个类型错误 (TypeError) 异常,而不会执行正常的转换。

var a = 1;
console.log(typeof a); // number
var b = new Number(a);
console.log(typeof b); // object

对象转布尔值

对象到布尔值的转换非常简单:所有对象(包括数组和函数)都转换为 true。对于包装对象也是这样,举个例子:

console.log(Boolean(new Boolean(false))) // true

对象转字符串和数字

对象到字符串和对象到数字的转换都是通过调用待转换对象的一个方法来完成的。而 JavaScript 对象有两个不同的方法来执行转换,一个是 toString,一个是 valueOf。注意这个跟上面所说的 ToStringToNumber 是不同的,这两个方法是真实暴露出来的方法。

所有的对象除了 null 和 undefined 之外的任何值都具有 toString 方法,通常情况下,它和使用 String 方法返回的结果一致。toString 方法的作用在于返回一个反映这个对象的字符串,然而这才是情况复杂的开始。

《JavaScript专题之类型判断(上)》中讲到过 Object.prototype.toString 方法会根据这个对象的[[class]]内部属性,返回由 "[object " 和 class 和 "]" 三个部分组成的字符串。举个例子:

Object.prototype.toString.call({a: 1}) // "[object Object]"
({a: 1}).toString() // "[object Object]"
({a: 1}).toString === Object.prototype.toString // true

我们可以看出当调用对象的 toString 方法时,其实调用的是 Object.prototype 上的 toString 方法。

然而 JavaScript 下的很多类根据各自的特点,定义了更多版本的 toString 方法。例如:

  1. 数组的 toString 方法将每个数组元素转换成一个字符串,并在元素之间添加逗号后合并成结果字符串。
  2. 函数的 toString 方法返回源代码字符串。
  3. 日期的 toString 方法返回一个可读的日期和时间字符串。
  4. RegExp 的 toString 方法返回一个表示正则表达式直接量的字符串。

读文字太抽象?我们直接写例子:

console.log(({}).toString()) // [object Object]

console.log([].toString()) // ""
console.log([0].toString()) // 0
console.log([1, 2, 3].toString()) // 1,2,3
console.log((function(){var a = 1;}).toString()) // function (){var a = 1;}
console.log((/\d+/g).toString()) // /\d+/g
console.log((new Date(2010, 0, 1)).toString()) // Fri Jan 01 2010 00:00:00 GMT+0800 (CST)

而另一个转换对象的函数是 valueOf,表示对象的原始值。默认的 valueOf 方法返回这个对象本身,数组、函数、正则简单的继承了这个默认方法,也会返回对象本身。日期是一个例外,它会返回它的一个内容表示: 1970 年 1 月 1 日以来的毫秒数。

var date = new Date(2017, 4, 21);
console.log(date.valueOf()) // 1495296000000

对象接着转字符串和数字

了解了 toString 方法和 valueOf 方法,我们分析下从对象到字符串是如何转换的。看规范 ES5 9.8,其实就是 ToString 方法的对应表,只是这次我们加上 Object 的转换规则:

参数类型结果
Object 1. primValue = ToPrimitive(input, String)
2. 返回ToString(primValue).

所谓的 ToPrimitive 方法,其实就是输入一个值,然后返回一个一定是基本类型的值。

我们总结一下,当我们用 String 方法转化一个值的时候,如果是基本类型,就参照 “原始值转字符” 这一节的对应表,如果不是基本类型,我们会将调用一个 ToPrimitive 方法,将其转为基本类型,然后再参照“原始值转字符” 这一节的对应表进行转换。

其实,从对象到数字的转换也是一样:

参数类型结果
Object 1. primValue = ToPrimitive(input, Number)
2. 返回ToNumber(primValue)。

虽然转换成基本值都会使用 ToPrimitive 方法,但传参有不同,最后的处理也有不同,转字符串调用的是 ToString,转数字调用 ToNumber

ToPrimitive

那接下来就要看看 ToPrimitive 了,在了解了 toString 和 valueOf 方法后,这个也很简单。

让我们看规范 9.1,函数语法表示如下:

ToPrimitive(input[, PreferredType])

第一个参数是 input,表示要处理的输入值。

第二个参数是 PreferredType,非必填,表示希望转换成的类型,有两个值可以选,Number 或者 String。

当不传入 PreferredType 时,如果 input 是日期类型,相当于传入 String,否则,都相当于传入 Number。

如果传入的 input 是 Undefined、Null、Boolean、Number、String 类型,直接返回该值。

如果是 ToPrimitive(obj, Number),处理步骤如下:

  1. 如果 obj 为 基本类型,直接返回
  2. 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
  3. 否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。
  4. 否则,JavaScript 抛出一个类型错误异常。

如果是 ToPrimitive(obj, String),处理步骤如下:

  1. 如果 obj为 基本类型,直接返回
  2. 否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。
  3. 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
  4. 否则,JavaScript 抛出一个类型错误异常。

对象转字符串

所以总结下,对象转字符串(就是 Number() 函数)可以概括为:

  1. 如果对象具有 toString 方法,则调用这个方法。如果他返回一个原始值,JavaScript 将这个值转换为字符串,并返回这个字符串结果。
  2. 如果对象没有 toString 方法,或者这个方法并不返回一个原始值,那么 JavaScript 会调用 valueOf 方法。如果存在这个方法,则 JavaScript 调用它。如果返回值是原始值,JavaScript 将这个值转换为字符串,并返回这个字符串的结果。
  3. 否则,JavaScript 无法从 toString 或者 valueOf 获得一个原始值,这时它将抛出一个类型错误异常。

对象转数字

对象转数字的过程中,JavaScript 做了同样的事情,只是它会首先尝试 valueOf 方法

  1. 如果对象具有 valueOf 方法,且返回一个原始值,则 JavaScript 将这个原始值转换为数字并返回这个数字
  2. 否则,如果对象具有 toString 方法,且返回一个原始值,则 JavaScript 将其转换并返回。
  3. 否则,JavaScript 抛出一个类型错误异常。

举个例子:

console.log(Number({})) // NaN
console.log(Number({a : 1})) // NaN

console.log(Number([])) // 0
console.log(Number([0])) // 0
console.log(Number([1, 2, 3])) // NaN
console.log(Number(function(){var a = 1;})) // NaN
console.log(Number(/\d+/g)) // NaN
console.log(Number(new Date(2010, 0, 1))) // 1262275200000
console.log(Number(new Error('a'))) // NaN

注意,在这个例子中,[][0] 都返回了 0,而 [1, 2, 3] 却返回了一个 NaN。我们分析一下原因:

当我们 Number([]) 的时候,先调用 []valueOf 方法,此时返回 [],因为返回了一个对象而不是原始值,所以又调用了 toString 方法,此时返回一个空字符串,接下来调用 ToNumber 这个规范上的方法,参照对应表,转换为 0, 所以最后的结果为 0

而当我们 Number([1, 2, 3]) 的时候,先调用 [1, 2, 3]valueOf 方法,此时返回 [1, 2, 3],再调用 toString 方法,此时返回 1,2,3,接下来调用 ToNumber,参照对应表,因为无法转换为数字,所以最后的结果为 NaN

JSON.stringify

值得一提的是:JSON.stringify() 方法可以将一个 JavaScript 值转换为一个 JSON 字符串,实现上也是调用了 toString 方法,也算是一种类型转换的方法。下面讲一讲JSON.stringify 的注意要点:

1.处理基本类型时,与使用toString基本相同,结果都是字符串,除了 undefined

console.log(JSON.stringify(null)) // null
console.log(JSON.stringify(undefined)) // undefined,注意这个undefined不是字符串的undefined
console.log(JSON.stringify(true)) // true
console.log(JSON.stringify(42)) // 42
console.log(JSON.stringify("42")) // "42"

2.布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。

JSON.stringify([new Number(1), new String("false"), new Boolean(false)]); // "[1,"false",false]"

3.undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。

JSON.stringify({x: undefined, y: Object, z: Symbol("")}); 
// "{}"

JSON.stringify([undefined, Object, Symbol("")]);          
// "[null,null,null]" 

4.JSON.stringify 有第二个参数 replacer,它可以是数组或者函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除。

function replacer(key, value) {
  if (typeof value === "string") {
    return undefined;
  }
  return value;
}

var foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7};
var jsonString = JSON.stringify(foo, replacer);

console.log(jsonString)
// {"week":45,"month":7}
var foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7};
console.log(JSON.stringify(foo, ['week', 'month']));
// {"week":45,"month":7}

5.如果一个被序列化的对象拥有 toJSON 方法,那么该 toJSON 方法就会覆盖该对象默认的序列化行为:不是那个对象被序列化,而是调用 toJSON 方法后的返回值会被序列化,例如:

var obj = {
  foo: 'foo',
  toJSON: function () {
    return 'bar';
  }
};
JSON.stringify(obj);      // '"bar"'
JSON.stringify({x: obj}); // '{"x":"bar"}'

深入系列

JavaScript深入系列目录地址:https://github.com/mqyqingfen...

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

再其他

建立了一个「前端校招面试冲刺互助群」,欢迎加「taoxiaozhao233」 入群~

查看原文

赞 14 收藏 7 评论 2

冴羽 发布了文章 · 2020-03-24

阿里前端攻城狮们写了一份前端面试题答案,请查收

前言

话说一周前,我发了这样一条沸点:

于是我真的就建群收集了题目,和团队的同事一起写答案,我们也不图什么,就是想做一件有意义的事情,现在我整理了下我们的回答,有的不一定就是非常具体的回答,但也提供了思路和参考资料,大家看看是否还有什么补充的?

[高德一面]一个 tcp 连接能发几个 http 请求?

如果是 HTTP 1.0 版本协议,一般情况下,不支持长连接,因此在每次请求发送完毕之后,TCP 连接即会断开,因此一个 TCP 发送一个 HTTP 请求,但是有一种情况可以将一条 TCP 连接保持在活跃状态,那就是通过 Connection 和 Keep-Alive 首部,在请求头带上 Connection: Keep-Alive,并且可以通过 Keep-Alive 通用首部中指定的,用逗号分隔的选项调节 keep-alive 的行为,如果客户端和服务端都支持,那么其实也可以发送多条,不过此方式也有限制,可以关注《HTTP 权威指南》4.5.5 节对于 Keep-Alive 连接的限制和规则。

而如果是 HTTP 1.1 版本协议,支持了长连接,因此只要 TCP 连接不断开,便可以一直发送 HTTP 请求,持续不断,没有上限;
同样,如果是 HTTP 2.0 版本协议,支持多用复用,一个 TCP 连接是可以并发多个 HTTP 请求的,同样也是支持长连接,因此只要不断开 TCP 的连接,HTTP 请求数也是可以没有上限地持续发送

[腾讯一面]Virtual Dom 的优势在哪里?

「Virtual Dom 的优势」其实这道题目面试官更想听到的答案不是上来就说「直接操作/频繁操作 DOM 的性能差」,如果 DOM 操作的性能如此不堪,那么 jQuery 也不至于活到今天。所以面试官更想听到 VDOM 想解决的问题以及为什么频繁的 DOM 操作会性能差。

首先我们需要知道:

DOM 引擎、JS 引擎 相互独立,但又工作在同一线程(主线程)
JS 代码调用 DOM API 必须 挂起 JS 引擎、转换传入参数数据、激活 DOM 引擎,DOM 重绘后再转换可能有的返回值,最后激活 JS 引擎并继续执行若有频繁的 DOM API 调用,且浏览器厂商不做“批量处理”优化,
引擎间切换的单位代价将迅速积累若其中有强制重绘的 DOM API 调用,重新计算布局、重新绘制图像会引起更大的性能消耗。

其次是 VDOM 和真实 DOM 的区别和优化:

  1. 虚拟 DOM 不会立马进行排版与重绘操作
  2. 虚拟 DOM 进行频繁修改,然后一次性比较并修改真实 DOM 中需要改的部分,最后在真实 DOM 中进行排版与重绘,减少过多DOM节点排版与重绘损耗
  3. 虚拟 DOM 有效降低大面积真实 DOM 的重绘与排版,因为最终与真实 DOM 比较差异,可以只渲染局部

[字节跳动] common.js 和 es6 中模块引入的区别?

CommonJS 是一种模块规范,最初被应用于 Nodejs,成为 Nodejs 的模块规范。运行在浏览器端的 JavaScript 由于也缺少类似的规范,在 ES6 出来之前,前端也实现了一套相同的模块规范 (例如: AMD),用来对前端模块进行管理。自 ES6 起,引入了一套新的 ES6 Module 规范,在语言标准的层面上实现了模块功能,而且实现得相当简单,有望成为浏览器和服务器通用的模块解决方案。但目前浏览器对 ES6 Module 兼容还不太好,我们平时在 Webpack 中使用的 export 和 import,会经过 Babel 转换为 CommonJS 规范。在使用上的差别主要有:

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  3. CommonJs 是单个值导出,ES6 Module可以导出多个
  4. CommonJs 是动态语法可以写在判断里,ES6 Module 静态语法只能写在顶层
  5. CommonJs 的 this 是当前模块,ES6 Module的 this 是 undefined

[未知] cookie token 和 session 的区别

这道题绝对不是你回答的点越多就越好。这道题考察的是你对浏览器缓存知识的理解程度,所以你应该回答的是 Cookie、 Session、Token 的产生背景、原理、有什么问题,在回答这个的基础上把差别讲出来。把这些东西答出本质,再加点装逼的东西,再故意拓展讲到你准备的其他内容才是答好这道题的关键,而要理解好这些东西,其实一两天就够了。关于 Cookie,最近还发生了 Chrome80 屏蔽第三方 Cookie 的事件,如果真的问到这个问题,讲到这件事情妥妥的加分项,前提是你对这件事情也有比较深入的了解。关于 Cookie 和这件事情 我写了篇文章 https://github.com/mqyqingfen... 可以看一下。

[头条]如何选择图片格式,例如 png, webp

图片格式压缩方式透明度动画浏览器兼容适应场景
JPEG有损压缩不支持不支持所有复杂颜色及形状、尤其是照片
GIF无损压缩支持支持所有简单颜色,动画
PNG无损压缩支持不支持所有需要透明时
APNG无损压缩支持支持FirefoxSafariiOS Safari需要半透明效果的动画
WebP有损压缩支持支持ChromeOperaAndroid ChromeAndroid Browser复杂颜色及形状浏览器平台可预知
SVG无损压缩支持支持所有(IE8以上)简单图形,需要良好的放缩体验需要动态控制图片特效

[未知]首屏和白屏时间如何计算?

首屏时间的计算,可以由 Native WebView 提供的类似 onload 的方法实现,在 ios 下对应的是 webViewDidFinishLoad,在 android 下对应的是onPageFinished事件。

白屏的定义有多种。可以认为“没有任何内容”是白屏,可以认为“网络或服务异常”是白屏,可以认为“数据加载中”是白屏,可以认为“图片加载不出来”是白屏。场景不同,白屏的计算方式就不相同。

方法1:当页面的元素数小于x时,则认为页面白屏。比如“没有任何内容”,可以获取页面的DOM节点数,判断DOM节点数少于某个阈值X,则认为白屏。
方法2:当页面出现业务定义的错误码时,则认为是白屏。比如“网络或服务异常”。
方法3:当页面出现业务定义的特征值时,则认为是白屏。比如“数据加载中”。

[未知]小程序和 H5 有什么区别?

  1. 渲染方式与 H5 不同,小程序一般是通过 Native 原生渲染的,但是小程序同时也支持 web 渲染,如果使用 web 渲染的方式,我们需要初始化一个WebView 组件,然后在 WebView 中加载 H5 页面;

所以当我们开发一个小程序时,通常会使用 hybrid 的方式,即会根据具体情况选择部分功能用小程序原生的代码来开发,部分功能通过 WebView 加载 H5 页面来实现。Native 与 Web 渲染混合使用,以实现项目的最优解;

这里值得注意的是,小程序下,native 方式通常情况下性能要优于 web 方式。

  1. 小程序特有的双线程设计。 H5 下我们所有资源通常都会打到一个 bundle.js 文件里(不考虑分包加载),而小程序编译后的结果会有两个bundle,index.js封装的是小程序项目的 view 层,以及 index.worker.js 封装的是项目的业务逻辑,在运行时,会有两条线程来分别处理这两个bundle,一个是主渲染线程,它负责加载并渲染 index.js 里的内容,另外一个是 Service Worker线 程,它负责执行 index.worker.js 里封装的业务逻辑,这里面会有很多对底层api调用。

[未知]如何判断 0.1 + 0.2 与 0.3 相等?

作为一道面试题,我觉得重要的是要讲出一点其他人一般不会答出来的深度。像这道题,可以从原理和解决方案两个地方作为答题点,最好在编一个案例。大致讲自己遇到过这个问题,于是很好奇深入研究了一下,发现是浮点数精度导致……原理怎样怎样……然后又看了业界的库的源码,然后怎样怎样解决。

关于原理,我专门写了一篇文章 https://github.com/mqyqingfen... 来解释,实际回答的时候,我觉得答出来

  1. 非是 ECMAScript 独有
  2. IEEE754 标准中 64 位的储存格式,比如 11 位存偏移值
  3. 其中涉及的三次精度丢失

就已经 OK 了。

再讲解决方案,这个可以直接搜索到,各种方案都了解一下,比较一下优劣,还可以参考业界的一些库的实现,比如 math.js,不过相关的我并没有看过……

如果还有精力的话,可以从加法再拓展讲讲超出安全值的数字的计算问题。

所以我觉得一能回答出底层实现,二能回答出多种解决方案的优劣,三能拓展讲讲 bignum 的处理,就是一个非常不错的回答了。

[腾讯二面]了解v8引擎吗,一段js代码如何执行的

在执行一段代码时,JS 引擎会首先创建一个执行栈

然后JS引擎会创建一个全局执行上下文,并push到执行栈中, 这个过程JS引擎会为这段代码中所有变量分配内存并赋一个初始值(undefined),在创建完成后,JS引擎会进入执行阶段,这个过程JS引擎会逐行的执行代码,即为之前分配好内存的变量逐个赋值(真实值)。

如果这段代码中存在function的声明和调用,那么JS引擎会创建一个函数执行上下文,并push到执行栈中,其创建和执行过程跟全局执行上下文一样。但有特殊情况,即当函数中存在对其它函数的调用时,JS引擎会在父函数执行的过程中,将子函数的全局执行上下文push到执行栈,这也是为什么子函数能够访问到父函数内所声明的变量。

还有一种特殊情况是,在子函数执行的过程中,父函数已经return了,这种情况下,JS引擎会将父函数的上下文从执行栈中移除,与此同时,JS引擎会为还在执行的子函数上下文创建一个闭包,这个闭包里保存了父函数内声明的变量及其赋值,子函数仍然能够在其上下文中访问并使用这边变量/常量。当子函数执行完毕,JS引擎才会将子函数的上下文及闭包一并从执行栈中移除。

最后,JS引擎是单线程的,那么它是如何处理高并发的呢?即当代码中存在异步调用时JS是如何执行的。比如setTimeout或fetch请求都是non-blocking的,当异步调用代码触发时,JS引擎会将需要异步执行的代码移出调用栈,直到等待到返回结果,JS引擎会立即将与之对应的回调函数push进任务队列中等待被调用,当调用(执行)栈中已经没有需要被执行的代码时,JS引擎会立刻将任务队列中的回调函数逐个push进调用栈并执行。这个过程我们也称之为事件循环。

附言:需要更深入的了解JS引擎,必须理解几个概念,执行上下文,闭包,作用域,作用域链,以及事件循环。建议去网上多看看相关文章,这里推荐一篇非常精彩的博客,对于JS引擎的执行做了图形化的说明,更加便于理解。

https://tylermcginnis.com/ult...

再其他

建立了一个「前端校招面试冲刺互助群」,欢迎加「taoxiaozhao233」 入群~

查看原文

赞 54 收藏 41 评论 7

冴羽 发布了文章 · 2020-03-18

预测最近面试会考 Cookie 的 SameSite 属性

前言

2 月份发布的 Chrome 80 版本中默认屏蔽了第三方的 Cookie,在灰度期间,就导致了阿里系的很多应用都产生了问题,为此还专门成立了小组,推动各 BU 进行改造,目前阿里系基本已经改造完成。所有的前端团队估计都收到过通知,也着实加深了一把大家对于 Cookie 的理解,所以很可能就此出个面试题,而即便不是面试题,当问到 HTTP 相关内容的时候,不妨也扯到这件事情来,一能表明你对前端时事的跟进,二还能借此引申到前端安全方面的内容,为你的面试加分。

所以本文就给大家介绍一下浏览器的 Cookie 以及这个"火热"的 SameSite 属性。

HTTP

一般我们都会说 “HTTP 是一个无状态的协议”,不过要注意这里的 HTTP 其实是指 HTTP 1.x,而所谓无状态协议,简单的理解就是即使同一个客户端连续两次发送请求给服务器,服务器也识别不出这是同一个客户端发送的请求,这导致的问题就比如你加了一个商品到购物车中,但因为识别不出是同一个客户端,你刷新下页面就没有了……

Cookie

为了解决 HTTP 无状态导致的问题,后来出现了 Cookie。不过这样说可能会让你产生一些误解,首先无状态并不是不好,有优点,但也会导致一些问题。而 Cookie 的存在也不是为了解决通讯协议无状态的问题,只是为了解决客户端与服务端会话状态的问题,这个状态是指后端服务的状态而非通讯协议的状态。

Cookie 介绍

那我们来看下 Cookie,引用下维基百科:

Cookie(复数形态Cookies),类型为「小型文本文件」,指某些网站为了辨别用户身份而储存在用户本地终端上的数据。

作为一段一般不超过 4KB 的小型文本数据,它由一个名称(Name)、一个值(Value)和其它几个用于控制 Cookie 有效期、安全性、使用范围的可选属性组成,这些涉及的属性我们会在后面会介绍。

Cookie 的查看

我们可以在浏览器的开发者工具中查看到当前页面的 Cookie:

尽管我们在浏览器里查看到了 Cookie,这并不意味着 Cookie 文件只是存放在浏览器里的。实际上,Cookies 相关的内容还可以存在本地文件里,就比如说 Mac 下的 Chrome,存放目录就是~/Library/Application Support/Google/Chrome/Default,里面会有一个名为 Cookies 的数据库文件,你可以使用 sqlite 软件打开它:

存放在本地的好处就在于即使你关闭了浏览器,Cookie 依然可以生效。

Cookie 的设置

那 Cookie 是怎么设置的呢?简单来说就是

  1. 客户端发送 HTTP 请求到服务器
  2. 当服务器收到 HTTP 请求时,在响应头里面添加一个 Set-Cookie 字段
  3. 浏览器收到响应后保存下 Cookie
  4. 之后对该服务器每一次请求中都通过 Cookie 字段将 Cookie 信息发送给服务器。

我们以https://main.m.taobao.com/为例来看下这个过程:

我们在请求返回的 Response Headers 可以看到 Set-Cookie 字段:

然后我们查看下 Cookie:

我们刷新一遍页面,再看下这个请求,可以在 Request Headers 看到 cookie 字段:

Cookies 的属性

在下面这张图里我们可以看到 Cookies 相关的一些属性:

这里主要说一些大家可能没有注意的点:

Name/Value

用 JavaScript 操作 Cookie 的时候注意对 Value 进行编码处理。

Expires

Expires 用于设置 Cookie 的过期时间。比如:

Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;

当 Expires 属性缺省时,表示是会话性 Cookie,像上图 Expires 的值为 Session,表示的就是会话性 Cookie。当为会话性 Cookie 的时候,值保存在客户端内存中,并在用户关闭浏览器时失效。需要注意的是,有些浏览器提供了会话恢复功能,这种情况下即使关闭了浏览器,会话期 Cookie 也会被保留下来,就好像浏览器从来没有关闭一样。

与会话性 Cookie 相对的是持久性 Cookie,持久性 Cookies 会保存在用户的硬盘中,直至过期或者清除 Cookie。这里值得注意的是,设定的日期和时间只与客户端相关,而不是服务端。

Max-Age

Max-Age 用于设置在 Cookie 失效之前需要经过的秒数。比如:

Set-Cookie: id=a3fWa; Max-Age=604800;

Max-Age 可以为正数、负数、甚至是 0。

如果 max-Age 属性为正数时,浏览器会将其持久化,即写到对应的 Cookie 文件中。

当 max-Age 属性为负数,则表示该 Cookie 只是一个会话性 Cookie。

当 max-Age 为 0 时,则会立即删除这个 Cookie。

假如 Expires 和 Max-Age 都存在,Max-Age 优先级更高。

Domain

Domain 指定了 Cookie 可以送达的主机名。假如没有指定,那么默认值为当前文档访问地址中的主机部分(但是不包含子域名)。

像淘宝首页设置的 Domain 就是 .taobao.com,这样无论是a.taobao.com还是b.taobao.com都可以使用 Cookie。

在这里注意的是,不能跨域设置 Cookie,比如阿里域名下的页面把 Domain 设置成百度是无效的:

Set-Cookie: qwerty=219ffwef9w0f; Domain=baidu.com; Path=/; Expires=Wed, 30 Aug 2020 00:00:00 GMT

Path

Path 指定了一个 URL 路径,这个路径必须出现在要请求的资源的路径中才可以发送 Cookie 首部。比如设置Path=/docs/docs/Web/下的资源会带 Cookie 首部,/test则不会携带 Cookie 首部。

Domain 和 Path 标识共同定义了 Cookie 的作用域:即 Cookie 应该发送给哪些 URL。

Secure属性

标记为 Secure 的 Cookie 只应通过被HTTPS协议加密过的请求发送给服务端。使用 HTTPS 安全协议,可以保护 Cookie 在浏览器和 Web 服务器间的传输过程中不被窃取和篡改。

HTTPOnly

设置 HTTPOnly 属性可以防止客户端脚本通过 document.cookie 等方式访问 Cookie,有助于避免 XSS 攻击。

SameSite

SameSite 是最近非常值得一提的内容,因为 2 月份发布的 Chrome80 版本中默认屏蔽了第三方的 Cookie,这会导致阿里系的很多应用都产生问题,为此还专门成立了问题小组,推动各 BU 进行改造。

作用

我们先来看看这个属性的作用:

SameSite 属性可以让 Cookie 在跨站请求时不会被发送,从而可以阻止跨站请求伪造攻击(CSRF)。

属性值

SameSite 可以有下面三种值:

  1. Strict仅允许一方请求携带 Cookie,即浏览器将只发送相同站点请求的 Cookie,即当前网页 URL 与请求目标 URL 完全一致。
  2. Lax允许部分第三方请求携带 Cookie
  3. None无论是否跨站都会发送 Cookie

之前默认是 None 的,Chrome80 后默认是 Lax。

跨域和跨站

首先要理解的一点就是跨站和跨域是不同的。同站(same-site)/跨站(cross-site)」和第一方(first-party)/第三方(third-party)是等价的。但是与浏览器同源策略(SOP)中的「同源(same-origin)/跨域(cross-origin)」是完全不同的概念。

同源策略的同源是指两个 URL 的协议/主机名/端口一致。例如,https://www.taobao.com/pages/...,它的协议是 https,主机名是www.taobao.com,端口是 443。

同源策略作为浏览器的安全基石,其「同源」判断是比较严格的,相对而言,Cookie中的「同站」判断就比较宽松:只要两个 URL 的 eTLD+1 相同即可,不需要考虑协议和端口。其中,eTLD 表示有效顶级域名,注册于 Mozilla 维护的公共后缀列表(Public Suffix List)中,例如,.com、.co.uk、.github.io 等。eTLD+1 则表示,有效顶级域名+二级域名,例如taobao.com等。

举几个例子,www.taobao.comwww.baidu.com是跨站,www.a.taobao.comwww.b.taobao.com是同站,a.github.iob.github.io是跨站(注意是跨站)。

改变

接下来看下从 None 改成 Lax 到底影响了哪些地方的 Cookies 的发送?直接来一个图表:

从上图可以看出,对大部分 web 应用而言,Post 表单,iframe,AJAX,Image 这四种情况从以前的跨站会发送三方 Cookie,变成了不发送。

Post表单:应该的,学 CSRF 总会举表单的例子。

iframe:iframe 嵌入的 web 应用有很多是跨站的,都会受到影响。

AJAX:可能会影响部分前端取值的行为和结果。

Image:图片一般放 CDN,大部分情况不需要 Cookie,故影响有限。但如果引用了需要鉴权的图片,可能会受到影响。

除了这些还有 script 的方式,这种方式也不会发送 Cookie,像淘宝的大部分请求都是 jsonp,如果涉及到跨站也有可能会被影响。

问题

我们再看看会出现什么的问题?举几个例子:

  1. 天猫和飞猪的页面靠请求淘宝域名下的接口获取登录信息,由于 Cookie 丢失,用户无法登录,页面还会误判断成是由于用户开启了浏览器的“禁止第三方 Cookie”功能导致而给与错误的提示
  2. 淘宝部分页面内嵌支付宝确认付款和确认收货页面、天猫内嵌淘宝的登录页面等,由于 Cookie 失效,付款、登录等操作都会失败
  3. 阿里妈妈在各大网站比如今日头条,网易,微博等投放的广告,也是用 iframe 嵌入的,没有了 Cookie,就不能准确的进行推荐
  4. 一些埋点系统会把用户 id 信息埋到 Cookie 中,用于日志上报,这种系统一般走的都是单独的域名,与业务域名分开,所以也会受到影响。
  5. 一些用于防止恶意请求的系统,对判断为恶意请求的访问会弹出验证码让用户进行安全验证,通过安全验证后会在请求所在域种一个Cookie,请求中带上这个Cookie之后,短时间内不再弹安全验证码。在Chrome80以上如果因为Samesite的原因请求没办法带上这个Cookie,则会出现一直弹出验证码进行安全验证。
  6. 天猫商家后台请求了跨域的接口,因为没有 Cookie,接口不会返回数据
  7. ……

如果不解决,影响的系统其实还是很多的……

解决

解决方案就是设置 SameSite 为 none。

以 Adobe 网站为例:https://www.adobe.com/sea/,查看请求可以看到:

不过也会有两点要注意的地方:

  1. HTTP 接口不支持 SameSite=none

如果你想加 SameSite=none 属性,那么该 Cookie 就必须同时加上 Secure 属性,表示只有在 HTTPS 协议下该 Cookie 才会被发送。

  1. 需要 UA 检测,部分浏览器不能加 SameSite=none

IOS 12 的 Safari 以及老版本的一些 Chrome 会把 SameSite=none 识别成 SameSite=Strict,所以服务端必须在下发 Set-Cookie 响应头时进行 User-Agent 检测,对这些浏览器不下发 SameSite=none 属性

Cookie 的作用

Cookie 主要用于以下三个方面:

  1. 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  2. 个性化设置(如用户自定义设置、主题等)
  3. 浏览器行为跟踪(如跟踪分析用户行为等)

Cookie 的缺点

如果被问到话,可以从大小、安全、增加请求大小等方面回答。

参考

  1. MDN
  2. HTTP是一个无状态的协议。这句话里的无状态是什么意思? - 灵剑的回答 - 知乎
  3. Chrome 80.0中将SameSite的默认值设为Lax,对现有的Cookie使用有什么影响? - 紫云飞的回答 - 知乎
  4. 一些内部文章

各种系列

各种系列文章目录地址:https://github.com/mqyqingfeng/Blog

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者 有所启发,欢迎 star,对作者也是一种鼓励。

再其他

建立了一个「前端校招面试冲刺互助群」,欢迎加「taoxiaozhao233」 入群~

查看原文

赞 29 收藏 20 评论 3

冴羽 发布了文章 · 2020-03-16

JavaScript 深入系列之浮点数精度

前言

0.1 + 0.2 是否等于 0.3 作为一道经典的面试题,已经广外熟知,说起原因,大家能回答出这是浮点数精度问题导致,也能辩证的看待这并非是 ECMAScript 这门语言的问题,今天就是具体看一下背后的原因。

数字类型

ECMAScript 中的 Number 类型使用 IEEE754 标准来表示整数和浮点数值。所谓 IEEE754 标准,全称 IEEE 二进制浮点数算术标准,这个标准定义了表示浮点数的格式等内容。

在 IEEE754 中,规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度、与延伸双精确度。像 ECMAScript 采用的就是双精确度,也就是说,会用 64 位字节来储存一个浮点数。

浮点数转二进制

我们来看下 1020 用十进制的表示:

1020 = 1 * 10^3 + 0 * 10^2 + 2 * 10^1 + 0 * 10^0

所以 1020 用十进制表示就是 1020……(哈哈)

如果 1020 用二进制来表示呢?

1020 = 1 * 2^9 + 1 * 2^8 + 1 * 2^7 + 1 * 2^6 + 1 * 2^5 + 1 * 2^4 + 1 * 2^3 + 1 * 2^2 + 0 * 2^1 + 0 * 2^0

所以 1020 的二进制为 1111111100

那如果是 0.75 用二进制表示呢?同理应该是:

0.75 = a * 2^-1 + b * 2^-2 + c * 2^-3 + d * 2^-4 + ...

因为使用的是二进制,这里的 abcd……的值的要么是 0 要么是 1。

那怎么算出 abcd…… 的值呢,我们可以两边不停的乘以 2 算出来,解法如下:

0.75 = a * 2^-1 + b * 2^-2 + c * 2^-3 + d * 2^-4...

两边同时乘以 2

1 + 0.5 = a * 2^0 + b * 2^-1 + c * 2^-2 + d * 2^-3... (所以 a = 1)

剩下的:

0.5 = b * 2^-1 + c * 2^-2 + d * 2^-3...

再同时乘以 2

1 + 0 = b * 2^0 + c * 2^-2 + d * 2^-3... (所以 b = 1)

所以 0.75 用二进制表示就是 0.ab,也就是 0.11

然而不是所有的数都像 0.75 这么好算,我们来算下 0.1:

0.1 = a * 2^-1 + b * 2^-2 + c * 2^-3 + d * 2^-4 + ...

0 + 0.2 = a * 2^0 + b * 2^-1 + c * 2^-2 + ...   (a = 0)
0 + 0.4 = b * 2^0 + c * 2^-1 + d * 2^-2 + ...   (b = 0)
0 + 0.8 = c * 2^0 + d * 2^-1 + e * 2^-2 + ...   (c = 0)
1 + 0.6 = d * 2^0 + e * 2^-1 + f * 2^-2 + ...   (d = 1)
1 + 0.2 = e * 2^0 + f * 2^-1 + g * 2^-2 + ...   (e = 1)
0 + 0.4 = f * 2^0 + g * 2^-1 + h * 2^-2 + ...   (f = 0)
0 + 0.8 = g * 2^0 + h * 2^-1 + i * 2^-2 + ...   (g = 0)
1 + 0.6 = h * 2^0 + i * 2^-1 + j * 2^-2 + ...   (h = 1)
....

然后你就会发现,这个计算在不停的循环,所以 0.1 用二进制表示就是 0.00011001100110011……

浮点数的存储

虽然 0.1 转成二进制时是一个无限循环的数,但计算机总要储存吧,我们知道 ECMAScript 使用 64 位字节来储存一个浮点数,那具体是怎么储存的呢?这就要说回 IEEE754 这个标准了,毕竟是这个标准规定了存储的方式。

这个标准认为,一个浮点数 (Value) 可以这样表示:

Value = sign * exponent * fraction

看起来很抽象的样子,简单理解就是科学计数法……

比如 -1020,用科学计数法表示就是:

-1 * 10^3 * 1.02

sign 就是 -1,exponent 就是 10^3,fraction 就是 1.02

对于二进制也是一样,以 0.1 的二进制 0.00011001100110011…… 这个数来说:

可以表示为:

1 * 2^-4 * 1.1001100110011……

其中 sign 就是 1,exponent 就是 2^-4,fraction 就是 1.1001100110011……

而当只做二进制科学计数法的表示时,这个 Value 的表示可以再具体一点变成:

V = (-1)^S * (1 + Fraction) * 2^E

(如果所有的浮点数都可以这样表示,那么我们存储的时候就把这其中会变化的一些值存储起来就好了)

我们来一点点看:

(-1)^S 表示符号位,当 S = 0,V 为正数;当 S = 1,V 为负数。

再看 (1 + Fraction),这是因为所有的浮点数都可以表示为 1.xxxx * 2^xxx 的形式,前面的一定是 1.xxx,那干脆我们就不存储这个 1 了,直接存后面的 xxxxx 好了,这也就是 Fraction 的部分。

最后再看 2^E

如果是 1020.75,对应二进制数就是 1111111100.11,对应二进制科学计数法就是 1 1.11111110011 2^9,E 的值就是 9,而如果是 0.1 ,对应二进制是 1 1.1001100110011…… 2^-4, E 的值就是 -4,也就是说,E 既可能是负数,又可能是正数,那问题就来了,那我们该怎么储存这个 E 呢?

我们这样解决,假如我们用 8 位字节来存储 E 这个数,如果只有正数的话,储存的值的范围是 0 ~ 254,而如果要储存正负数的话,值的范围就是 -127~127,我们在存储的时候,把要存储的数字加上 127,这样当我们存 -127 的时候,我们存 0,当存 127 的时候,存 254,这样就解决了存负数的问题。对应的,当取值的时候,我们再减去 127。

所以呢,真到实际存储的时候,我们并不会直接存储 E,而是会存储 E + bias,当用 8 个字节的时候,这个 bias 就是 127。

所以,如果要存储一个浮点数,我们存 S 和 Fraction 和 E + bias 这三个值就好了,那具体要分配多少个字节位来存储这些数呢?IEEE754 给出了标准:

IEEE754

在这个标准下:

我们会用 1 位存储 S,0 表示正数,1 表示负数。

用 11 位存储 E + bias,对于 11 位来说,bias 的值是 2^(11-1) - 1,也就是 1023。

用 52 位存储 Fraction。

举个例子,就拿 0.1 来看,对应二进制是 1 1.1001100110011…… 2^-4, Sign 是 0,E + bias 是 -4 + 1023 = 1019,1019 用二进制表示是 1111111011,Fraction 是 1001100110011……

对应 64 个字节位的完整表示就是:

0 01111111011 1001100110011001100110011001100110011001100110011010

同理, 0.2 表示的完整表示是:

0 01111111100 1001100110011001100110011001100110011001100110011010

所以当 0.1 存下来的时候,就已经发生了精度丢失,当我们用浮点数进行运算的时候,使用的其实是精度丢失后的数。

浮点数的运算

关于浮点数的运算,一般由以下五个步骤完成:对阶、尾数运算、规格化、舍入处理、溢出判断。我们来简单看一下 0.1 和 0.2 的计算。

首先是对阶,所谓对阶,就是把阶码调整为相同,比如 0.1 是 1.1001100110011…… * 2^-4,阶码是 -4,而 0.2 就是 1.10011001100110...* 2^-3,阶码是 -3,两个阶码不同,所以先调整为相同的阶码再进行计算,调整原则是小阶对大阶,也就是 0.1 的 -4 调整为 -3,对应变成 0.11001100110011…… * 2^-3

接下来是尾数计算:

  0.1100110011001100110011001100110011001100110011001101
+ 1.1001100110011001100110011001100110011001100110011010
————————————————————————————————————————————————————————
 10.0110011001100110011001100110011001100110011001100111

我们得到结果为 10.0110011001100110011001100110011001100110011001100111 * 2^-3

将这个结果处理一下,即结果规格化,变成 1.0011001100110011001100110011001100110011001100110011(1) * 2^-2

括号里的 1 意思是说计算后这个 1 超出了范围,所以要被舍弃了。

再然后是舍入,四舍五入对应到二进制中,就是 0 舍 1 入,因为我们要把括号里的 1 丢了,所以这里会进一,结果变成

1.0011001100110011001100110011001100110011001100110100 * 2^-2

本来还有一个溢出判断,因为这里不涉及,就不讲了。

所以最终的结果存成 64 位就是

0 01111111101 0011001100110011001100110011001100110011001100110100

将它转换为10进制数就得到 0.30000000000000004440892098500626

因为两次存储时的精度丢失加上一次运算时的精度丢失,最终导致了 0.1 + 0.2 !== 0.3

其他

// 十进制转二进制
parseFloat(0.1).toString(2);
=> "0.0001100110011001100110011001100110011001100110011001101"

// 二进制转十进制
parseInt(1100100,2)
=> 100

// 以指定的精度返回该数值对象的字符串表示
(0.1 + 0.2).toPrecision(21)
=> "0.300000000000000044409"
(0.3).toPrecision(21)
=> "0.299999999999999988898"

参考

  1. why is 0.1+0.2 not equal to 0.3 in most programming languages
  2. IEEE-754标准与浮点数运算

深入系列

JavaScript深入系列目录地址:https://github.com/mqyqingfen...

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

再其他

建立了一个「前端校招面试冲刺互助群」,欢迎加「taoxiaozhao233」 入群~

查看原文

赞 7 收藏 5 评论 2

认证与成就

  • 获得 2274 次点赞
  • 获得 6 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • React-admin 脚手架

    基于React + ant-design的用作后台管理项目的脚手架

  • 移动端开发脚手架

    基于gulp的使用zepto、handlebars、sass并且带移动端适配方案(flexible.js)的前端工作流,旨在帮助移动端项目的开发

注册于 2016-07-14
个人主页被 22.5k 人浏览