linong

linong 查看完整档案

北京编辑  |  填写毕业院校优贝在线  |  前端开发工程师 编辑 www.lilnong.top/ 编辑
编辑

Read-Search-Ask
做技术的时候,往往不是我想怎么样就怎么样。而是在现有场景下我可以怎么样。
赢了大吉大利,输了再接再厉

微信公众号:前端linong OTIwMTEwNjMzQHFxLmNvbQ==

个人动态

linong 关注了问题 · 2 分钟前

js获取电子秤数据

想问下js能获取到链接电脑的电子秤的实时重量数据吗? 通过什么获取呢?

关注 2 回答 0

linong 关注了问题 · 3 分钟前

iframe和新窗口有何区别?

求教iframe和新窗口有什么区别?

关注 2 回答 0

linong 关注了问题 · 3 分钟前

一种前端解析html字符串片段的方案求教

规则制定


<h1></h1>
::content1
  <h2></h2>
    ::content2
    content2::
  <h2></h2> 
    ::content2
      <h3></h3> 
      ::content3
        <h4></h4>
        ::content4
        content4::
      content3::
    content2::
  <h2></h2>
  <h3></h3>
content1::
<h1></h1>
content1::
  <h2></h2>
  content2::
  ::content2
  <h2></h2>
  content2::
  ::content2
::content1

规则如上, 我需要解析标签和内容的对应结构。
例如: H1对应::content1 结尾content1:: 中间的内容 依次类推。
从里层到外层或者从外层到里层的递归。
如果content1层级对应里没有H2-H6标签,则该content的内容全部为该H1标签对应的内容。以此形成对应键值对结构。如果content里没有下级H标签,则此内容为最内层内容。

请教大神有无算法方案。

关注 2 回答 0

linong 回答了问题 · 3 分钟前

el-input type="textarea"字数问题

是在提需求嘛?

image.png

关注 2 回答 1

linong 关注了问题 · 4月15日

解决css关于展开/收起动画 height overflow 高度100%无法产生动画问题

使用vue项目中想通过高度变化对某块元素显示展开收起效果
由于是长列表, 所以数据为遍历出来的list, 设置一个默认高度比如50px, 当点击展开时高度展开为100%, 来回切换展开收起都产生动画效果
image.png

image.png
收起1的收起和展开为固定max-height 双向切换动画可以成功,
收起2设置展开时为100%时 切换动画单向无效
预览效果dome: http://jsrun.net/Yv3Kp/edit

关注 3 回答 1

linong 回答了问题 · 4月15日

node如何处理前端传送的数组。

image.png

你应该处理过数据,所以你直接用json最好了。支持。。

axios({
    url: 'https://www.lilnong.top/cors/1010000039822562',
    method: 'post',
    data:{
        channel: '头条',
        list: [
            {txt: '前端linong'},
            {txt: '微信公众号:前端linong'},
            {txt: 'segmentfault.com/u/linong'}
        ]
    }
}).then(v=>v.data).then(console.log)

关注 2 回答 1

linong 关注了问题 · 4月15日

在一个html页面中,通过v-html插入另一个html页面,如果限制第二个html页面的缩放范围?

在页面A中的div元素B(有固定宽髙),通过v-html渲染了另一个html页面C。
要对这个新插入的页面C增加缩放功能,但是缩放范围仅局限在B元素的范围内,不影响整个A页面。
页面C本身是无法缩放的,通过增加meta name='viewport' content='xxx' 标签实现了缩放功能。但是进而影响到了整个页面。A页面也变得可以进行缩放了。
有什么方法可以只让页面C在div元素B范围内进行缩放,而不影响到整个页面么?

或者有没有什么办法可以限制meta标签的有效范围?

关注 3 回答 1

linong 回答了问题 · 4月15日

js中image对象如何中止加载请求(低网速下加载时间过长)

正常场景下,图片资源不会阻塞界面渲染

所以肯定是js上有控制,那么问题其实就变成如何把串行改成并行,或者让图片资源不阻塞界面渲染即可。

比如说你可以把图片资源懒加载,先绘制首屏,然后onload之后,再把视口图片展示。

当然你还可以考虑使用 AJAX (XMLHttpRequest) 去请求,他不是有个abort的方法嘛。(但是内部具体实现就还得抓包测试了)

关注 3 回答 2

linong 回答了问题 · 4月15日

两个数组怎么去匹配删除没有的项

你还过滤干嘛。你直接赋值不就好了

当然,你非要用的话,你可以用 双层遍历的方式。如果你对这些不熟的话,你可以搞个库,比如lodash,写起来简单,还少。

image.png

关注 4 回答 5

linong 赞了回答 · 4月15日

两个数组怎么去匹配删除没有的项

两年了哥,为什么你在思否问了两年这么基础的问题?
但凡你用点心,都省去多少提问的功夫了。

关注 4 回答 5

linong 关注了问题 · 4月14日

nginx处理跨域 当status_code 不等于200时不添加头部

我的nginx配置如下,我发现在status_code 200的时候会返回add_header
如果是其他状态码就不会返回,请问是我哪里配置有问题么?

server{
    listen  80;
    server_name ppt.com;
    root    /public;
    index index.html index.php;

    location ~ \.php$ {
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }
        
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS,PUT,DELETE';
    add_header Access-Control-Allow-Headers '*';
 
    if ($request_method = 'OPTIONS') {
        return 204;
    }

    location / { 
        try_files $uri $uri/ /index.php?$query_string;
    } 

    access_log  ./nginx/ppt.server.log;
    error_log  ./nginx/ppt.server.error.log;
}

关注 2 回答 0

linong 赞了文章 · 4月14日

向往的社区@2050 召集令,期待对社区运营感兴趣的你来和我们团聚!

社区发展至今,不免许多问题被越来越多的社区工作者所关注和讨论,包括“社区全球化与社区本地化之间的矛盾”、“社区综合化和社区垂直化的纷争”等等,今天“向往的社区@2050”希望组织各类型社区的参与者对社区这一话题进行讨论。

