TerryZeng

TerryZeng 查看完整档案

福州编辑  |  填写毕业院校  |  填写所在公司/组织 terryz.github.io 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

TerryZeng 收藏了文章 · 2019-12-31

精读《Serverless 给前端带来了什么》

1. 引言

Serverless 是一种 “无服务器架构”,让用户无需关心程序运行环境、资源及数量,只要将精力 Focus 到业务逻辑上的技术。

现在公司已经实现 DevOps 化,正在向 Serverless 迈进,而为什么前端要关注 Serverless?

对业务前端同学:

  1. 会改变前后端接口定义规范。
  2. 一定会改变前后端联调方式,让前端参与服务器逻辑开发,甚至 Node Java 混部。
  3. 大大降低 Nodejs 服务器维护门槛,只要会写 JS 代码就可以维护 Node 服务,而无需学习 DevOps 相关知识。

对一个自由开发者:

  1. 未来服务器部署更弹性,更省钱。
  2. 部署速度更快,更不易出错。

前端框架总是带入后端思维,而 Serverless 则是把前端思维带入了后端运维。

前端开发者其实是最早享受到 “Serverless” 好处的群体。他们不需要拥有自己的服务,甚至不需要自己的浏览器,就可以让自己的 JS 代码均匀、负载均衡的运行在每一个用户的电脑中。

而每个用户的浏览器,就像现在最时髦,最成熟的 Serverless 集群,从远程加载 JS 代码开始冷启动,甚至在冷启动上也是卓越领先的:利用 JIT 加速让代码实现毫秒级别的冷启动。不仅如此,浏览器还是实现了 BAAS 服务的完美环境,我们可以调用任何函数获取用户的 Cookie、环境信息、本地数据库服务,而无需关心用户用的是什么电脑,连接了怎样的网络,甚至硬盘的大小。

这就是 Serverless 理念。通过 FAAS(函数及服务)与 BAAS(后台及服务)企图在服务端制造前端开发者习以为常的开发环境,所以前端开发者应该更能理解 Serverless 带来的好处。

2. 精读

FAAS(函数即服务) + BAAS(后台即服务) 可以称为一个完整的 Serverless 的实现,除此之外,还有 PASS(平台即服务)的概念。而通常平台环境都通过容器技术实现,最终都为了达到 NoOps(无人运维),或者至少 DevOps(开发&运维)。

简单介绍一下这几个名词,防止大家被绕晕:

FAAS - Function as a service

函数即服务,每一个函数都是一个服务,函数可以由任何语言编写,除此之外不需要关心任何运维细节,比如:计算资源、弹性扩容,而且可以按量计费,且支持事件驱动。业界大云厂商都支持 FAAS,各自都有一套工作台、或者可视化工作流来管理这些函数。

BAAS - Backend as a service

后端及服务,就是集成了许多中间件技术,可以无视环境调用服务,比如数据即服务(数据库服务),缓存服务等。虽然下面还有很多 XASS,但组成 Serverless 概念的只有 FAAS + BAAS。

PAAS - Platform as a service

平台即服务,用户只要上传源代码就可以自动持续集成并享受高可用服务,如果速度足够快,可以认为是类似 Serverless。但随着以 Docker 为代表的容器技术兴起,以容器为粒度的 PASS 部署逐渐成为主流,是最常用的应用部署方式。比如中间件、数据库、操作系统等。

DAAS - Data as a service

数据即服务,将数据采集、治理、聚合、服务打包起来提供出去。DASS 服务可以应用 Serverless 的架构。

IAAS - Infrastructure as a Service

基础设施即服务,比如计算机存储、网络、服务器等基建设施以服务的方式提供。

SAAS - Software as a Service

软件即服务,比如 ERP、CRM、邮箱服务等,以软件为粒度提供服务。

容器

容器就是隔离了物理环境的虚拟程序执行环境,而且环境可被描述、迁移。比较热门的容器技术是 Docker。

随着容器数量增多,就出现了管理容器集群的技术,比较有名的容器编排平台是 Kubernetes。容器技术是 Serverless 架构实现的一种选择,也是实现的基础。

NoOps

就是无人运维,比较理想主义,也许要借助 AI 的能力才能实现完全无人运维。

无人运维不代表 Serverless,Serverless 可能也需要人运维(至少现在),只是开发者不再需要关心环境。

DevOps

笔者觉得可以理解为 “开发即运维”,毕竟出了事情,开发要被问责,而一个成熟的 DevOps 体系可以让更多的开发者承担 OP 的职责,或者与 OP 更密切的合作。


回到 Serverless,未来后端开发的体验可能与前端相似:不需要关心代码运行在哪台服务器(浏览器),无需关心服务器环境(浏览器版本)、不用担心负载均衡(前端从未担心过)、中间件服务随时调用(LocalStorage、Service Worker)

前端同学对 Serverless 应该尤为激动。就拿笔者亲身经历举例吧。

从做一款游戏说起

笔者非常迷恋养成类游戏,养成游戏最常见的就是资源建造、收集,或者挂机时计算资源的 读秒规则。笔者在开发游戏的时候,最初是将客户端代码与服务端代码完全分成两套实现的:

// ... UI 部分,画出一个倒计时伐木场建造进度条
const currentTime = await requestBuildingProcess();
const leftTime = new Date().getTime() - currentTime;
// ... 继续倒计时读条
// 读条完毕后,每小时木头产量 + 100,更新到客户端计时器
store.woodIncrement += 100;

为了游戏体验,用户可以在不刷新浏览器的情况下,看到伐木场建造进度的读条,以及 嘭 一下建造完毕,并且发现每秒钟多获得了 100 点木材!但是当伐木场 建造完成前、完成时、完成后的任意时间点刷新浏览器,都要保持逻辑的统一,而且数据需要在后端离线计算。 此时就要写后端代码了:

// 每次登陆时,校验当前登陆
const currentTime = new Date().getTime()
// 获取伐木场当前状态
if ( /* 建造中 */) {
  // 返回给客户端当前时间
  const leftTime = building.startTime - currentTime
  res.body = leftTime
} else {
  // 建造完毕
  store.woodIncrement += 100
}

很快,建筑的种类多了起来,不同的状态、等级产量都不同,前后端分开维护成本会越来越大,我们需要做配置同步。

配置同步

为了做前后端配置同步,可以将配置单独托管起来前后端共用,比如新建一个配置文件,专门存储游戏信息:

export const buildings = {
  wood: {
    name: "..",
    maxLevel: 100,
    increamentPerLevel: 50,
    initIncreament: 100
  }
  /* .. and so on .. */
};

这虽然复用了配置,但前后端都有一些共同的逻辑可以复用,比如 根据建筑建造时间判断建筑状态,判断 N 秒后建筑的产量等等。 而 Serverless 带来了进一步优化的空间。

在 Serverless 环境下做游戏

试想一下,可以在服务器以函数粒度执行代码,我们可以这样抽象游戏逻辑:

// 根据建筑建造时间判断建筑状态
export const getBuildingStatusByTime = (instanceId: number, time: number) => {
  /**/
};

// 判断建筑生产量
export const getBuildingProduction = (instanceId: number, lastTime: number) => {
  const status = getBuildingStatusByTime(instanceId, new Date().getTime());
  switch (status) {
    case "building":
      return 0;
    case "finished":
      // 根据 (当前时间 - 上次打开时间)* 每秒产量得到总产量
      return; /**/
  }
};

// 前端 UI 层,每隔一秒调用一次 getBuildingProduction 函数,及时更新生产数据
// 前端入口函数
export const frontendMain = () => {
  /**/
};

// 后端 根据每次打开时间,调用一次 getBuildingProduction 函数并入库
// 后端入口函数
export const backendMain = () => {
  /**/
};

利用 PASS 服务,将前后端逻辑写在一起,将 getBuildingProduction 函数片段上传至 FAAS 服务,这样就可以同时共享前后端逻辑了!

在文件夹视图下,可以做如下结构规划:

.
├── client    # 前端入口
├── server    # 后端入口
├── common    # 共享工具函数,可以包含 80% 的通用游戏逻辑

也许有人会问:前后端共享代码不止有 Serverless 才能做到。

的确如此,如果代码抽象足够好,有成熟的工程方案支持,是可以将一份代码分别导出到浏览器与服务器的。但 Serverless 基于函数粒度功能更契合前后端复用代码的理念,它的出现可能会推动更广泛的前后端代码复用,这虽然不是新发明,但足够称为一个伟大的改变。

前后端的视角

对于前端开发者,会发现后台服务变简单了。对于后端开发者,发现服务做厚了,面临的挑战更多了。

更简单的后台服务

传统 ECS 服务器在租赁时,CentOS 与 AliyunOS 的环境选择就足够让人烦恼。对个人开发者而言,我们要搭建一个完整的持续集成服务是很困难的,而且面临的选择很多,让人眼花缭乱:

  • 可以在服务器安装数据库等服务,本地直联服务器的数据库进行开发。
  • 可以本地安装 Docker 连接本地数据库服务,将环境打包成镜像整体部署到服务器。
  • 将前后端代码分离,前端代码在本地开发,服务端代码在服务器开发。

甚至服务器的稳定性,需要 PM2 等工具进行管理。当服务器面临攻击、重启、磁盘故障时,打开复杂的工作台或登陆 Shell 后一通操作才能恢复。这怎么让人专心把精力放在要做的事情上呢?

Serverless 解决了这个问题,因为我们要上传的只是一个代码片段,不再需要面对服务器、系统环境、资源等环境问题,外部服务也有封装好的 BAAS 体系支持。

实际上在 Serverless 出来之前,就有许多后端团队利用 FAAS 理念简化开发流程。

为了减少写后端业务逻辑时,环境、部署问题的干扰,许多团队会将业务逻辑抽象成一个个区块(Block),对应到代码片段或 Blockly,这些区块可以独立维护、发布,最后将这些代码片段注入到主程序中,或动态加载。如果习惯了这种开发方式,那也更容易接受 Serverless。

更厚的后台服务

站在后台角度,事情就变得比较复杂了。相对于提供简单的服务器和容器,现在要对用户屏蔽执行环境,将服务做得更厚。

笔者通过一些文章了解到,Serverless 的推行还面临着如下一些挑战:

  • Serverless 各厂商实现种类很多,想让业务做到多云部署,需要抹平差异。
  • 成熟的 PASS 服务其实是伪 Serverless,后续该如何标准化。
  • FAAS 冷启动需要重新加载代码、动态分配资源,导致冷启动速度很慢,除了预热,还需要经济的优化方式。
  • 对于高并发(比如双 11 秒杀)场景的应用,无需容量评估是很危险的事情,但如果真能做到完全弹性化,就省去了烦恼的容量评估。
  • 存量应用如何迁移。业界的 Serverless 服务厂商大部分都没有解决存量应用迁移的问题。
  • Serverless 的特性导致了无状态,而复杂的互联网应用都是有状态的,因此挑战在不改变开发习惯的情况下支持状态。

所幸的是,这些问题都已经在积极处理中,而且不少有了已经落地的解决方案。