什么是 2050?

2050 大会是由阿里云创始人王坚博士和志愿者共同发起的一场让年青人因科技而团聚的大会。

年青人可以自己站上舞台来讲,他们在想什么,他们想做什么。这也是一场没有设定议题的大会,上百名志愿出品人带来的数百场丰富的主题论坛:航空航天人工智能量子计算无人驾驶创新创业女性的力量…… 我们相遇在 2050,共同激发创新灵感与未来思考。

2050 是一场要一直办到 2050 年的大会。时间上的漫长跨度,使得 2050 的活动能不断延续、生长。往年 2050 的很多活动,在 2050 结束后还有继续;今年的 2050,他们也会回来,为故事续写新的篇章。

“来 2050,不是来参加会议,而是来创造一些东西。”年青人因科技而团聚,让越是不可能见面的人见了面,就越有奇迹会发生。

今年的 2050 准备了三天三夜的精彩活动,4 月 23 日白天有 50 场青年团聚,在这里你能找到志同道合的朋友,晚上有团聚之夜音乐会以及百城连线,为无法参与本次 2050 的海外朋友提供了团聚的场所;24 日和 25 日除了有精彩的新生论坛外,还有星空露营鲤鱼山刷山逐日晨跑足球风暴等等有意思的活动.......总之,来到 2050,只有你想不到没有你做不到,了解更多活动安排:如何在云栖小镇 HIGH 三天三夜

时间:2021 年 4 月 23 日 2 点 ~4 月 25 日下午 5 点
地点:杭州.云栖小镇 & 全球 100 个城市连线 48 小时

为什么我们要举办“向往的社区@2050”团聚?

“社区”一直是社会学家们热衷于研究和探讨的概念,它起初是“聚居在一定地域范围内的人们所组成的社会生活共同体”,是实体组织。随着互联网特别是 Web2.0 时代的发展,越来越多的虚拟社区成立,社区成为了人们日常生活中不可或缺的一部分,可以说每个人既是他所处的地域所形成的实体社区的一部分,也是他的兴趣带来的虚拟社区的一部分。

社区发展至今,不免许多问题被越来越多的社区工作者所关注和讨论,包括“社区的价值定位”、“社区发展趋势:综合化 or 垂直化?”、“如何促进社区用户活跃?”、“社区商业化难题”等等……今年“向往的社区@2050”希望组织各类型社区的参与者对社区这一话题进行讨论。

在活动中,你既可以使聆听者也可以是分享者,你可以深入于某一具体话题,最终本次活动也会成为一个 Community,每个人都成为了这个限定的小社区中的一份子,大家带着观点而来,带着收获而走。

团聚召集人:阿里云开发者关系负责人孟晋宇,SegmentFault 思否 COO 江波
团聚时间:2021 年 4 月 23 日下午 14-18 点
地点:云栖小镇博悟馆(具体楼层待更新)

如何参加 2050 及本次社区团聚?

Step 1:获取你的 2050Pass
Step 2:通过本表单报名向往的社区@2050 活动
Step 3:填写完表单后将有专门的工作人员与你取得联系
Step 4:准备好的你行程准备出发吧

本次活动场地位于浙江省杭州市云栖小镇,可以自驾或乘坐公共交通前往
小提示:你可以根据自己的情况选择 2 天及 3 天通票,3 天更便宜哦~另外你还可以自选是否要参加星空露营(即居住在 2050 营地的帐篷中)及逐日晨跑活动

向往的社区@2050 怎么玩?

4 月 23 日下午 14:00 开始,召集人会提前准备 3-4 个社区相关话题,每个话题都有 1 位领论员进行立论,参与者可以就领论员的观点进行进一步展开或阐述不同思想。每个话题讨论 30-40 分钟,讨论结束后领论员应输出总结观点,为讨论画上句号。

惊喜贴纸:每个人入场时都会收到 5 个惊喜贴纸,你可以把惊喜贴纸给到你认为输出了惊喜观点的参与者,领论员有额外的 5 个惊喜贴纸,可以分配给参与其负责话题讨论的同学。参与者可以使用惊喜贴纸在离场时换取相应的礼品。

金句之王:活动结束后,每个话题的主持人可以评选一个本话题的金句之王,金句之王所说的“金句”将被制作成纪念品,送给获奖者。

待定的话题(你可以继续向我们提交 idea):

  • 议题一:社区在互联网版图中的价值定位未来发展方向
  • 议题二:社区发展趋势:综合化还是垂直化
  • 议题三:如何做好社区里的社交回报来增加用户活跃?或社区规模实现持续增长的关键机制是什么?
  • 议题四:社区运营人如何处理商业化等难题带来的的挑战?

活动流程:

  • 13:00 - 14:00 签到,参与者入场后领取“惊喜贴纸”并入坐
  • 14:00 - 14:10 活动发起人致辞,宣布本次活动主旨及玩法
  • 14:10 - 16:50 话题讨论,除了激烈的观点碰撞外,我们还会为大家准备精美的茶点
  • 16:50 - 18:00 你可以继续留在活动场地进行交流,也可以体验活动场地中的产品
  • 18:00 - 21:00 团聚之夜音乐会等其他活动等你参加

4 月 24 - 25 日我还可以做些什么?

2050 是一个三天都可以放肆 HIGH 的活动,除了 4 月 23 日的青年团聚你可以参加我们的话题讨论外,还有意外的精彩活动等你来参与,具体详情及更多活动请见 2050 官网

4 月 24 日和 4 月 25 日整天:50 场新生论坛,每个论坛只有三十分钟,内容精华,汇成一场科技的饕餮大餐。
4 月 23 日-4 月 25 日展览:海洋艺术展、涡喷发动机点火、919 舱段、磁悬浮列车……
4 月 23 日-4 月 25 日户外活动:48 小时无人机比赛、露天影院、飞过云栖 、又见纸飞机、星空露营、鲤鱼山刷山、逐日晨跑、足球风暴……

其他值得参观的地标:2050 创世届纪念、2050 倒计时塔、2050 时光隧道、2050 博悟馆、云栖小镇展馆、杭州城市大脑运营指挥中心(需提前一周预约)

我们在 2050 等你来!

我是小尾巴:来自召集人江波的一点碎碎念

如何做好一个技术社区如何更好地服务开发者是我个人和 SegmentFault 思否永恒的追求,我希望通过活动汲取来自各个社区的营养,也希望通过活动让大家彼此交流、共同成长。
2021 年 6 月 5 日,SegmentFault 还将在北京举办中国开发者生态峰会,技术社区的运营也是其中一个重要的方向。希望这次聚会可以作为我出品 6 月大会的一次预演,也为我提供新的思路和启迪,并在中国开发者生态峰会上为大家奉上一场充实、有所成长的技术运营人盛宴。

查看原文

赞 6 收藏 0 评论 0

linong 回答了问题 · 4月14日

原生js如何像layui表单那样更新渲染?

重新给所有的select赋值一下就好了。

selects.forEach(v=>{
    v.value = 'aaa'
})

大概就是这么个意思吧,重新把数据渲染一遍。当然,也有可能是直接 innerHTML=

关注 2 回答 1

linong 回答了问题 · 4月14日

vue 想实现类似于如下可折叠多选选择器,有合适的插件吗?

普通的 elementui 就可以满足。

点多选只是把 el-checked 显示出来。

收起、展开 只是把宽高放开

关注 3 回答 2

linong 回答了问题 · 4月14日

解决大佬们 有谁搞过类似的需求啊,一脸蒙蔽了

多少来点文字描述呀。

看上去是个树状结构,那么可以有tree组件来搞。

换一种看法就是有箭头,像步骤条,那么就是Steps

如果上面的都不是,那么看什么上游下游,有可能级联菜单

image.png

image.png

image.png

关注 2 回答 2

linong 关注了问题 · 4月14日

解决要写一个登录界面,设计给我的图是1920*1080的,但是浏览器窗口部分最大高度只有930,要怎么写呢?

不借助其他库(bootstrap等)的情况下 实现.
第一次写浏览器适配,给我个思路或者能搜索的关键词就行. 谢谢大佬们

现在的问题是,在全屏下marginTop + elementHeight + marginBottom > 1080 也就是说marginTopmarginBottom只能顾一个, 导致页面布局不对.

关注 3 回答 1

linong 关注了问题 · 4月14日

微信不能上传

fileInput.multiple = 'multiple';

这样的,微信里面不能上传,点了上传按钮后是显示无应用可执行。

请问最简单的办法是怎要解决。

关注 2 回答 0

linong 赞了文章 · 4月13日

17K star 仓库,解决 90% 的大厂基础面试题

前言

笔者开源的前端进阶之道已有三年之久,至今也有 17k star,承蒙各位读者垂爱。在当下部分内容已经略微过时,因此决定提笔翻新内容。

翻新后的内容会全部集合在「干爆前端」中,有兴趣的读者可以前往查看。

阅读前重要提示:

本文非百科全书,只专为面试复习准备、查漏补缺、深入某知识点的引子、了解相关面试题等准备。

笔者一直都是崇尚学会面试题底下涉及到的知识点,而不是刷一大堆面试题,结果变了个题型就不会的那种。所以本文和别的面经不一样,旨在提炼面试题底下的常用知识点,而不是甩一大堆面试题给各位看官。

大家也可以在笔者的网站上阅读,体验更佳!

数据类型

JS 数据类型分为两大类,九个数据类型:

  1. 原始类型
  2. 对象类型

其中原始类型又分为七种类型,分别为:

  • boolean
  • number
  • string
  • undefined
  • null
  • symbol
  • bigint

对象类型分为两种,分别为:

  • Object
  • Function

其中 Object 中又包含了很多子类型,比如 ArrayRegExpMathMapSet 等等,也就不一一列出了。

原始类型存储在栈上,对象类型存储在堆上,但是它的引用地址还是存在栈上。

注意:以上结论前半句是不准确的,更准确的内容我会在闭包章节里说明。

常见考点

  • JS 类型有哪些?
  • 大数相加、相乘算法题,可以直接使用 bigint,当然再加上字符串的处理会更好。
  • NaN 如何判断

另外还有一类常见的题目是对于对象的修改,比如说往函数里传一个对象进去,函数内部修改参数。

function test(person) {
  person.age = 26
  person = {}

  return person
}
const p1 = {
  age: 25
}

这类题目我们只需要牢记以下几点:

  1. 对象存储的是引用地址,传来传去、赋值给别人那都是在传递值(存在栈上的那个内容),别人一旦修改对象里的属性,大家都被修改了。
  2. 但是一旦对象被重新赋值了,只要不是原对象被重新赋值,那么就永远不会修改原对象。

类型判断

类型判断有好几种方式。

typeof

原始类型中除了 null,其它类型都可以通过 typeof 来判断。

typeof null 的值为 object,这是因为一个久远的 Bug,没有细究的必要,了解即可。如果想具体判断 null 类型的话直接 xxx === null 即可。

对于对象类型来说,typeof 只能具体判断函数的类型为 function,其它均为 object

instanceof

instanceof 内部通过原型链的方式来判断是否为构建函数的实例,常用于判断具体的对象类型。

[] instanceof Array

都说 instanceof 只能判断对象类型,其实这个说法是不准确的,我们是可以通过 hake 的方式得以实现,虽然不会有人这样去玩吧。

class CheckIsNumber {
  static [Symbol.hasInstance](number) {
    return typeof number === 'number'
  }
}

// true
1 instanceof CheckIsNumber

另外其实我们还可以直接通过构建函数来判断类型:

// true
[].constructor === Array

Object.prototype.toString

前几种方式或多或少都存在一些缺陷,Object.prototype.toString 综合来看是最佳选择,能判断的类型最完整。

上图是一部分类型判断,更多的就不列举了,[object XXX] 中的 XXX 就是判断出来的类型。

isXXX API

同时还存在一些判断特定类型的 API,选了两个常见的:

常见考点

  • JS 类型如何判断,有哪几种方式可用
  • instanceof 原理
  • 手写 instanceof

类型转换

类型转换分为两种情况,分别为强制转换及隐式转换。

强制转换

强制转换就是转成特定的类型:

Number(false) // -> 0
Number('1') // -> 1
Number('zb') // -> NaN
(1).toString() // '1'

这部分是日常常用的内容,就不具体展开说了,主要记住强制转数字和布尔值的规则就行。

转布尔值规则:

  • undefined、null、false、NaN、''、0、-0 都转为 false
  • 其他所有值都转为 true,包括所有对象。

转数字规则:

  • true 为 1,false 为 0
  • null 为 0,undefinedNaNsymbol 报错
  • 字符串看内容,如果是数字或者进制值就正常转,否则就 NaN
  • 对象的规则隐式转换再讲

隐式转换

隐式转换规则是最烦的,其实笔者也记不住那么多内容。况且根据笔者目前收集到的最新面试题来说,这部分考题基本绝迹了,当然讲还是讲一下吧。

对象转基本类型:

  • 调用 Symbol.toPrimitive,转成功就结束
  • 调用 valueOf,转成功就结束
  • 调用 toString,转成功就结束
  • 报错

四则运算符:

  • 只有当加法运算时,其中一方是字符串类型,就会把另一个也转为字符串类型
  • 其他运算只要其中一方是数字,那么另一方就转为数字

== 操作符

常见考点

如果这部分规则记不住也不碍事,确实有点繁琐,而且考的也越来越少了,拿一道以前常考的题目看看吧:

[] == ![] // -> ?

this

this 是很多人会混淆的概念,但是其实他一点都不难,不要被那些长篇大论的文章吓住了(我其实也不知道为什么他们能写那么多字),你只需要记住几个规则就可以了。

普通函数

function foo() {
    console.log(this.a)
}
var a = 1
foo()

var obj = {
    a: 2,
    foo: foo
}
obj.foo()

// 以上情况就是看函数是被谁调用,那么 `this` 就是谁,没有被对象调用,`this` 就是 `window`

// 以下情况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)

// 还有种就是利用 call,apply,bind 改变 this,这个优先级仅次于 new

箭头函数

因为箭头函数没有 this,所以一切妄图改变箭头函数 this 指向都是无效的。

箭头函数的 this 只取决于定义时的环境。比如如下代码中的 fn 箭头函数是在 windows 环境下定义的,无论如何调用,this 都指向 window

var a = 1
const fn = () => {
  console.log(this.a)
}
const obj = {
  fn,
  a: 2
}
obj.fn()

常见考点

这里一般都是考 this 的指向问题,牢记上述的几个规则就够用了,比如下面这道题:

const a = {
  b: 2,
  foo: function () { console.log(this.b) }
}

function b(foo) {
  // 输出什么?
  foo()
}

b(a.foo)

闭包

首先闭包正确的定义是:假如一个函数能访问外部的变量,那么这个函数它就是一个闭包,而不是一定要返回一个函数。这个定义很重要,下面的内容需要用到。

let a = 1
// fn 是闭包
function fn() {
  console.log(a);
}

function fn1() {
  let a = 1
  // 这里也是闭包
  return () => {
    console.log(a);
  }
}
const fn2 = fn1()
fn2()

大家都知道闭包其中一个作用是访问私有变量,就比如上述代码中的 fn2 访问到了 fn1 函数中的变量 a。但是此时 fn1 早已销毁,我们是如何访问到变量 a 的呢?不是都说原始类型是存放在栈上的么,为什么此时却没有被销毁掉?

接下来笔者会根据浏览器的表现来重新理解关于原始类型存放位置的说法。

先来说下数据存放的正确规则是:局部、占用空间确定的数据,一般会存放在栈中,否则就在堆中(也有例外)。 那么接下来我们可以通过 Chrome 来帮助我们验证这个说法说法。

上图中画红框的位置我们能看到一个内部的对象 [[Scopes]],其中存放着变量 a,该对象是被存放在堆上的,其中包含了闭包、全局对象等等内容,因此我们能通过闭包访问到本该销毁的变量。

另外最开始我们对于闭包的定位是:假如一个函数能访问外部的变量,那么这个函数它就是一个闭包,因此接下来我们看看在全局下的表现是怎么样的。

let a = 1
var b = 2
// fn 是闭包
function fn() {
  console.log(a, b);
}

从上图我们能发现全局下声明的变量,如果是 var 的话就直接被挂到 globe 上,如果是其他关键字声明的话就被挂到 Script 上。虽然这些内容同样还是存在 [[Scopes]],但是全局变量应该是存放在静态区域的,因为全局变量无需进行垃圾回收,等需要回收的时候整个应用都没了。

只有在下图的场景中,原始类型才可能是被存储在栈上。

这里为什么要说可能,是因为 JS 是门动态类型语言,一个变量声明时可以是原始类型,马上又可以赋值为对象类型,然后又回到原始类型。这样频繁的在堆栈上切换存储位置,内部引擎是不是也会有什么优化手段,或者干脆全部都丢堆上?只有 const 声明的原始类型才一定存在栈上?当然这只是笔者的一个推测,暂时没有深究,读者可以忽略这段瞎想。

因此笔者对于原始类型存储位置的理解为:局部变量才是被存储在栈上,全局变量存在静态区域上,其它都存储在堆上。

当然这个理解是建立的 Chrome 的表现之上的,在不同的浏览器上因为引擎的不同,可能存储的方式还是有所变化的。

常见考点

闭包能考的很多,概念和笔试题都会考。

概念题就是考考闭包是什么了。

笔试题的话基本都会结合上异步,比如最常见的:

for (var i = 0; i < 6; i++) {
  setTimeout(() => {
    console.log(i)
  })
}

这道题会问输出什么,有哪几种方式可以得到想要的答案?

new