Serverless 给后台带来的好处远比面临的挑战多:

  • 推进前后端一体化。进一步降低 Node 写服务端代码的门槛,免除应用运营的学习成本。笔者曾经遇到过自己申请的数据库服务被迁移到其他机房而导致的应用服务中断,以后再也不需要担心了,因为数据库作为 BAAS 服务,是不需要关心在哪部署,是否跨机房,以及如何做迁移的。
  • 提高资源利用效率。杜绝应用独占资源,换成按需加载必然能减少不必要的资源消耗,而且将服务均摊到集群的每一台机器,拉平集群的 CPU 水位。
  • 降低云平台使用门槛。无需运维,灵活拓展,按价值服务,高可用,这些能力在吸引更多客户的同时,完全按需计费的特性也减少了用户开销,达到双赢。

利用 Serverless 尝试服务开放

笔者在公司负责一个大型 BI 分析平台建设,BI 分析平台的底层能力之一就是可视化搭建。

那么可视化搭建能力该如何开放呢?现在比较容易做到的是组件开放,毕竟前端可以与后端设计相对解耦,利用 AMD 加载体系也比较成熟。

现在遇到的一个挑战就是后端能力开放,因为当对取数能力有定制要求时,可能需要定制后端数据处理的逻辑。目前能做到的是利用 maven3、jdk7 搭建本地开发环境测试,如果想上线,还需要后端同学的协助。

如果后端搭建一个特有的 Serverless BAAS 服务,那么就可以像前端组件一样进行线上 Coding,调试,甚至灰度发布进行预发测试。现在前端云端开发已经有了不少成熟的探索,Serverless 可以统一前后端代码在云端开发的体验,而不需要关心环境。

Serverless 应用架构设计

看了一些 Serverless 应用架构图,发现大部分业务都可以套用这样一张架构图:

将业务函数抽象成一个个 FAAS 函数,将数据库、缓存、加速等服务抽象成 BAAS 服务。

上层提供 Restful 或事件触发机制调用,对应到不同的端(PC、移动端)。

想要拓展平台能力,只要在端上做开放(组件接入)与 FAAS 服务做开放(后端接入)即可。

收益与挑战

Serverless 带来了的收益与挑战并存,本文站在前端角度聊一聊。

收益一:前端更 Focus 在前端体验技术,而不需要具备太多应用管理知识。

最近看了很多前端前辈写的总结文,最大的体会就是回忆 “前端在这几年到底起到了什么作用”。我们往往会夸大自己的存在感,其实前端存在的意义就是解决人机交互问题,大部分场景下,都是一种景上添花的作用,而不是必须。

回忆你最自豪的工作经历,可能是掌握了 Node 应用的运维知识、前端工程体系建设、研发效能优化、标准规范制定等,但真正对业务起效的部分,恰恰是你觉得写得最不值得一提的业务代码。前端花了太多的时间在周边技术上,而减少了很多对业务、交互的思考。

即便是大公司,也难以招到既熟练使用 Nodejs,又具备丰富运维知识的人,同时还要求他前端技术精湛,对业务理解深刻,鱼和熊掌几乎不可兼得。

Serverless 可以有效解决这个问题,前端同学只需要会写 JS 代码而无需掌握任何运维知识,就可以快速实现自己的整套想法。

诚然,了解服务端知识是有必要的,但站在合理分工的角度,前端就应该 focus 在前端技术上。前端的核心竞争力或者带来的业务价值,并不会随着了解多一些运维知识而得到补充,相反,这会吞噬掉我们本可以带来更多业务价值的时间。

语言的进化、浏览器的进化、服务器的进化,都是从复杂到简单,底层到封装的过程,而 serverless 是后端 + 运维作为一个整体的进一步封装的过程。

收益二:逻辑编排带来的代码高度复用、可维护,拓展 云+端 的能力。

云+端 是前端开发的下个形态,提供强大的云编码能力,或者通过插件将端打造为类似云的开发环境。其最大好处就是屏蔽前端开发环境细节,理念与 Serverless 类似。

之前有不少团队尝试过利用 Graphsql 让接口 “更有弹性”,而 Serverless 则是更彻底的方案。

我自己的团队就尝试过 Graphsql 方案,但由于业务非常复杂,难以用标准的模型描述所有场景的需求,因此不适合使用 Graphsql。恰恰是一套基于 Blockly 的可视化后端开发平台坚持了下来,而且取得了惊人的开发提效。这套 Blockly 通用化抽象后几乎可以由 Serverless 来代替。所以 Serverless 可以解决复杂场景下后端研发提效的问题。

Serverless 在融合了云端开发后,就可以通过逻辑编排进一步可视化调整函数执行顺序、依赖关系。

笔者之前在百度广告数据处理团队使用过这种平台计算离线日志,每个 MapReduce 计算节点经过可视化后,就可以轻松看出故障时哪个节点在阻塞,还可以看到最长执行链路,并为每个节点重新分配执行权重。即便逻辑编排不能解决开发的所有痛点,但在某个具体业务场景下一定可以大有作为。

挑战一:Serverless 可以完全取消前端转后端的门槛?

前端同学写 Node 代码最容易犯的毛病就是内存溢出。

浏览器 + Tab 天然是一个用完即关的场景,UI 组件与逻辑创建与销毁也非常频繁,因此前端同学很少,也不太需要关心 GC 问题。而 GC 在后端开发场景中是一个早已养成的习惯,因此 Nodejs 程序缓存溢出是大家最关注的问题。

Serverless 应用是动态加载,长时间不用就会释放的,因此一般来说不需要太担心 GC 的问题,就算内存溢出,在内存被占满前可能已经进程被释放,或者被监测到异常强制 Kill 掉。

但毕竟 FAAS 函数的加载与释放完全是由云端控制的,一个常用的函数长时间不卸载也是有可能的,因此 FAAS 函数还是要注意控制副作用。

所以 Serverless 虽然抹平了运维环境,但服务端基本知识还需要了解,必须意识到代码跑在前端还是后端。

挑战二:性能问题

Serverless 的冷启动会导致性能问题,而让业务方主动关心程序的执行频率或者性能要求,再开启预热服务又重新将研发拖入了运维的深渊中。

即便是业界最成熟的亚马逊 Serverless 云服务,也无法做到业务完全不关心调用频率,就可以轻松应付秒杀场景。

因此目前 Serverless 可能更适合结合合适的场景使用,而不是任何应用都强行套用 Serverless。

虽然可以通过定期运行 FAAS 服务来保证程序一直 Online,但笔者认为这还是违背了 Serverless 的理念。

挑战三:如何保证代码可迁移性

有一张很经典的 Serverless 定位描述图:

TB1i7PULkzoK1RjSZFlXXai4VXa-804-1184.png =00x300

网络、存储、服务、虚拟家、操作系统、中间件、运行时、数据都不需要关心了,甚至连应用层都只需要关心其中函数部分,而不需要关心其他比如启动、销毁部分。

前面总拿这点当优势,但也可以反过来认为是个劣势。 当你的代码完全依赖某个公有云环境后,你就失去了整体环境的掌控力,甚至代码都只能在特定的云平台才能运行。

不同云平台提供的 BAAS 服务规范可能不同,FAAS 的入口、执行方式也可能不同,想要采用多云部署就必须克服这个问题。

现在许多 Serverless 平台都在考虑做标准化,但同时也有一些自下而上的工具库抹平一些差异,比如 Serverless Framework 等。

而我们写 FAAS 函数时,也尽量将与平台绑定的入口函数写得轻一些,将真正的入口放在通用的比如 main 函数中。

3. 总结

Serverless 的价值远比挑战大,其理念可以切实解决许多研发效能问题。

但目前 Serverless 发展阶段仍处于早期,国内的 Serverless 也处于尝试阶段,而且执行环境存在诸多限制,也就是并没有完全实现 Serverless 的美好理念,因此如果什么都往上套一定会踩坑。

可能在 3-5 年后,这些坑会被填平,那么你是选择加入填坑大军,还是选一个合适的场景使用 Serverless 呢?

讨论地址是:精读《Serverless 给前端带来了什么》 · Issue #135 · dt-fe/weekly

如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

查看原文

TerryZeng 发布了文章 · 2019-11-28

VuePress 中增加用户登录功能

在 VuePress 中增加用户登录

VuePress 是 Vuejs 官方提供的一个快速建设文档站点的工具,在简单配置好功能后,需要做的事情就剩下写好一个个 Markdown 文档。

因为 VuePress 提供了可以在 Markdown 中使用 Vue 的能力,所以有时候,希望可以在它的文档功能基础上增加部分常规功能,比如用户登录;有团队希望公司建设的文档内容仅公司员工可查看,因为有可能会有涉及内容保密的部分

VuePress 本身的安装配置过程不再赘述,可参考官方文档,本文将介绍使用 v-dialogs 对 VuePress 增加用户登录功能的进行改造,仅作为抛砖引玉,更多的需求,大家可以自由发挥想象。

安装插件

安装 v-dialogs 插件,将会使用它的模态窗口 (Modal) 和消息通知 (Alert) 的功能

# npm
npm i v-dialogs -D

# yarn
yarn add -D v-dialogs

创建登录表单

新增 Login.vue 文件用于登录表单,它将使用模态窗口(Modal)进行展示

<template>
  <div class="login-form">
    <div class="form-header">User Name</div>
    <div>
      <input type="text" class="form-control" v-model="username">
    </div>
    <div class="form-header">Password</div>
    <div>
      <input type="password" class="form-control" v-model="password">
    </div>

    <div class="btn-row">
      <button class="btn" @click="login">
        OK
      </button>
    </div>
  </div>
</template>

<script>
import { STORAGE_KEY } from './helper'

export default {
  data () {
    return {
      username: '',
      password: ''
    }
  },
  methods: {
    login () {
      if (this.username && this.password) {
        const data = JSON.stringify({
          name: this.username,
          time: new Date().getTime()
        })
        // 登录成功后的逻辑处理,这里将数据保存在 localStorage 中仅作为功能演示
        window.localStorage.setItem(STORAGE_KEY, data)
        // 关闭窗口
        this.$emit('close', true)
      } else {
        this.$dlg.alert('Please complete the content', {
          messageType: 'warning'
        })
      }
    }
  }
}
</script>

<style lang="stylus">
.login-form
  padding: 1rem
  display flex
  flex-direction column
  box-sizing border-box
  .btn-row
    margin-top 1rem
  .btn
    padding 0.6rem 2rem
    outline none
    background-color #60C084
    color white
    border 0
  .form-header
    color #666
    margin-bottom 0.5rem
  .form-control
    padding 0.6rem
    border 2px solid #ddd
    width 100%
    margin-bottom 0.5rem
    box-sizing border-box
    outline none
    transition border 0.2s ease
    &:focus
      border 2px solid #aaa
</style>

VuePress 配置

/.vuepress 位置新增 enhanceApp.js 文件,该文件是 VuePress 对 应用级别的配置 的配置文件,文件 export default 了一个钩子函数,并接受一个包含了一些应用级别属性的对象作为参数。你可以使用这个钩子来安装一些附加的 Vue 插件、注册全局组件,或者增加额外的路由钩子等

import { checkAuth } from './login/helper'
import Login from './login/Login'

export default ({
  Vue,
  options,
  router,
  siteData
}) => {
  Vue.mixin({
    mounted() {
      const doCheck = () => {
        if (!checkAuth()) {
          this.$dlg.modal(Login, {
            width: 300,
            height: 350,
            title: 'Employee login',
            singletonKey: 'employee-login',
            maxButton: false,
            closeButton: false,
            callback: data => {
              if (data === true) {
                // do some stuff after login
              }
            }
          })
        }
      }

      if (this.$dlg) {
        doCheck()
      } else {
        import('v-dialogs').then(resp => {
          Vue.use(resp.default)
          this.$nextTick(() => {
            doCheck()
          })
        })
      }
    }
  })
}

代码中使用了 Vue.mixin 对全局进行了混入操作,所以在每个文档页面被访问后都会触发该 mounted() 生命周期进行用户权限的校验。在这里需要特别说明的是,原来对于权限检查的操作,本是希望在 Vue Router 的路由守卫中处理,但由于 浏览器的 API 访问限制 原因,Vue 插件在注册的过程中因为需要使用到浏览器的API (window 或 document),但在编译为静态文件的过程中,需要通过 Node.js 服务端渲染,因此所有的 Vue 相关代码都应当遵循 编写通用代码 的要求。简而言之,请确保只在 beforeMount 或者 mounted 访问浏览器 / DOM 的 API

v-dialogs 在注册的过程中需要使用到 document 这个对象,所以在编译的过程中会出现 document is not defined 的错误信息,基于上述的原因,对于功能权限的检查在 mounted 生命周期中执行,并将该操作进行全局混入,才能达到全局校验的效果

上述的代码编写部署并重新构建后,就会在每个文档页面中执行用户身份校验

  • 未登录,则弹出模态窗口要求输入身份信息进行校验
  • 已登录时就显示正确的文档内容

实例

可以访问下面的站点进行在线预览登录功能的改造

输入任意用户名和密码进行体验即可

源代码

请访问: https://github.com/TerryZ/vuepress-login

查看原文

赞 0 收藏 0 评论 1

TerryZeng 收藏了文章 · 2019-10-21

前端架构之路(7) - 私有 npm 仓库

私有 npm 仓库

1. “私有 npm 仓库” 有何用

组件化之后,搭建 “私有 npm 仓库” 是个不错选择。

私有 npm 仓库可以让我们使用组件就像 npm 官方仓库里的包一样方便。

一般私有 npm 仓库有以下一些特性:

  • 私有包托管在内部服务器或者单独的服务器上;
  • 可以同步整个官方仓库,也可以只同步需要的;
  • 下载的时候,可以让公共包走公共仓库,私有包走私有仓库;
  • 可以缓存下载过的包;
  • 对于下载,发布,有对应的权限管理。

目前比较好的解决方案有两种:

  1. sinopiaverdaccio
  2. cnpm + cnpmjs.org

2. sinopiaverdaccio

sinopia 在15年的时候就停止更新了,继而由 verdaccio 提供更新升级,所以两者用法基本上都是一致的,以下都以 sinopia 为例说明。

2.1 安装

$ npm install -g sinopia

2.2 配置

配置文件位于 ~/.config/sinopia/config.yaml

# path to a directory with all packages
storage: /home/{user}/.local/share/sinopia/storage # 库存路径,需要考虑磁盘空间

web: # 自定义web项,即浏览器访问页面
  title: Sinopia

auth:
  htpasswd:
    file: ./htpasswd # 添加用户(npm adduser)后自动创建,保存用户信息,可以初始化用户

uplinks: # 可以配置多个上游地址,后面packages中的proxy指定用哪个
  npmjs:
    url: https://registry.npm.taobao.org/ # 更改此上游地址

packages: # 包的权限管理,$all为所有人,$authenticated为通过验证人
          # 分布和安装两种权限,值可以特指某几人
  '@*/*': # 跟package.json中的name属性进行匹配
    # scoped packages
    access: $all
    publish: $authenticated

  '*':
    access: $all

    publish: $authenticated

    proxy: npmjs

logs:
  - {type: stdout, format: pretty, level: http}

listen: 0.0.0.0:4873 # 设置监听地址,0.0.0.0匹配本机地址

更多配置参考 sinopia

2.3 设置 npm registry 属性

$ npm config set registry http://{服务器ip}:4873/

推荐使用 nrm 来快速切换 npm registry 配置。

2.4 运行

sinopia

或者使用 pm2 永久运行

pm2 start sinopia

现在你就可以发布私有包了。

3. cnpm + cnpmjs.org

sinopia 的优点是配置简单,对环境依赖少(仅 node 就够了),并且支持 windows 系统下运行。

但它也有缺点,主要是以下几点:

  • 权限管理比较弱,对用户权限,发布权限,下载权限控制不是很得心应手;
  • 缓存优化不足,经常会在安装共有包的时候处于挂起状态;
  • 不能做官方仓库的镜像。

所以,sinopia 比较适合个人搭建在本地作为 npm 缓存,这样,安装过的包会直接从缓存中获取,加快安装速度。

对于企业级的应用来说,就需要另外一个解决方案了:cnpm + cnpmjs.org

环境依赖:

  • node >= 4.3.1
  • linux(不支持 windows)
  • 数据库(mysql, sqlite, MariaDB, PostgreSQL)

3.1 安装

$ npm install -g cnpm
$ npm install -g cnpmjs.org

3.2 配置

配置文件位于 ~/.cnpmjs.org/config.json

{
  "debug": false, // 是否启动 debug 模式
  "enableCluster": true, // 是否启用 cluster 模式
  "mysqlServers": [ // 数据库配置,以 mysql 为例
    {
      "host": "host",
      "port": 3306,
      "user": "user",
      "password": "password"
    }
  ],
  "mysqlDatabase": "cnpmjs", // 数据库名
  "enablePrivate": true, // 是否启用私有化,这样只有定义在 `admins` 中的用户才能发布
  "admins": { // 管理员配置,可以配置多个
    "senntyou": "jiangjinbelief@163.com"
  },
  "syncModel": "exist", // 同步模式
  "scopes": [ // 包前缀,如果不是以这个前缀命名的包将不能发布,可以配置多个
    "@test"
  ],
  "registryHost": "http://your.company.npm.registry.com", // 你的服务器对应的 npm registry 地址
  "sourceNpmRegistry": "https://registry.npm.taobao.org" // 如果在该仓库中找不到的包,会上游到哪里去找
}

更多配置参考 cnpmjs.org config

3.3 设置 cnpm registry 属性

$ cnpm config set registry http://your.company.npm.registry.com

3.4 运行

cnpmjs.org start

运行的时候将会开启两个端口:7001(registry),7002(web)。

7001(registry)

用来在命令行发布,下载包等对用的远程 registry 地址。比如一些比较常用的 registry 地址:

nginx 配置 url 地址到 7001 端口:

server {
    listen 80;
    listen [::]:80;

    server_name your.company.npm.registry.com;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass http://127.0.0.1:7001;

    }

    location ~ /.well-known {
        allow all;
    }

    client_max_body_size 50m;
}

重启 nginx,现在你就可以通过 http://your.company.npm.registry.com 去发布私有包到这个仓库里了(如果没有其他问题的话)。

7002(web)

用来在 web 端查看仓库信息,搜索包,包信息等。比如一些比较常用的地址:

nginx 配置 url 地址到 7002 端口:

server {
    listen 80;
    listen [::]:80;

    server_name your.company.npm.com;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass http://127.0.0.1:7002;

    }

    location ~ /.well-known {
        allow all;
    }

    client_max_body_size 50m;
}

重启 nginx,现在你就可以通过 http://your.company.npm.com 访问仓库web页面了(如果没有其他问题的话)。

永久运行

如果仅通过 cnpmjs.org start 运行,一旦关闭终端,程序就会停止运行。推荐使用 screen 命令开启永久运行,可以通过 Linux screen 了解下这个命令的用法。

# 创建一个名为 cnpmjs 的窗口
$ screen -S cnpmjs

# 运行程序
$ cnpmjs.org start

# 按Ctrl+a,然后再按d,离开当前窗口,而程序不会终止
Ctrl+a+d

# 重新进入 cnpmjs 的窗口,做更多的操作
$ screen -r cnpmjs

4. 后续

上一篇:组件化

下一篇:单页面应用(SPA)、按需加载

参考文章:

  1. 用sinopia在linux系统中搭建npm私有库

更多博客,查看 https://github.com/senntyou/blogs

作者:深予之 (@senntyou)

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

查看原文

TerryZeng 赞了文章 · 2019-10-21

前端架构之路(7) - 私有 npm 仓库

私有 npm 仓库

1. “私有 npm 仓库” 有何用

组件化之后,搭建 “私有 npm 仓库” 是个不错选择。

私有 npm 仓库可以让我们使用组件就像 npm 官方仓库里的包一样方便。

一般私有 npm 仓库有以下一些特性:

  • 私有包托管在内部服务器或者单独的服务器上;
  • 可以同步整个官方仓库,也可以只同步需要的;
  • 下载的时候,可以让公共包走公共仓库,私有包走私有仓库;
  • 可以缓存下载过的包;
  • 对于下载,发布,有对应的权限管理。

目前比较好的解决方案有两种:

  1. sinopiaverdaccio
  2. cnpm + cnpmjs.org

2. sinopiaverdaccio

sinopia 在15年的时候就停止更新了,继而由 verdaccio 提供更新升级,所以两者用法基本上都是一致的,以下都以 sinopia 为例说明。

2.1 安装

$ npm install -g sinopia

2.2 配置

配置文件位于 ~/.config/sinopia/config.yaml

# path to a directory with all packages
storage: /home/{user}/.local/share/sinopia/storage # 库存路径,需要考虑磁盘空间

web: # 自定义web项,即浏览器访问页面
  title: Sinopia

auth:
  htpasswd:
    file: ./htpasswd # 添加用户(npm adduser)后自动创建,保存用户信息,可以初始化用户

uplinks: # 可以配置多个上游地址,后面packages中的proxy指定用哪个
  npmjs:
    url: https://registry.npm.taobao.org/ # 更改此上游地址

packages: # 包的权限管理,$all为所有人,$authenticated为通过验证人
          # 分布和安装两种权限,值可以特指某几人
  '@*/*': # 跟package.json中的name属性进行匹配
    # scoped packages
    access: $all
    publish: $authenticated

  '*':
    access: $all

    publish: $authenticated

    proxy: npmjs

logs:
  - {type: stdout, format: pretty, level: http}

listen: 0.0.0.0:4873 # 设置监听地址,0.0.0.0匹配本机地址

更多配置参考 sinopia

2.3 设置 npm registry 属性

$ npm config set registry http://{服务器ip}:4873/

推荐使用 nrm 来快速切换 npm registry 配置。

2.4 运行

sinopia

或者使用 pm2 永久运行

pm2 start sinopia

现在你就可以发布私有包了。

3. cnpm + cnpmjs.org

sinopia 的优点是配置简单,对环境依赖少(仅 node 就够了),并且支持 windows 系统下运行。

但它也有缺点,主要是以下几点:

  • 权限管理比较弱,对用户权限,发布权限,下载权限控制不是很得心应手;
  • 缓存优化不足,经常会在安装共有包的时候处于挂起状态;
  • 不能做官方仓库的镜像。