new 操作符可以帮助我们构建出一个实例,并且绑定上 this,内部执行步骤可大概分为以下几步:

  1. 新生成了一个对象
  2. 对象连接到构造函数原型上,并绑定 this
  3. 执行构造函数代码
  4. 返回新对象

在第四步返回新对象这边有一个情况会例外:

function Test(name) {
  this.name = name
  console.log(this) // Test { name: 'yck' }
  return { age: 26 }
}
const t = new Test('yck')
console.log(t) // { age: 26 }
console.log(t.name) // 'undefined'

当在构造函数中返回一个对象时,内部创建出来的新对象就被我们返回的对象所覆盖,所以一般来说构建函数就别返回对象了(返回原始类型不影响)。

常见考点

  • new 做了那些事?
  • new 返回不同的类型时会有什么表现?
  • 手写 new 的实现过程

作用域

作用域可以理解为变量的可访问性,总共分为三种类型,分别为:

  • 全局作用域
  • 函数作用域
  • 块级作用域,ES6 中的 letconst 就可以产生该作用域

其实看完前面的闭包、this 这部分内部的话,应该基本能了解作用域的一些应用。

一旦我们将这些作用域嵌套起来,就变成了另外一个重要的知识点「作用域链」,也就是 JS 到底是如何访问需要的变量或者函数的。

首先作用域链是在定义时就被确定下来的,和箭头函数里的 this 一样,后续不会改变,JS 会一层层往上寻找需要的内容。

其实作用域链这个东西我们在闭包小结中已经看到过它的实体了:[[Scopes]]

图中的 [[Scopes]] 是个数组,作用域的一层层往上寻找就等同于遍历 [[Scopes]]

常见考点

  • 什么是作用域
  • 什么是作用域链

原型

原型在面试里只需要几句话、一张图的概念就够用了,没人会让你长篇大论讲上一堆内容的,问原型更多的是为了引出继承这个话题。

根据上图,原型总结下来的概念为:

  • 所有对象都有一个属性 __proto__ 指向一个对象,也就是原型
  • 每个对象的原型都可以通过 constructor 找到构造函数,构造函数也可以通过 prototype 找到原型
  • 所有函数都可以通过 __proto__ 找到 Function 对象
  • 所有对象都可以通过 __proto__ 找到 Object 对象
  • 对象之间通过 __proto__ 连接起来,这样称之为原型链。当前对象上不存在的属性可以通过原型链一层层往上查找,直到顶层 Object 对象,再往上就是 null

常见考点

  • 聊聊你理解的原型是什么

继承

即使是 ES6 中的 class 也不是其他语言里的类,本质就是一个函数。

class Person {}
Person instanceof Function // true

其实在当下都用 ES6 的情况下,ES5 的继承写法已经没啥学习的必要了,但是因为面试还会被问到,所以复习一下还是需要的。

首先来说下 ES5 和 6 继承的区别:

  1. ES6 继承的子类需要调用 super() 才能拿到子类,ES5 的话是通过 apply 这种绑定的方式
  2. 类声明不会提升,和 let 这些一致

接下来就是回字的几种写法的名场面了,ES5 实现继承的方式有很多种,面试了解一种已经够用:

function Super() {}
Super.prototype.getNumber = function() {
  return 1
}

function Sub() {}
Sub.prototype = Object.create(Super.prototype, {
  constructor: {
    value: Sub,
    enumerable: false,
    writable: true,
    configurable: true
  }
})
let s = new Sub()
s.getNumber()

常见考点

  • JS 中如何实现继承
  • 通过原型实现的继承和 class 有何区别
  • 手写任意一种原型继承

深浅拷贝

浅拷贝

两个对象第一层的引用不相同就是浅拷贝的含义。

我们可以通过 assign 、扩展运算符等方式来实现浅拷贝:

let a = {
    age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1
b = {...a}
a.age = 3
console.log(b.age) // 2

深拷贝

两个对象内部所有的引用都不相同就是深拷贝的含义。

最简单的深拷贝方式就是使用 JSON.parse(JSON.stringify(object)),但是该方法存在不少缺陷。

比如说只支持 JSON 支持的类型,JSON 是门通用的语言,并不支持 JS 中的所有类型。

同时还存在不能处理循环引用的问题:

如果想解决以上问题,我们可以通过递归的方式来实现代码:

// 利用 WeakMap 解决循环引用
let map = new WeakMap()
function deepClone(obj) {
  if (obj instanceof Object) {
    if (map.has(obj)) {
      return map.get(obj)
    }
    let newObj
    if (obj instanceof Array) {
      newObj = []     
    } else if (obj instanceof Function) {
      newObj = function() {
        return obj.apply(this, arguments)
      }
    } else if (obj instanceof RegExp) {
      // 拼接正则
      newobj = new RegExp(obj.source, obj.flags)
    } else if (obj instanceof Date) {
      newobj = new Date(obj)
    } else {
      newObj = {}
    }
    // 克隆一份对象出来
    let desc = Object.getOwnPropertyDescriptors(obj)
    let clone = Object.create(Object.getPrototypeOf(obj), desc)
    map.set(obj, clone)
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        newObj[key] = deepClone(obj[key])
      }
    }
    return newObj
  }
  return obj
}

上述代码解决了常见的类型以及循环引用的问题,当然还是一部分缺陷的,但是面试时候能写出上面的代码已经足够了,剩下的能口述思路基本这道题就能拿到高分了。

比如说递归肯定会存在爆栈的问题,因为执行栈的大小是有限制的,到一定数量栈就会爆掉。

因此遇到这种问题,我们可以通过遍历的方式来改写递归。这个就是如何写层序遍历(BFS)的问题了,通过数组来模拟执行栈就能解决爆栈问题,有兴趣的读者可以咨询查阅。

Promise

Promise 是一个高频考点了,但是更多的是在笔试题中出现,概念题反倒基本没有,多是来问 Event loop 的。

对于这块内容的复习我们需要熟悉涉及到的所有 API,因为考题里可能会问到 allrace 等等用法或者需要你用这些 API 实现一些功能。