所以,sinopia 比较适合个人搭建在本地作为 npm 缓存,这样,安装过的包会直接从缓存中获取,加快安装速度。

对于企业级的应用来说,就需要另外一个解决方案了:cnpm + cnpmjs.org

环境依赖:

  • node >= 4.3.1
  • linux(不支持 windows)
  • 数据库(mysql, sqlite, MariaDB, PostgreSQL)

3.1 安装

$ npm install -g cnpm
$ npm install -g cnpmjs.org

3.2 配置

配置文件位于 ~/.cnpmjs.org/config.json

{
  "debug": false, // 是否启动 debug 模式
  "enableCluster": true, // 是否启用 cluster 模式
  "mysqlServers": [ // 数据库配置,以 mysql 为例
    {
      "host": "host",
      "port": 3306,
      "user": "user",
      "password": "password"
    }
  ],
  "mysqlDatabase": "cnpmjs", // 数据库名
  "enablePrivate": true, // 是否启用私有化,这样只有定义在 `admins` 中的用户才能发布
  "admins": { // 管理员配置,可以配置多个
    "senntyou": "jiangjinbelief@163.com"
  },
  "syncModel": "exist", // 同步模式
  "scopes": [ // 包前缀,如果不是以这个前缀命名的包将不能发布,可以配置多个
    "@test"
  ],
  "registryHost": "http://your.company.npm.registry.com", // 你的服务器对应的 npm registry 地址
  "sourceNpmRegistry": "https://registry.npm.taobao.org" // 如果在该仓库中找不到的包,会上游到哪里去找
}

更多配置参考 cnpmjs.org config

3.3 设置 cnpm registry 属性

$ cnpm config set registry http://your.company.npm.registry.com

3.4 运行

cnpmjs.org start

运行的时候将会开启两个端口:7001(registry),7002(web)。

7001(registry)

用来在命令行发布,下载包等对用的远程 registry 地址。比如一些比较常用的 registry 地址:

nginx 配置 url 地址到 7001 端口:

server {
    listen 80;
    listen [::]:80;

    server_name your.company.npm.registry.com;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass http://127.0.0.1:7001;

    }

    location ~ /.well-known {
        allow all;
    }

    client_max_body_size 50m;
}

重启 nginx,现在你就可以通过 http://your.company.npm.registry.com 去发布私有包到这个仓库里了(如果没有其他问题的话)。

7002(web)

用来在 web 端查看仓库信息,搜索包,包信息等。比如一些比较常用的地址:

nginx 配置 url 地址到 7002 端口:

server {
    listen 80;
    listen [::]:80;

    server_name your.company.npm.com;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass http://127.0.0.1:7002;

    }

    location ~ /.well-known {
        allow all;
    }

    client_max_body_size 50m;
}

重启 nginx,现在你就可以通过 http://your.company.npm.com 访问仓库web页面了(如果没有其他问题的话)。

永久运行

如果仅通过 cnpmjs.org start 运行,一旦关闭终端,程序就会停止运行。推荐使用 screen 命令开启永久运行,可以通过 Linux screen 了解下这个命令的用法。

# 创建一个名为 cnpmjs 的窗口
$ screen -S cnpmjs

# 运行程序
$ cnpmjs.org start

# 按Ctrl+a,然后再按d,离开当前窗口,而程序不会终止
Ctrl+a+d

# 重新进入 cnpmjs 的窗口,做更多的操作
$ screen -r cnpmjs

4. 后续

上一篇:组件化

下一篇:单页面应用(SPA)、按需加载

参考文章:

  1. 用sinopia在linux系统中搭建npm私有库

更多博客,查看 https://github.com/senntyou/blogs

作者:深予之 (@senntyou)

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

查看原文

赞 18 收藏 15 评论 0

TerryZeng 发布了文章 · 2019-10-15

v-region 2.2.2 更新 - 基于 Vue2 的中国行政区划选择器(4 级联动城市选择器)

v-region v2.2.2 版本更新内容:

  • 使用 render 函数的方式完全重构插件
  • 重构样式设计,使得交互界面更加简洁、清晰
  • 移除 uitextcolumncity-picker prop 入参,改为使用 type prop 进行设置,默认值为 "select"
  • 移除 selected prop
  • 增加 v-model/value 支持
  • 增加自定义选择器呼出对象的作用域插槽( scoped slot )支持,输出 "region" 和 "show" 数据
  • 增加单元测试及代码测试覆盖率检测
  • 修改 css 预编译程序从 SCSSStylus
  • 使用 Javascript Standard 语法标准对项目进行格式化
  • 升级 v-dropdown 升级至 v2.1.1 版本

插件简介

v-region

基于 Vue2 的简洁易用的中国行政区划选择器,4 级联动城市选择器。

包含以下几种使用模式:

  • 常规表单下拉元素模式( Select )
  • 多分组切换( Group )
  • 多列竖排模式( Column )
  • 纯文本展示模式( Text )
  • 专用城市选择模式( City )

实例和文档

可在 CodePen 上快速预览,更多的实例和文档请浏览以下的站点

v-region

查看原文

赞 0 收藏 0 评论 0

TerryZeng 赞了问题 · 2019-03-05

解决如何使用localStorage结合Vuex来保存用户登录信息?

我现在有一个 headerBar 组件,上面显示了用户名称(如果设置了昵称显示昵称,否则显示用户名称),当用户点击登录按钮时,调用api获取用户信息,保存到Vuexstate里面,headerBar通过this.$store.getters.xxx来获取用户登录信息,但是当用户刷新时,state里面的用户信息全没了,所以我考虑加入 localStorage 来保存用户信息,但是这部分代码不知道该如何“分布”,因为要考虑用户登录超时,请小伙伴们指点指点,谢谢!

代码如下:

export default new Vuex.Store({
    state: {
        loginInfo: null,//当前用户简要信息
    },
    getters: {
        GET_LOGININFO(state) {
            //先从state里面获取用户登录信息
            let loginInfo = state.loginInfo;
            //如果 state 里面获取不到,那么从localStorage里面获取
            if(!loginInfo){
                loginInfo = JSON.parse(window.localStorage.getItem('loginInfo') || null)
            }
            return loginInfo;
        },
    },
    mutations: {
        SET_LOGININFO(state, data){
            state.userInfo = data.data;
        }
    },
    actions: {
        Login(context, data) {
            axios.post('/api/login', {
                userName: 'admin',
                pwd: '123456'
            })
            .then((res) => {
                //登录成功,保存当前用户信息到 state 里面,以便其他组建获取
                context.commit('SET_LOGININFO', res.data);
                //保存到localStorage里面
                window.localStorage.setItem('loginInfo', JSON.stringify(items));
                return res;                
            })
            .catch(function (error) {});
        },
    }
})

想请教几个问题:
1、这么使用 localStorage 正确么?或者合理么?有没有更好的方法呢?
2、在getters下面的GET_LOGININFO方法里面,如果进入了if(!loginInfo)语句,该方法是否可以为 state 属性loginInfo赋值呢(或者调用mutations方法来给loginInfo赋值)?
3、这样的话,如果用户点击退出按钮,是不是意味着需要清空state的loginInfo,还需要清除localStorage下面的loginInfo呢?
4、这样的话,从客户端角度来看,是不是客户就永远保持以登录状态了?这个环节该怎么做好呢?

补充:第二个问题其实就是:getters里面的方法是否可以为state属性赋值(直接访问state属性赋值 或者 调用mutations方法赋值)?

关注 14 回答 2

TerryZeng 收藏了问题 · 2019-03-05

如何使用localStorage结合Vuex来保存用户登录信息?

我现在有一个 headerBar 组件,上面显示了用户名称(如果设置了昵称显示昵称,否则显示用户名称),当用户点击登录按钮时,调用api获取用户信息,保存到Vuexstate里面,headerBar通过this.$store.getters.xxx来获取用户登录信息,但是当用户刷新时,state里面的用户信息全没了,所以我考虑加入 localStorage 来保存用户信息,但是这部分代码不知道该如何“分布”,因为要考虑用户登录超时,请小伙伴们指点指点,谢谢!

代码如下:

export default new Vuex.Store({
    state: {
        loginInfo: null,//当前用户简要信息
    },
    getters: {
        GET_LOGININFO(state) {
            //先从state里面获取用户登录信息
            let loginInfo = state.loginInfo;
            //如果 state 里面获取不到,那么从localStorage里面获取
            if(!loginInfo){
                loginInfo = JSON.parse(window.localStorage.getItem('loginInfo') || null)
            }
            return loginInfo;
        },
    },
    mutations: {
        SET_LOGININFO(state, data){
            state.userInfo = data.data;
        }
    },
    actions: {
        Login(context, data) {
            axios.post('/api/login', {
                userName: 'admin',
                pwd: '123456'
            })
            .then((res) => {
                //登录成功,保存当前用户信息到 state 里面,以便其他组建获取
                context.commit('SET_LOGININFO', res.data);
                //保存到localStorage里面
                window.localStorage.setItem('loginInfo', JSON.stringify(items));
                return res;                
            })
            .catch(function (error) {});
        },
    }
})

想请教几个问题:
1、这么使用 localStorage 正确么?或者合理么?有没有更好的方法呢?
2、在getters下面的GET_LOGININFO方法里面,如果进入了if(!loginInfo)语句,该方法是否可以为 state 属性loginInfo赋值呢(或者调用mutations方法来给loginInfo赋值)?
3、这样的话,如果用户点击退出按钮,是不是意味着需要清空state的loginInfo,还需要清除localStorage下面的loginInfo呢?
4、这样的话,从客户端角度来看,是不是客户就永远保持以登录状态了?这个环节该怎么做好呢?

补充:第二个问题其实就是:getters里面的方法是否可以为state属性赋值(直接访问state属性赋值 或者 调用mutations方法赋值)?

TerryZeng 赞了回答 · 2019-03-05

解决如何使用localStorage结合Vuex来保存用户登录信息?

  1. 不合理。用户登录成功以后应该在本地保存一份用户数据,注意我说的是保存到本地不是保存到localstorage,因为保存本地的方法有很多种,比如cookieindexedDB等,所以,代码中不应该直接调用window.localStorage,而是应该封装一个用户数据的读取类,解除代码耦合,将来要改成其他存储方式比较简单:

    const USER_INFO='USER_INFO'
    function getUserinfo(){}
    function setUserinfo(){}
  2. 不应该。个人认为不应该,getters语义上就是获取数据,但是却改变了数据,导致不纯净,可能会埋下维护上的隐患。

  3. 是的。理论上应该有一个接口用来更新用户状态,比如判断用户是否需要重新登录之类的,比如api/refresh, 所以逻辑应该是:

    • 用户进入app,判断本地是否有用户信息。

      • 有,调用api/refresh,判断是否需要重新登录。

        • 不需要(连续登录),将信息保存在vuex中,并进入首页,往后数据读取全部走vuex

        • 需要(长时间未登录),删除本地用户信息并跳转到登录流程。

      • 没有, 跳转到登录流程(以下是登录流程)。

        • 调用api/login登录。

        • 将保存到本地,并保存到vuex中,往后数据读取全部走vuex

  4. 看3,具体还可以看看jwt,或者基于tokenapi设计相关的文章。

关注 14 回答 2