对于 Promise 进阶点的知识可以具体阅读笔者的这篇文章,这里就不复制过来占用篇幅了:Promise 你真的用明白了么?

常见考点

另外还有一道很常见的串行题目:

页面上有三个按钮,分别为 A、B、C,点击各个按钮都会发送异步请求且互不影响,每次请求回来的数据都为按钮的名字。 请实现当用户依次点击 A、B、C、A、C、B 的时候,最终获取的数据为 ABCACB。

这道题目主要两个考点:

  1. 请求不能阻塞,但是输出可以阻塞。比如说 B 请求需要耗时 3 秒,其他请求耗时 1 秒,那么当用户点击 BAC 时,三个请求都应该发起,但是因为 B 请求回来的慢,所以得等着输出结果。
  2. 如何实现一个队列?

其实我们无需自己去构建一个队列,直接利用 promise.then 方法就能实现队列的效果了。

class Queue {
  promise = Promise.resolve();

  excute(promise) {
    this.promise = this.promise.then(() => promise);
    return this.promise;
  }
}

const queue = new Queue();

const delay = (params) => {
  const time = Math.floor(Math.random() * 5);
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(params);
    }, time * 500);
  });
};

const handleClick = async (name) => {
  const res = await queue.excute(delay(name));
  console.log(res);
};

handleClick('A');
handleClick('B');
handleClick('C');
handleClick('A');
handleClick('C');
handleClick('B');

async、await

awaitpromise 一样,更多的是考笔试题,当然偶尔也会问到和 promise 的一些区别。

await 相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码。缺点在于滥用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性,此时更应该使用 Promise.all

下面来看一道很容易做错的笔试题。

var a = 0
var b = async () => {
  a = a + await 10
  console.log('2', a) // -> ?
}
b()
a++
console.log('1', a) // -> ?

这道题目大部分读者肯定会想到 await 左边是异步代码,因此会先把同步代码执行完,此时 a 已经变成 1,所以答案应该是 11。

其实 a 为 0 是因为加法运算法,先算左边再算右边,所以会把 0 固定下来。如果我们把题目改成 await 10 + a 的话,答案就是 11 了。

事件循环

在开始讲事件循环之前,我们一定要牢记一点:JS 是一门单线程语言,在执行过程中永远只能同时执行一个任务,任何异步的调用都只是在模拟这个过程,或者说可以直接认为在 JS 中的异步就是延迟执行的同步代码。另外别的什么 Web worker、浏览器提供的各种线程都不会影响这个点。

大家应该都知道执行 JS 代码就是往执行栈里 push 函数(不知道的自己搜索吧),那么当遇到异步代码的时候会发生什么情况?

其实当遇到异步的代码时,只有当遇到 Task、Microtask 的时候才会被挂起并在需要执行的时候加入到 Task(有多种 Task) 队列中。

从图上我们得出两个疑问:

  1. 什么任务会被丢到 Microtask Queue 和 Task Queue 中?它们分别代表了什么?
  2. Event loop 是如何处理这些 task 的?

首先我们来解决问题一。

Task(宏任务):同步代码、setTimeout 回调、setInteval 回调、IO、UI 交互事件、postMessageMessageChannel

MicroTask(微任务):Promise 状态改变以后的回调函数(then 函数执行,如果此时状态没变,回调只会被缓存,只有当状态改变,缓存的回调函数才会被丢到任务队列)、Mutation observer 回调函数、queueMicrotask 回调函数(新增的 API)。

宏任务会被丢到下一次事件循环,并且宏任务队列每次只会执行一个任务。

微任务会被丢到本次事件循环,并且微任务队列每次都会执行任务直到队列为空。

假如每个微任务都会产生一个微任务,那么宏任务永远都不会被执行了。

接下来我们来解决问题二。

Event Loop 执行顺序如下所示:

  1. 执行同步代码
  2. 执行完所有同步代码后且执行栈为空,判断是否有微任务需要执行
  3. 执行所有微任务且微任务队列为空
  4. 是否有必要渲染页面
  5. 执行一个宏任务

如果你觉得上面的表述不大理解的话,接下来我们通过代码示例来巩固理解上面的知识:

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
    queueMicrotask(() => console.log('queueMicrotask'))
    console.log('promise');
});

console.log('script end');
  1. 遇到 console.log 执行并打印
  2. 遇到 setTimeout,将回调加入宏任务队列
  3. 遇到 Promise.resolve(),此时状态已经改变,因此将 then 回调加入微任务队列
  4. 遇到 console.log 执行并打印

此时同步任务全部执行完毕,分别打印了 'script start' 以及 'script end',开始判断是否有微任务需要执行。

  1. 微任务队列存在任务,开始执行 then 回调函数
  2. 遇到 queueMicrotask,将回到加入微任务队列
  3. 遇到 console.log 执行并打印
  4. 检查发现微任务队列存在任务,执行 queueMicrotask 回调
  5. 遇到 console.log 执行并打印

此时发现微任务队列已经清空,判断是否需要进行 UI 渲染。

  1. 执行宏任务,开始执行 setTimeout 回调
  2. 遇到 console.log 执行并打印

执行一个宏任务即结束,寻找是否存在微任务,开始循环判断...

其实事件循环没啥难懂的,理解 JS 是个单线程语言,明白哪些是微宏任务、循环的顺序就好了。

最后需要注意的一点:正是因为 JS 是门单线程语言,只能同时执行一个任务。因此所有的任务都可能因为之前任务的执行时间过长而被延迟执行,尤其对于一些定时器而言。

常见考点

  • 什么是事件循环?
  • JS 的执行原理?
  • 哪些是微宏任务?
  • 定时器是准时的嘛?

模块化

当下模块化主要就是 CommonJS 和 ES6 的 ESM 了,其它什么的 AMD、UMD 了解下就行了。

ESM 我想应该没啥好说的了,主要我们来聊聊 CommonJS 以及 ESM 和 CommonJS 的区别。

CommonJS

CommonJs 是 Node 独有的规范,当然 Webpack 也自己实现了这套东西,让我们能在浏览器里跑起来这个规范。

// a.js
module.exports = {
    a: 1
}
// or
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1

在上述代码中,module.exportsexports 很容易混淆,让我们来看看大致内部实现

// 基本实现
var module = {
  exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
    // 导出的东西
    var a = 1
    module.exports = a
    return module.exports
};

根据上面的大致实现,我们也能看出为什么对 exports 直接赋值不会有任何效果。

对于 CommonJS 和 ESM 的两者区别是:

  • 前者支持动态导入,也就是 require(${path}/xx.js),后者使用 import()
  • 前者是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  • 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化

垃圾回收

本小结内容建立在 V8 引擎之上。

首先聊垃圾回收之前我们需要知道堆栈到底是存储什么数据的,当然这块内容上文已经讲过,这里就不再赘述了。

接下来我们先来聊聊栈是如何垃圾回收的。其实栈的回收很简单,简单来说就是一个函数 push 进栈,执行完毕以后 pop 出来就当可以回收了。当然我们往深层了讲深层了讲就是汇编里的东西了,操作 esp 和 ebp 指针,了解下即可。

然后就是堆如何回收垃圾了,这部分的话会分为两个空间及多个算法。

两个空间分别为新生代和老生代,我们分开来讲每个空间中涉及到的算法。

新生代

新生代中的对象一般存活时间较短,空间也较小,使用 Scavenge GC 算法。

在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。

老生代

老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除和标记压缩算法。

在讲算法前,先来说下什么情况下对象会出现在老生代空间中:

  • 新生代中的对象是否已经经历过一次以上 Scavenge 算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。
  • To 空间的对象占比大小超过 25 %。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中。

老生代中的空间很复杂,有如下几个空间

enum AllocationSpace {
  // TODO(v8:7464): Actually map this space's memory as read-only.
  RO_SPACE,    // 不变的对象空间
  NEW_SPACE,   // 新生代用于 GC 复制算法的空间
  OLD_SPACE,   // 老生代常驻对象空间
  CODE_SPACE,  // 老生代代码对象空间
  MAP_SPACE,   // 老生代 map 对象
  LO_SPACE,    // 老生代大空间对象
  NEW_LO_SPACE,  // 新生代大空间对象

  FIRST_SPACE = RO_SPACE,
  LAST_SPACE = NEW_LO_SPACE,
  FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
  LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};

在老生代中,以下情况会先启动标记清除算法:

  • 某一个空间没有分块的时候
  • 空间中被对象超过一定限制
  • 空间不能保证新生代中的对象移动到老生代中

在这个阶段中,会遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。在标记大型对内存时,可能需要几百毫秒才能完成一次标记。这就会导致一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工作分解为更小的模块,可以让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿情况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可以让 GC 扫描和标记对象时,同时允许 JS 运行,你可以点击 该博客 详细阅读。

清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象像一端移动,直到所有对象都移动完成然后清理掉不需要的内存。

其它考点

0.1 + 0.2 !== 0.3

因为 JS 采用 IEEE 754 双精度版本(64位),并且只要采用 IEEE 754 的语言都有该问题。

不止 0.1 + 0.2 存在问题,0.7 + 0.1、0.2 + 0.4 同样也存在问题。

存在问题的原因是浮点数用二进制表示的时候是无穷的,因为精度的问题,两个浮点数相加会造成截断丢失精度,因此再转换为十进制就出了问题。

解决的办法可以通过以下代码:

export const addNum = (num1: number, num2: number) => {
  let sq1;
  let sq2;
  let m;
  try {
    sq1 = num1.toString().split('.')[1].length;
  } catch (e) {
    sq1 = 0;
  }
  try {
    sq2 = num2.toString().split('.')[1].length;
  } catch (e) {
    sq2 = 0;
  }
  m = Math.pow(10, Math.max(sq1, sq2));
  return (Math.round(num1 * m) + Math.round(num2 * m)) / m;
};

核心就是计算出两个浮点数最大的小数长度,比如说 0.1 + 0.22 的小数最大长度为 2,然后两数乘上 10 的 2次幂再相加得出数字 32,然后除以 10 的 2次幂即可得出正确答案 0.32。

手写题

防抖

你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。

这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。

PS:防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于阈值,防抖的情况下只会调用一次,而节流会每隔一定时间调用函数。

我们先来看一个袖珍版的防抖理解一下防抖的实现:

// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
  // 缓存一个定时器id
  let timer = 0
  // 这里返回的函数是每次用户实际调用的防抖函数
  // 如果已经设定过定时器了就清空上一次的定时器
  // 开始一个新的定时器,延迟执行用户传入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}
// 不难看出如果用户调用该函数的间隔小于 wait 的情况下,上一次的时间还未到就被清除了,并不会执行函数

这是一个简单版的防抖,但是有缺陷,这个防抖只能在最后调用。一般的防抖会有immediate选项,表示是否立即调用。这两者的区别,举个栗子来说:

  • 例如在搜索引擎搜索问题的时候,我们当然是希望用户输入完最后一个字才调用查询接口,这个时候适用延迟执行的防抖函数,它总是在一连串(间隔小于wait的)函数触发之后调用。
  • 例如用户给interviewMap点star的时候,我们希望用户点第一下的时候就去调用接口,并且成功之后改变star按钮的样子,用户就可以立马得到反馈是否star成功了,这个情况适用立即执行的防抖函数,它总是在第一次调用,并且下一次调用必须与前一次调用的时间间隔大于wait才会触发。

下面我们来实现一个带有立即执行选项的防抖函数