TerryZeng 收藏了文章 · 2019-03-04

WEB前端面试选择题解答(共36题)

第1题

["1", "2", "3"].map(parseInt) 

A:["1", "2", "3"]

B:[1, 2, 3]

C:[0, 1, 2]

D:other

解释:该题目的答案为:[1, NaN, NaN];即选择D。该题用到了map与parseInt;parseInt() 函数的语法是parseInt(string, radix);
string 必需。要被解析的字符串。
radix可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。
如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。
如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN

实际上 map里面的callback函数接受的是三个参数 分别为元素 下标和数组 (虽然很多情况只使用第一个参数)

回调函数的语法如下所示:
function callbackfn(value, index, array1)
可使用最多三个参数来声明回调函数。

例:
var a=["1", "2", "3", "4","5",6,7,8,9,10,11,12,13,14,15];
a.map(parseInt);
返回结果为:[1, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, 9, 11, 13, 15, 17, 19]

第2题

[typeof null, null instanceof Object]

A: ["object", false]

B: [null, false]

C: ["object", true]

D: other

解释:

考察typeof运算符和instanceof运算符,上MDN上看一下typeof运算符,一些基础类型的结果为:

Undefined "undefined"
Null "object"
Boolean "boolean"
Number "number"
String "string"
Any other object "object"
Array "object"

null instanceof 任何类型 都是false,所以选A。

第3题

[ [3,2,1].reduce(Math.pow), [].reduce(Math.pow)] 

A: an error

B: [9, 0]

C: [9, NaN]

D: [9, undefined]

解答:这题考的Math.powArray.prototype.reduce

Math.pow(base, exponent)接受两个参数:基数、需要计算的次方

reduce传递给其作为参数的函数几个值:

  • previousValue:上一次计算的结果
  • currentValue:当前元素的值
  • index: 当前元素在数组中的位置
  • array:整个数组

reduce本身接受两个参数,callbackinitialValue,分别是reduce的回调函数和计算初始值--也就是第一次reduce的callback被调用时的previousValue的值,默认为0

reduce在数组为空且没有定义initialValue时,会抛出错误,在火狐下报错为:TypeError: reduce of empty array with no initial value
所以选A

第4题

var val = 'smtg';
console.log('Value is ' + (val === 'smtg') ? 'Something' : 'Nothing');

A: Value is Something

B: Value is Nothing

C: NaN

D: other

解答:这题考的javascript中的运算符优先级,这里'+'运算符的优先级要高于'?'所以运算符,实际上是 'Value is true'?'Something' : 'Nothing',当字符串不为空时,转换为bool为true,所以结果为'Something',选D

第5题

var name = 'World!';
(function () {
    if (typeof name === 'undefined') {
        var name = 'Jack';
        console.log('Goodbye ' + name);
    } else {
        console.log('Hello ' + name);
    }
})(); 

A: Goodbye Jack

B: Hello Jack

C: Hello undefined

D: Hello World

这题考的是javascript作用域中的变量提升,javascript的作用于中使用var定义的变量都会被提升到所有代码的最前面,于是乎这段代码就成了:

var name = 'World!';
(function () {
    var name;//现在还是undefined
    if (typeof name === 'undefined') {
        name = 'Jack';
        console.log('Goodbye ' + name);
    } else {
        console.log('Hello ' + name);
    }
})();

这样就很好理解了,typeof name === 'undefined'的结果为true,所以最后会输出'Goodbye Jack',选A

第6题

var END = Math.pow(2, 53);
var START = END - 100;
var count = 0;
for (var i = START; i <= END; i++) {
    count++;
}
console.log(count);

A: 0

B: 100

C: 101

D: other

解答:这题考查javascript中的数字的概念:首先明确一点,javascript和其他语言不同,仅有一种数字,IEEE 754标准的64位浮点数,能够表示的整数范围是-2^53~2^53(包含边界值),所以Math.pow(2, 53)即为javascript中所能表示的最大整数,在最大整数在继续增大就会出现精度丢失的情况,END + 1 的值其实是等于END的,这也就造成了死循环。

第7题

var ary = [0,1,2];
ary[10] = 10;
ary.filter(function(x) { return x === undefined;});

A: [undefined × 7]

B: [0, 1, 2, 10]

C: []

D: [undefined]

解答:选择C Array.prototype.filter is not invoked for the missing elements(缺少的元素,不会调用过滤器)。

第8题

var two   = 0.2
var one   = 0.1
var eight = 0.8
var six   = 0.6
[two - one == one, eight - six == two]

A: [true, true]

B: [false, false]

C: [true, false]

D: other

解答:浮点数计算时的精度丢失问题。.chrome中计算出来的结果:[0.1, 0.20000000000000007],也就是[true, false],最终答案选C。

第9题

function showCase(value) {
    switch(value) {
    case 'A':
        console.log('Case A');
        break;
    case 'B':
        console.log('Case B');
        break;
    case undefined:
        console.log('undefined');
        break;
    default:
        console.log('Do not know!');
    }
}
showCase(new String('A'));

A: Case A

B: Case B

C: Do not know!

D: undefined

解答:此题考察的是关于new string();其实就是new一个实例对象。要匹配的也是object;所以答案是Do not know!,选择C。

第10题

function showCase2(value) {
    switch(value) {
    case 'A':
        console.log('Case A');
        break;
    case 'B':
        console.log('Case B');
        break;
    case undefined:
        console.log('undefined');
        break;
    default:
        console.log('Do not know!');
    }
}
showCase(String('A'));

A: Case A

B: Case B

C: Do not know!

D: undefined

和上题原理一样,不过这里没有使用new来生成字符串,所以生成的结果就是原始字符串,相当于showCase('A'),所以结果就是A

第11题

function isOdd(num) {
    return num % 2 == 1;
}
function isEven(num) {
    return num % 2 == 0;
}
function isSane(num) {
    return isEven(num) || isOdd(num);
}
var values = [7, 4, '13', -9, Infinity];
values.map(isSane);

A: [true, true, true, true, true]

B: [true, true, true, true, false]

C: [true, true, true, false, false]

D: [true, true, false, false, false]

还是JS的数字相关,不过这次考察的是取模,这题我也是瞎蒙的(果断跪了)。

前两个基本上没什么疑问,必然是true

'13'在进行计算前则会进行隐式类型转换(JS最恶心的部分之一),详细参见$雨$的文章《Javascript类型转换的规则》,这里的规则就是将字符串通过Number()方法转换为数字,所以结果为13 % 2 ,也就是true

而JS中负数取模的结果是负数,这里-9%2的结果实际上是-1,所以为false

Infinity对任意数取模都是NaN,所以是false

综上,结果为[true, true, true, false, false],也就是C

第12题

parseInt(3, 8)
parseInt(3, 2)
parseInt(3, 0)

A: 3, 3, 3

B: 3, 3, NaN

C: 3, NaN, NaN

D: other

还是parseInt的题,考的和第一题类似,第一个值为3没什么好说的。如果出现的数字不符合后面输入的进制,则为NaN,所以第二个值为NaN。而radix为0时的情况第一题下面有介绍,这里也是一样为默认10,所以结果为3,所以答案为3, NaN, 3,选D

第13题

Array.isArray( Array.prototype )

A: true

B: false

C: error

D: other

死知识,MDN传送门,这是MDN官方给的例子...选A

第14题

var a = [0];
if ([0]) { 
  console.log(a == true);
} else { 
  console.log("wut");
}

A: true

B: false

C: "wut"

D: other

同样是一道隐式类型转换的题,不过这次考虑的是'=='运算符,a本身是一个长度为1的数组,而当数组不为空时,其转换成bool值为true。

而==左右的转换,会使用如果一个操作值为布尔值,则在比较之前先将其转换为数值的规则来转换,Number([0]),也就是0,于是变成了0 == true,结果自然是false,所以最终结果为B

第15题

[] == []

A: true

B: false

C: error

D: other

这题考的是数组字面量创建数组的原理和==运算符,首先JS中数组的真实类型是Object这点很明显typeof []的值为"object",而==运算符当左右都是对象时,则会比较其是否指向同一个对象。而每次调用字面量创建,都会创造新的对象,也就是会开辟新的内存区域。所以指针的值自然不一样,结果为 false,选B

第16题

'5' + 3  
'5' - 3 

A: 53, 2

B: 8, 2

C: error

D: other

又是一道隐式类型转换的题

加法: 加法运算中,如果有一个操作值为字符串类型,则将另一个操作值转换为字符串,最后连接起来

减法: 如果操作值之一不是数值,则被隐式调用Number()函数进行转换

所以第一行结果为字符串运算,为'53'。第二行结果为2,选A

第17题

1 + - + + + - + 1 

A: 2

B: 1

C: error

D: other

C语言中的经典...对于这种问题,原理什么的不懂,蒙吧,结果是2。选A

第18题

var ary = Array(3);
ary[0]=2
ary.map(function(elem) { return '1'; }); 

A: [2, 1, 1]

B: ["1", "1", "1"]

C: [2, "1", "1"]

D: other

又是考的Array.prototype.map的用法,map在使用的时候,只有数组中被初始化过元素才会被触发,其他都是undefined,所以结果为["1", undefined × 2],选D

第19题

function sidEffecting(ary) { 
  ary[0] = ary[2];
}
function bar(a,b,c) { 
  c = 10
  sidEffecting(arguments);
  return a + b + c;
}
bar(1,1,1)

A: 3

B: 12

C: error

D: other

这题考的是JS的函数arguments的概念:

在调用函数时,函数内部的arguments维护着传递到这个函数的参数列表。它看起来是一个数组,但实际上它只是一个有length属性的Object,不从Array.prototype继承。所以无法使用一些Array.prototype的方法。

arguments对象其内部属性以及函数形参创建getter和setter方法,因此改变形参的值会影响到arguments对象的值,反过来也是一样

具体例子可以参见Javascript秘密花园#arguments

所以,这里所有的更改都将生效,a和c的值都为10,a+b+c的值将为21,选D

第20题

var a = 111111111111111110000,
    b = 1111;
a + b;

A: 111111111111111111111

B: 111111111111111110000

C: NaN

D: Infinity

又是一道考查JavaScript数字的题,与第七题考察点相似。由于JavaScript实际上只有一种数字形式IEEE 754标准的64位双精度浮点数,其所能表示的整数范围为-2^53~2^53(包括边界值)。这里的111111111111111110000已经超过了2^53次方,所以会发生精度丢失的情况。综上选B

第21题

var x = [].reverse;
x();

A: []

B: undefined

C: error

D: window

这题考查的是函数调用时的thisArray.prototype.reverse方法。
首先看Array.prototype.reverse方法,首先举几个栗子:

console.log(Array.prototype.reverse.call("skyinlayer"));
//skyinlayer
console.log(Array.prototype.reverse.call({}));
//Object {}
console.log(Array.prototype.reverse.call(123));
//123

这几个栗子可以得出一个结论,Array.prototype.reverse方法的返回值,就是this

Javascript中this有如下几种情况:

全局下this,指向window对象

console.log(this);
//输出结果:
//Window {top: Window, window: Window, location: Location, external: Object, chrome: Object…}

函数调用,this指向全局window对象:

function somefun(){
    console.log(this);
}
somefun();
//输出结果:
//Window {top: Window, window: Window, location: Location, external: Object, chrome: Object…}

方法调用,this指向拥有该方法的对象:

var someobj = {};
someobj.fun = function(){
    console.log(this);
};
console.log(someobj.fun());
//输出结果:
//Object {fun: function}

调用构造函数,构造函数内部的this指向新创建对象:

function Con() {
    console.log(this);
}
Con.prototype.somefun = function(){};
console.log(new Con());
//输出结果:
//Con {somefun: function}

显示确定this:

function somefun(){
    console.log(this);
};
somefun.apply("skyinlayer");
somefun.call("skyinlayer");
//输出结果:
//String {0: "s", 1: "k", 2: "y", 3: "i", 4: "n", 5: "l", 6: "a", 7: "y", 8: "e", 9: "r", length: 10}
//String {0: "s", 1: "k", 2: "y", 3: "i", 4: "n", 5: "l", 6: "a", 7: "y", 8: "e", 9: "r", length: 10} 

这里报错:Uncaught TypeError: Array.prototype.reverse called on null or undefined,选C。

第22题

Number.MIN_VALUE > 0

A: false

B: true

C: error

D: other

考查的Number.MIN_VALUE的概念,MDN传送门,关键的几句话

  • The Number.MIN_VALUE property represents the smallest positive numeric value representable in JavaScript.

翻译:Number.MIN_VALUE表示的是JavaScript中最小的正数

  • The MIN_VALUE property is the number closest to 0, not the most negative number, that JavaScript can represent.

翻译:MIN_VALUE是接近0的数,而不是最小的数

  • MIN_VALUE has a value of approximately 5e-324. Values smaller than MIN_VALUE ("underflow values") are converted to 0.

翻译:MIN_VALUE值约等于5e-324,比起更小的值(大于0),将被转换为0

所以,这里是true,选B

顺带把Number的几个常量拉出来:

    • Number.MAX_VALUE:最大的正数
    • Number.MIN_VALUE:最小的正数
    • Number.NaN:特殊值,用来表示这不是一个数
    • Number.NEGATIVE_INFINITY:负无穷大
    • Number.POSITIVE_INFINITY:正无穷大

如果要表示最小的负数和最大的负数,可以使用-Number.MAX_VALUE-Number.MIN_VALUE

第23题

[1 < 2 < 3, 3 < 2 < 1]

A: [true, true]

B: [true, false]

C: error

D: other

运算符的运算顺序和隐式类型转换的题,从MDN上运算符优先级,'<'运算符顺序是从左到右,所以变成了[true < 3, false < 1]

接着进行隐式类型转换,'<'操作符的转换规则(来自$雨$的文章《Javascript类型转换的规则》):

  • 如果两个操作值都是数值,则进行数值比较
  • 如果两个操作值都是字符串,则比较字符串对应的字符编码值
  • 如果只有一个操作值是数值,则将另一个操作值转换为数值,进行数值比较
  • 如果一个操作数是对象,则调用valueOf()方法(如果对象没有valueOf()方法则调用toString()方法),得到的结果按照前面的规则执行比较
  • 如果一个操作值是布尔值,则将其转换为数值,再进行比较

所以,这里首先通过Number()转换为数字然后进行比较,true会转换成1,而false转换成0,就变成了[1 < 3, 0 < 1]

所以结果为A

第24题

// the most classic wtf
2 == [[[2]]]

A: true

B: false

C: undefined

D: other

又是隐式类型转换的题(汗)

题目作者的解释是:
both objects get converted to strings and in both cases the resulting string is "2"

也就是说左右两边都被转换成了字符串,而字符串都是"2"

这里首先需要对==右边的数组进行类型转换,根据以下规则(来自justjavac的文章《「译」JavaScript 的怪癖 1:隐式类型转换》):

  1. 调用 valueOf()。如果结果是原始值(不是一个对象),则将其转换为一个数字。
  2. 否则,调用 toString() 方法。如果结果是原始值,则将其转换为一个数字。
  3. 否则,抛出一个类型错误。

所以右侧被使用toString()方法转换为"2",然后又通过Number("2")转换为数字2进行比较,结果就是true了,选A

第25题

3.toString()
3..toString()
3...toString()

A: "3", error, error

B: "3", "3.0", error

C: error, "3", error

D: other

说实话这题有点常见了,很多人都踩过3.toString()的坑(包括我)...虽然JavaScript会在调用方法时对原始值进行包装,但是这个点是小数点呢、还是方法调用的点呢,于是乎第一个就是error了,因为JavaScript解释器会将其认为是小数点。

而第二个则很好说通了,第一个点解释为小数点,变成了(3.0).toString(),结果就是"3"了

第三个也是,第一个点为小数点,第二个是方法调用的点,但是后面接的不是一个合法的方法名,于是乎就error了

综上,选C

第26题

(function(){
  var x = y = 1;
})();
console.log(y);
console.log(x);

A: 1, 1

B: error, error

C: 1, error

D: other

变量提升和隐式定义全局变量的题,也是一个JavaScript经典的坑...

还是那句话,在作用域内,变量定义和函数定义会先行提升,所以里面就变成了:

(function(){
    var x;
    y = 1;
    x = 1;
})();

这点会问了,为什么不是var x, y;,这就是坑的地方...这里只会定义第一个变量x,而y则会通过不使用var的方式直接使用,于是乎就隐式定义了一个全局变量y

所以,y是全局作用域下,而x则是在函数内部,结果就为1, error,选C

第27题

var a = /123/,
    b = /123/;
a == b
a === b

A: true, true

B: true, false

C: false, false

D: other

首先需要明确JavaScript的正则表达式是什么。JavaScript中的正则表达式依旧是对象,使用typeof运算符就能得出结果:

console.log(typeof /123/);
//输出结果:
//"object"

==运算符左右两边都是对象时,会比较他们是否指向同一个对象,可以理解为C语言中两个指针的值是否一样(指向同一片内存),所以两个结果自然都是false

第28题

var a = [1, 2, 3],
    b = [1, 2, 3],
    c = [1, 2, 4]
a == b
a === b
a > c
a < c

A: false, false, false, true

B: false, false, false, false

C: true, true, false, true

D: other

和上题类似,JavaScript中Array的本质也是对象,所以前两个的结果都是false,

而JavaScript中Array的'>'运算符和'<'运算符的比较方式类似于字符串比较字典序,会从第一个元素开始进行比较,如果一样比较第二个,还一样就比较第三个,如此类推,所以第三个结果为false,第四个为true。

综上所述,结果为false, false, false, true,选A

第29题

var a = {}, b = Object.prototype;
[a.prototype === b, Object.getPrototypeOf(a) === b]

A: [false, true]

B: [true, true]

C: [false, false]

D: other

原型链的题(总会有的),考查的__proto__和prototype的区别。首先要明确对象和构造函数的关系,对象在创建的时候,其__proto__会指向其构造函数的prototype属性

Object实际上是一个构造函数(typeof Object的结果为"function"),使用字面量创建对象和new Object创建对象是一样的,所以a.__proto__也就是Object.prototype,而Object.getPrototypeOf(a)与a.__proto__是一样的,所以第二个结果为true

而实例对象是没有prototype属性的,只有函数才有,所以a.prototype其实是undefined,第一个结果为false

综上,选A

第30题

function f() {}
var a = f.prototype, b = Object.getPrototypeOf(f);
a === b

A: true

B: false

C: null

D: other

还是__proto__和prototype的区别,两者不是一个东西,所以选B

第31题

function foo() { }
var oldName = foo.name;
foo.name = "bar";
[oldName, foo.name]

A: error

B: ["", ""]

C: ["foo", "foo"]

D: ["foo", "bar"]

考察了函数的name属性,使用函数定义方式时,会给function对象本身添加一个name属性,保存了函数的名称,很好理解oldName为"foo"。name属性时只读的,不允许修改,所以foo.name = "bar";之后,foo.name还是"foo",所以结果为["foo", "foo"],选C

PS:name属性不是一个标准属性,不要去使用,除非你想要坑别人

第32题

"1 2 3".replace(/\d/g, parseInt)

A: "1 2 3"

B: "0 1 2"

C: "NaN 2 3"

D: "1 NaN 3"

String.prototype.replace、正则表达式的全局匹配和parseInt(又是parseInt...),可以根据题意看出来题目上漏了一个''

首先需要确定replace会传给parseInt哪些参数。举个栗子:

"1 2 3".replace(/\d/g, function(){
    console.log(arguments);
});
//输出结果:
//["1", 0, "1 2 3"]
//["2", 2, "1 2 3"]
//["3", 4, "1 2 3"] 

一共三个:

  1. match:正则表达式被匹配到的子字符串
  2. offset:被匹配到的子字符串在原字符串中的位置
  3. string:原字符串

这样就很好理解了,又回到之前parseInt的问题了,结果就是parseInt("1", 10), parseInt("2", 2), parseInt("3", 4)所以结果为"1, NaN, 3",选D

第33题

function f() {}
var parent = Object.getPrototypeOf(f);
f.name // ?
parent.name // ?
typeof eval(f.name) // ?
typeof eval(parent.name) //  ?

A: "f", "Empty", "function", "function"

B: "f", undefined, "function", error

C: "f", "Empty", "function", error

D: other

又是Function.name属性的题,和三十二题一样样,f.name值为"f",而eval("f")则会输出f函数,所以结果为"function"

接着看parent,parent实际上就是f.__proto__,需要明确的是JavaScript中的函数也是对象,其也有自己的构造函数Function,所以f.__proto__ === Function.prototype结果是true,而Function.prototype就是一个名为Empty的function

console.log(Function.prototype);
console.log(Function.prototype.name);
//输出结果:
//function Empty() {}
//Empty

所以parent.name的值为Empty

如果想直接在全局作用域下调用Empty,显示未定义...因为Empty并不在全局作用域下

综上所述,结果为C

第34题

var lowerCaseOnly =  /^[a-z]+$/;
[lowerCaseOnly.test(null), lowerCaseOnly.test()]

A: [true, false]

B: error

C: [true, true]

D: [false, true]

正则表达式的test方法会自动将参数转换为字符串,原式就变成了[lowerCaseOnly.test("null"), lowerCaseOnly.test("undefined")],结果都是真,所以选C

第35题

[,,,].join(", ")

A: ", , , "

B: "undefined, undefined, undefined, undefined"

C: ", , "

D: ""

JavaScript中使用字面量创建数组时,如果最末尾有一个逗号',',会背省略,所以实际上这个数组只有三个元素(都是undefined):

console.log([,,,].length);
//输出结果:
//3

而三个元素,使用join方法,只需要添加两次,所以结果为", , ",选C

第36题

var a = {class: "Animal", name: 'Fido'};
a.class

A: "Animal"

B: Object

C: an error

D: other

经典坑中的一个,class是关键字。根据浏览器的不同,结果不同:

chrome的结果: "Animal"

Firefox的结果:"Animal"

Opera的结果:"Animal"

IE 8以上也是: "Animal"

IE 8 及以下: 报错

查看原文

TerryZeng 赞了文章 · 2019-03-04

WEB前端面试选择题解答(共36题)

第1题

["1", "2", "3"].map(parseInt) 

A:["1", "2", "3"]