// 这个是用来获取当前时间戳的
function now() {
  return +new Date()
}
/**
 * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
 *
 * @param  {function} func        回调函数
 * @param  {number}   wait        表示时间窗口的间隔
 * @param  {boolean}  immediate   设置为ture时,是否立即调用函数
 * @return {function}             返回客户调用函数
 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args
  
  // 延迟执行函数
  const later = () => setTimeout(() => {
    // 延迟函数执行完毕,清空缓存的定时器序号
    timer = null
    // 延迟执行的情况下,函数会在延迟函数中执行
    // 使用到之前缓存的参数和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)

  // 这里返回的函数是每次实际调用的函数
  return function(...params) {
    // 如果没有创建延迟执行函数(later),就创建一个
    if (!timer) {
      timer = later()
      // 如果是立即执行,调用函数
      // 否则缓存参数和调用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
    // 这样做延迟函数会重新计时
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}

整体函数实现的不难,总结一下。

  • 对于按钮防点击来说的实现:如果函数是立即执行的,就立即调用,如果函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都重新计时。一旦你点累了,定时器时间到,定时器重置为 null,就可以再次点击了。
  • 对于延时执行函数来说的实现:清除定时器ID,如果是延迟调用就调用函数

节流

防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。

/**
 * underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
 *
 * @param  {function}   func      回调函数
 * @param  {number}     wait      表示时间窗口的间隔
 * @param  {object}     options   如果想忽略开始函数的的调用,传入{leading: false}。
 *                                如果想忽略结尾函数的调用,传入{trailing: false}
 *                                两者不能共存,否则函数不能执行
 * @return {function}             返回客户调用函数   
 */
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 之前的时间戳
    var previous = 0;
    // 如果 options 没传则设为空对象
    if (!options) options = {};
    // 定时器回调函数
    var later = function() {
      // 如果设置了 leading,就将 previous 设为 0
      // 用于下面函数的第一个 if 判断
      previous = options.leading === false ? 0 : _.now();
      // 置空一是为了防止内存泄漏,二是为了下面的定时器判断
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    return function() {
      // 获得当前时间戳
      var now = _.now();
      // 首次进入前者肯定为 true
      // 如果需要第一次不执行函数
      // 就将上次时间戳设为当前的
      // 这样在接下来计算 remaining 的值时会大于0
      if (!previous && options.leading === false) previous = now;
      // 计算剩余时间
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 如果当前调用已经大于上次调用时间 + wait
      // 或者用户手动调了时间
       // 如果设置了 trailing,只会进入这个条件
      // 如果没有设置 leading,那么第一次会进入这个条件
      // 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
      // 其实还是会进入的,因为定时器的延时
      // 并不是准确的时间,很可能你设置了2秒
      // 但是他需要2.2秒才触发,这时候就会进入这个条件
      if (remaining <= 0 || remaining > wait) {
        // 如果存在定时器就清理掉否则会调用二次回调
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        // 判断是否设置了定时器和 trailing
        // 没有的话就开启一个定时器
        // 并且不能不能同时设置 leading 和 trailing
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };

Event Bus

class Events {
  constructor() {
    this.events = new Map();
  }

  addEvent(key, fn, isOnce, ...args) {
    const value = this.events.get(key) ? this.events.get(key) : this.events.set(key, new Map()).get(key)
    value.set(fn, (...args1) => {
        fn(...args, ...args1)
        isOnce && this.off(key, fn)
    })
  }

  on(key, fn, ...args) {
    if (!fn) {
      console.error(`没有传入回调函数`);
      return
    }
    this.addEvent(key, fn, false, ...args)
  }

  fire(key, ...args) {
    if (!this.events.get(key)) {
      console.warn(`没有 ${key} 事件`);
      return;
    }
    for (let [, cb] of this.events.get(key).entries()) {
      cb(...args);
    }
  }

  off(key, fn) {
    if (this.events.get(key)) {
      this.events.get(key).delete(fn);
    }
  }

  once(key, fn, ...args) {
    this.addEvent(key, fn, true, ...args)
  }
}

instanceof

instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype

function instanceof(left, right) {
    // 获得类型的原型
    let prototype = right.prototype
    // 获得对象的原型
    left = left.__proto__
    // 判断对象的类型是否等于类型的原型
    while (true) {
        if (left === null)
            return false
        if (prototype === left)
            return true
        left = left.__proto__
    }
}

call

Function.prototype.myCall = function(context, ...args) {
  context = context || window
  let fn = Symbol()
  context[fn] = this
  let result = context[fn](...args)
  delete context[fn]
  return result
}

apply

Function.prototype.myApply = function(context) {
  context = context || window
  let fn = Symbol()
  context[fn] = this
  let result
  if (arguments[1]) {
    result = context[fn](...arguments[1])
  } else {
    result = context[fn]()
  }
  delete context[fn]
  return result
}

bind

Function.prototype.myBind = function (context) {
  var _this = this
  var args = [...arguments].slice(1)
  // 返回一个函数
  return function F() {
    // 因为返回了一个函数,我们可以 new F(),所以需要判断
    if (this instanceof F) {
      return new _this(...args, ...arguments)
    }
    return _this.apply(context, args.concat(...arguments))
  }
}

其他

其他手写题上文已经有提及,比如模拟 new、ES5 实现继承、深拷贝。

另外大家可能经常能看到手写 Promise 的文章,其实根据笔者目前收集到的数百道面试题以及读者的反馈来看,压根就没人遇到这个考点,所以我们大可不必在这上面花时间。

最后

以上就是本篇基础的全部内容了,如果有各位读者认为重要的知识点笔者却遗漏的话,欢迎大家指出。

大家也可以在笔者的网站上阅读,体验更佳!
查看原文

赞 47 收藏 34 评论 1

linong 回答了问题 · 4月13日

解决为什么本地正常部署到服务器就一大堆问题呢?

编译的问题

比如说有的资源你引用的不对,然后部署之后就找不到资源了。

还有js压缩的问题,有的地方用了骚操作,解析不对了。

css压缩问题,有的会合并之类的。然后合并的时候如果有一些不生效的,会造成一些意外情况。

关注 2 回答 1

linong 回答了问题 · 4月13日

解决请问不用jq能不能做到局部刷新

加一个随机请求参数,资源是会缓存的。

比如说https://segmentfault.com/q/1010000039811463改成https://segmentfault.com/q/1010000039811463?1

难道你用 jquery就可以刷新了嘛?如果可以的话,还值得研究一下 .html 的源码。

关注 5 回答 5