B:[1, 2, 3]

C:[0, 1, 2]

D:other

解释:该题目的答案为:[1, NaN, NaN];即选择D。该题用到了map与parseInt;parseInt() 函数的语法是parseInt(string, radix);
string 必需。要被解析的字符串。
radix可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。
如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。
如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN

实际上 map里面的callback函数接受的是三个参数 分别为元素 下标和数组 (虽然很多情况只使用第一个参数)

回调函数的语法如下所示:
function callbackfn(value, index, array1)
可使用最多三个参数来声明回调函数。

例:
var a=["1", "2", "3", "4","5",6,7,8,9,10,11,12,13,14,15];
a.map(parseInt);
返回结果为:[1, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, 9, 11, 13, 15, 17, 19]

第2题

[typeof null, null instanceof Object]

A: ["object", false]

B: [null, false]

C: ["object", true]

D: other

解释:

考察typeof运算符和instanceof运算符,上MDN上看一下typeof运算符,一些基础类型的结果为:

Undefined "undefined"
Null "object"
Boolean "boolean"
Number "number"
String "string"
Any other object "object"
Array "object"

null instanceof 任何类型 都是false,所以选A。

第3题

[ [3,2,1].reduce(Math.pow), [].reduce(Math.pow)] 

A: an error

B: [9, 0]

C: [9, NaN]

D: [9, undefined]

解答:这题考的Math.powArray.prototype.reduce

Math.pow(base, exponent)接受两个参数:基数、需要计算的次方

reduce传递给其作为参数的函数几个值:

  • previousValue:上一次计算的结果
  • currentValue:当前元素的值
  • index: 当前元素在数组中的位置
  • array:整个数组

reduce本身接受两个参数,callbackinitialValue,分别是reduce的回调函数和计算初始值--也就是第一次reduce的callback被调用时的previousValue的值,默认为0

reduce在数组为空且没有定义initialValue时,会抛出错误,在火狐下报错为:TypeError: reduce of empty array with no initial value
所以选A

第4题

var val = 'smtg';
console.log('Value is ' + (val === 'smtg') ? 'Something' : 'Nothing');

A: Value is Something

B: Value is Nothing

C: NaN

D: other

解答:这题考的javascript中的运算符优先级,这里'+'运算符的优先级要高于'?'所以运算符,实际上是 'Value is true'?'Something' : 'Nothing',当字符串不为空时,转换为bool为true,所以结果为'Something',选D

第5题

var name = 'World!';
(function () {
    if (typeof name === 'undefined') {
        var name = 'Jack';
        console.log('Goodbye ' + name);
    } else {
        console.log('Hello ' + name);
    }
})(); 

A: Goodbye Jack

B: Hello Jack

C: Hello undefined

D: Hello World

这题考的是javascript作用域中的变量提升,javascript的作用于中使用var定义的变量都会被提升到所有代码的最前面,于是乎这段代码就成了:

var name = 'World!';
(function () {
    var name;//现在还是undefined
    if (typeof name === 'undefined') {
        name = 'Jack';
        console.log('Goodbye ' + name);
    } else {
        console.log('Hello ' + name);
    }
})();

这样就很好理解了,typeof name === 'undefined'的结果为true,所以最后会输出'Goodbye Jack',选A

第6题

var END = Math.pow(2, 53);
var START = END - 100;
var count = 0;
for (var i = START; i <= END; i++) {
    count++;
}
console.log(count);

A: 0

B: 100

C: 101

D: other

解答:这题考查javascript中的数字的概念:首先明确一点,javascript和其他语言不同,仅有一种数字,IEEE 754标准的64位浮点数,能够表示的整数范围是-2^53~2^53(包含边界值),所以Math.pow(2, 53)即为javascript中所能表示的最大整数,在最大整数在继续增大就会出现精度丢失的情况,END + 1 的值其实是等于END的,这也就造成了死循环。

第7题

var ary = [0,1,2];
ary[10] = 10;
ary.filter(function(x) { return x === undefined;});

A: [undefined × 7]

B: [0, 1, 2, 10]

C: []

D: [undefined]

解答:选择C Array.prototype.filter is not invoked for the missing elements(缺少的元素,不会调用过滤器)。

第8题

var two   = 0.2
var one   = 0.1
var eight = 0.8
var six   = 0.6
[two - one == one, eight - six == two]

A: [true, true]

B: [false, false]

C: [true, false]

D: other

解答:浮点数计算时的精度丢失问题。.chrome中计算出来的结果:[0.1, 0.20000000000000007],也就是[true, false],最终答案选C。

第9题

function showCase(value) {
    switch(value) {
    case 'A':
        console.log('Case A');
        break;
    case 'B':
        console.log('Case B');
        break;
    case undefined:
        console.log('undefined');
        break;
    default:
        console.log('Do not know!');
    }
}
showCase(new String('A'));

A: Case A

B: Case B

C: Do not know!

D: undefined

解答:此题考察的是关于new string();其实就是new一个实例对象。要匹配的也是object;所以答案是Do not know!,选择C。

第10题

function showCase2(value) {
    switch(value) {
    case 'A':
        console.log('Case A');
        break;
    case 'B':
        console.log('Case B');
        break;
    case undefined:
        console.log('undefined');
        break;
    default:
        console.log('Do not know!');
    }
}
showCase(String('A'));

A: Case A

B: Case B

C: Do not know!

D: undefined

和上题原理一样,不过这里没有使用new来生成字符串,所以生成的结果就是原始字符串,相当于showCase('A'),所以结果就是A

第11题

function isOdd(num) {
    return num % 2 == 1;
}
function isEven(num) {
    return num % 2 == 0;
}
function isSane(num) {
    return isEven(num) || isOdd(num);
}
var values = [7, 4, '13', -9, Infinity];
values.map(isSane);

A: [true, true, true, true, true]

B: [true, true, true, true, false]

C: [true, true, true, false, false]

D: [true, true, false, false, false]

还是JS的数字相关,不过这次考察的是取模,这题我也是瞎蒙的(果断跪了)。

前两个基本上没什么疑问,必然是true

'13'在进行计算前则会进行隐式类型转换(JS最恶心的部分之一),详细参见$雨$的文章《Javascript类型转换的规则》,这里的规则就是将字符串通过Number()方法转换为数字,所以结果为13 % 2 ,也就是true

而JS中负数取模的结果是负数,这里-9%2的结果实际上是-1,所以为false

Infinity对任意数取模都是NaN,所以是false

综上,结果为[true, true, true, false, false],也就是C

第12题

parseInt(3, 8)
parseInt(3, 2)
parseInt(3, 0)

A: 3, 3, 3

B: 3, 3, NaN

C: 3, NaN, NaN

D: other

还是parseInt的题,考的和第一题类似,第一个值为3没什么好说的。如果出现的数字不符合后面输入的进制,则为NaN,所以第二个值为NaN。而radix为0时的情况第一题下面有介绍,这里也是一样为默认10,所以结果为3,所以答案为3, NaN, 3,选D

第13题

Array.isArray( Array.prototype )

A: true

B: false

C: error

D: other

死知识,MDN传送门,这是MDN官方给的例子...选A

第14题

var a = [0];
if ([0]) { 
  console.log(a == true);
} else { 
  console.log("wut");
}

A: true

B: false

C: "wut"

D: other

同样是一道隐式类型转换的题,不过这次考虑的是'=='运算符,a本身是一个长度为1的数组,而当数组不为空时,其转换成bool值为true。

而==左右的转换,会使用如果一个操作值为布尔值,则在比较之前先将其转换为数值的规则来转换,Number([0]),也就是0,于是变成了0 == true,结果自然是false,所以最终结果为B

第15题

[] == []

A: true

B: false

C: error

D: other

这题考的是数组字面量创建数组的原理和==运算符,首先JS中数组的真实类型是Object这点很明显typeof []的值为"object",而==运算符当左右都是对象时,则会比较其是否指向同一个对象。而每次调用字面量创建,都会创造新的对象,也就是会开辟新的内存区域。所以指针的值自然不一样,结果为 false,选B

第16题

'5' + 3  
'5' - 3 

A: 53, 2

B: 8, 2

C: error

D: other

又是一道隐式类型转换的题

加法: 加法运算中,如果有一个操作值为字符串类型,则将另一个操作值转换为字符串,最后连接起来

减法: 如果操作值之一不是数值,则被隐式调用Number()函数进行转换

所以第一行结果为字符串运算,为'53'。第二行结果为2,选A

第17题

1 + - + + + - + 1 

A: 2

B: 1

C: error

D: other

C语言中的经典...对于这种问题,原理什么的不懂,蒙吧,结果是2。选A

第18题

var ary = Array(3);
ary[0]=2
ary.map(function(elem) { return '1'; }); 

A: [2, 1, 1]

B: ["1", "1", "1"]

C: [2, "1", "1"]

D: other

又是考的Array.prototype.map的用法,map在使用的时候,只有数组中被初始化过元素才会被触发,其他都是undefined,所以结果为["1", undefined × 2],选D

第19题

function sidEffecting(ary) { 
  ary[0] = ary[2];
}
function bar(a,b,c) { 
  c = 10
  sidEffecting(arguments);
  return a + b + c;
}
bar(1,1,1)

A: 3

B: 12

C: error

D: other

这题考的是JS的函数arguments的概念:

在调用函数时,函数内部的arguments维护着传递到这个函数的参数列表。它看起来是一个数组,但实际上它只是一个有length属性的Object,不从Array.prototype继承。所以无法使用一些Array.prototype的方法。

arguments对象其内部属性以及函数形参创建getter和setter方法,因此改变形参的值会影响到arguments对象的值,反过来也是一样

具体例子可以参见Javascript秘密花园#arguments

所以,这里所有的更改都将生效,a和c的值都为10,a+b+c的值将为21,选D

第20题

var a = 111111111111111110000,
    b = 1111;
a + b;

A: 111111111111111111111

B: 111111111111111110000

C: NaN

D: Infinity

又是一道考查JavaScript数字的题,与第七题考察点相似。由于JavaScript实际上只有一种数字形式IEEE 754标准的64位双精度浮点数,其所能表示的整数范围为-2^53~2^53(包括边界值)。这里的111111111111111110000已经超过了2^53次方,所以会发生精度丢失的情况。综上选B

第21题

var x = [].reverse;
x();

A: []

B: undefined

C: error

D: window

这题考查的是函数调用时的thisArray.prototype.reverse方法。
首先看Array.prototype.reverse方法,首先举几个栗子:

console.log(Array.prototype.reverse.call("skyinlayer"));
//skyinlayer
console.log(Array.prototype.reverse.call({}));
//Object {}
console.log(Array.prototype.reverse.call(123));
//123

这几个栗子可以得出一个结论,Array.prototype.reverse方法的返回值,就是this

Javascript中this有如下几种情况:

全局下this,指向window对象

console.log(this);
//输出结果:
//Window {top: Window, window: Window, location: Location, external: Object, chrome: Object…}

函数调用,this指向全局window对象:

function somefun(){
    console.log(this);
}
somefun();
//输出结果:
//Window {top: Window, window: Window, location: Location, external: Object, chrome: Object…}

方法调用,this指向拥有该方法的对象:

var someobj = {};
someobj.fun = function(){
    console.log(this);
};
console.log(someobj.fun());
//输出结果:
//Object {fun: function}

调用构造函数,构造函数内部的this指向新创建对象:

function Con() {
    console.log(this);
}
Con.prototype.somefun = function(){};
console.log(new Con());
//输出结果:
//Con {somefun: function}

显示确定this:

function somefun(){
    console.log(this);
};
somefun.apply("skyinlayer");
somefun.call("skyinlayer");
//输出结果:
//String {0: "s", 1: "k", 2: "y", 3: "i", 4: "n", 5: "l", 6: "a", 7: "y", 8: "e", 9: "r", length: 10}
//String {0: "s", 1: "k", 2: "y", 3: "i", 4: "n", 5: "l", 6: "a", 7: "y", 8: "e", 9: "r", length: 10} 

这里报错:Uncaught TypeError: Array.prototype.reverse called on null or undefined,选C。

第22题

Number.MIN_VALUE > 0

A: false

B: true

C: error

D: other

考查的Number.MIN_VALUE的概念,MDN传送门,关键的几句话

  • The Number.MIN_VALUE property represents the smallest positive numeric value representable in JavaScript.

翻译:Number.MIN_VALUE表示的是JavaScript中最小的正数

  • The MIN_VALUE property is the number closest to 0, not the most negative number, that JavaScript can represent.

翻译:MIN_VALUE是接近0的数,而不是最小的数

  • MIN_VALUE has a value of approximately 5e-324. Values smaller than MIN_VALUE ("underflow values") are converted to 0.

翻译:MIN_VALUE值约等于5e-324,比起更小的值(大于0),将被转换为0

所以,这里是true,选B

顺带把Number的几个常量拉出来:

    • Number.MAX_VALUE:最大的正数
    • Number.MIN_VALUE:最小的正数
    • Number.NaN:特殊值,用来表示这不是一个数
    • Number.NEGATIVE_INFINITY:负无穷大
    • Number.POSITIVE_INFINITY:正无穷大

如果要表示最小的负数和最大的负数,可以使用-Number.MAX_VALUE-Number.MIN_VALUE

第23题

[1 < 2 < 3, 3 < 2 < 1]

A: [true, true]

B: [true, false]

C: error

D: other

运算符的运算顺序和隐式类型转换的题,从MDN上运算符优先级,'<'运算符顺序是从左到右,所以变成了[true < 3, false < 1]

接着进行隐式类型转换,'<'操作符的转换规则(来自$雨$的文章《Javascript类型转换的规则》):

  • 如果两个操作值都是数值,则进行数值比较
  • 如果两个操作值都是字符串,则比较字符串对应的字符编码值
  • 如果只有一个操作值是数值,则将另一个操作值转换为数值,进行数值比较
  • 如果一个操作数是对象,则调用valueOf()方法(如果对象没有valueOf()方法则调用toString()方法),得到的结果按照前面的规则执行比较
  • 如果一个操作值是布尔值,则将其转换为数值,再进行比较

所以,这里首先通过Number()转换为数字然后进行比较,true会转换成1,而false转换成0,就变成了[1 < 3, 0 < 1]

所以结果为A

第24题

// the most classic wtf
2 == [[[2]]]

A: true

B: false

C: undefined

D: other

又是隐式类型转换的题(汗)

题目作者的解释是:
both objects get converted to strings and in both cases the resulting string is "2"

也就是说左右两边都被转换成了字符串,而字符串都是"2"

这里首先需要对==右边的数组进行类型转换,根据以下规则(来自justjavac的文章《「译」JavaScript 的怪癖 1:隐式类型转换》):

  1. 调用 valueOf()。如果结果是原始值(不是一个对象),则将其转换为一个数字。
  2. 否则,调用 toString() 方法。如果结果是原始值,则将其转换为一个数字。
  3. 否则,抛出一个类型错误。

所以右侧被使用toString()方法转换为"2",然后又通过Number("2")转换为数字2进行比较,结果就是true了,选A

第25题

3.toString()
3..toString()
3...toString()

A: "3", error, error

B: "3", "3.0", error

C: error, "3", error

D: other

说实话这题有点常见了,很多人都踩过3.toString()的坑(包括我)...虽然JavaScript会在调用方法时对原始值进行包装,但是这个点是小数点呢、还是方法调用的点呢,于是乎第一个就是error了,因为JavaScript解释器会将其认为是小数点。

而第二个则很好说通了,第一个点解释为小数点,变成了(3.0).toString(),结果就是"3"了

第三个也是,第一个点为小数点,第二个是方法调用的点,但是后面接的不是一个合法的方法名,于是乎就error了

综上,选C

第26题

(function(){
  var x = y = 1;
})();
console.log(y);
console.log(x);

A: 1, 1

B: error, error

C: 1, error

D: other

变量提升和隐式定义全局变量的题,也是一个JavaScript经典的坑...

还是那句话,在作用域内,变量定义和函数定义会先行提升,所以里面就变成了:

(function(){
    var x;
    y = 1;
    x = 1;
})();

这点会问了,为什么不是var x, y;,这就是坑的地方...这里只会定义第一个变量x,而y则会通过不使用var的方式直接使用,于是乎就隐式定义了一个全局变量y

所以,y是全局作用域下,而x则是在函数内部,结果就为1, error,选C

第27题

var a = /123/,
    b = /123/;
a == b
a === b

A: true, true

B: true, false

C: false, false

D: other

首先需要明确JavaScript的正则表达式是什么。JavaScript中的正则表达式依旧是对象,使用typeof运算符就能得出结果:

console.log(typeof /123/);
//输出结果:
//"object"

==运算符左右两边都是对象时,会比较他们是否指向同一个对象,可以理解为C语言中两个指针的值是否一样(指向同一片内存),所以两个结果自然都是false

第28题

var a = [1, 2, 3],
    b = [1, 2, 3],
    c = [1, 2, 4]
a == b
a === b
a > c
a < c

A: false, false, false, true

B: false, false, false, false

C: true, true, false, true

D: other

和上题类似,JavaScript中Array的本质也是对象,所以前两个的结果都是false,

而JavaScript中Array的'>'运算符和'<'运算符的比较方式类似于字符串比较字典序,会从第一个元素开始进行比较,如果一样比较第二个,还一样就比较第三个,如此类推,所以第三个结果为false,第四个为true。

综上所述,结果为false, false, false, true,选A

第29题

var a = {}, b = Object.prototype;
[a.prototype === b, Object.getPrototypeOf(a) === b]

A: [false, true]

B: [true, true]

C: [false, false]

D: other

原型链的题(总会有的),考查的__proto__和prototype的区别。首先要明确对象和构造函数的关系,对象在创建的时候,其__proto__会指向其构造函数的prototype属性

Object实际上是一个构造函数(typeof Object的结果为"function"),使用字面量创建对象和new Object创建对象是一样的,所以a.__proto__也就是Object.prototype,而Object.getPrototypeOf(a)与a.__proto__是一样的,所以第二个结果为true

而实例对象是没有prototype属性的,只有函数才有,所以a.prototype其实是undefined,第一个结果为false

综上,选A

第30题

function f() {}
var a = f.prototype, b = Object.getPrototypeOf(f);
a === b

A: true

B: false

C: null

D: other

还是__proto__和prototype的区别,两者不是一个东西,所以选B

第31题

function foo() { }
var oldName = foo.name;
foo.name = "bar";
[oldName, foo.name]

A: error

B: ["", ""]

C: ["foo", "foo"]

D: ["foo", "bar"]

考察了函数的name属性,使用函数定义方式时,会给function对象本身添加一个name属性,保存了函数的名称,很好理解oldName为"foo"。name属性时只读的,不允许修改,所以foo.name = "bar";之后,foo.name还是"foo",所以结果为["foo", "foo"],选C

PS:name属性不是一个标准属性,不要去使用,除非你想要坑别人

第32题

"1 2 3".replace(/\d/g, parseInt)

A: "1 2 3"

B: "0 1 2"

C: "NaN 2 3"

D: "1 NaN 3"

String.prototype.replace、正则表达式的全局匹配和parseInt(又是parseInt...),可以根据题意看出来题目上漏了一个''

首先需要确定replace会传给parseInt哪些参数。举个栗子:

"1 2 3".replace(/\d/g, function(){
    console.log(arguments);
});
//输出结果:
//["1", 0, "1 2 3"]
//["2", 2, "1 2 3"]
//["3", 4, "1 2 3"] 

一共三个:

  1. match:正则表达式被匹配到的子字符串
  2. offset:被匹配到的子字符串在原字符串中的位置
  3. string:原字符串

这样就很好理解了,又回到之前parseInt的问题了,结果就是parseInt("1", 10), parseInt("2", 2), parseInt("3", 4)所以结果为"1, NaN, 3",选D

第33题

function f() {}
var parent = Object.getPrototypeOf(f);
f.name // ?
parent.name // ?
typeof eval(f.name) // ?
typeof eval(parent.name) //  ?

A: "f", "Empty", "function", "function"

B: "f", undefined, "function", error

C: "f", "Empty", "function", error

D: other

又是Function.name属性的题,和三十二题一样样,f.name值为"f",而eval("f")则会输出f函数,所以结果为"function"

接着看parent,parent实际上就是f.__proto__,需要明确的是JavaScript中的函数也是对象,其也有自己的构造函数Function,所以f.__proto__ === Function.prototype结果是true,而Function.prototype就是一个名为Empty的function

console.log(Function.prototype);
console.log(Function.prototype.name);
//输出结果:
//function Empty() {}
//Empty

所以parent.name的值为Empty

如果想直接在全局作用域下调用Empty,显示未定义...因为Empty并不在全局作用域下

综上所述,结果为C

第34题

var lowerCaseOnly =  /^[a-z]+$/;
[lowerCaseOnly.test(null), lowerCaseOnly.test()]

A: [true, false]

B: error

C: [true, true]

D: [false, true]

正则表达式的test方法会自动将参数转换为字符串,原式就变成了[lowerCaseOnly.test("null"), lowerCaseOnly.test("undefined")],结果都是真,所以选C

第35题

[,,,].join(", ")

A: ", , , "

B: "undefined, undefined, undefined, undefined"

C: ", , "

D: ""

JavaScript中使用字面量创建数组时,如果最末尾有一个逗号',',会背省略,所以实际上这个数组只有三个元素(都是undefined):

console.log([,,,].length);
//输出结果:
//3

而三个元素,使用join方法,只需要添加两次,所以结果为", , ",选C

第36题

var a = {class: "Animal", name: 'Fido'};
a.class

A: "Animal"

B: Object

C: an error

D: other

经典坑中的一个,class是关键字。根据浏览器的不同,结果不同:

chrome的结果: "Animal"

Firefox的结果:"Animal"

Opera的结果:"Animal"

IE 8以上也是: "Animal"

IE 8 及以下: 报错

查看原文

赞 12 收藏 9 评论 0

认证与成就

  • 获得 12 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • SelectPage

    下拉选择框更简洁易用的替换方案;支持远程数据( AJAX )、autocomplete、键盘快速导航操作、分页展示、多选标签、i18n 国际化支持等多功能的选择器插件

注册于 2015-07-24
个人主页被 284 人浏览