前端经典面试题(60道前端面试题包含JS、CSS、React、网络、浏览器、程序题等)

(如下全部答案仅供参考)javascript

简答题

一、什么是防抖和节流?有什么区别?如何实现?

参考答案php

防抖css

触发高频事件后n秒内函数只会执行一次,若是n秒内高频事件再次被触发,则从新计算时间
  • 思路:
每次触发事件时都取消以前的延时调用方法
function debounce(fn) {
      let timeout = null; // 建立一个标记用来存放定时器的返回值
      return function () {
        clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉
        timeout = setTimeout(() => { // 而后又建立一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内若是还有字符输入的话,就不会执行 fn 函数
          fn.apply(this, arguments);
        }, 500);
      };
    }
    function sayHi() {
      console.log('防抖成功');
    }

    var inp = document.getElementById('inp');
    inp.addEventListener('input', debounce(sayHi)); // 防抖

节流html

高频事件触发,但在n秒内只会执行一次,因此节流会稀释函数的执行频率
  • 思路:
每次触发事件时都判断当前是否有等待执行的延时函数
function throttle(fn) {
      let canRun = true; // 经过闭包保存一个标记
      return function () {
        if (!canRun) return; // 在函数开头判断标记是否为true,不为true则return
        canRun = false; // 当即设置为false
        setTimeout(() => { // 将外部传入的函数的执行放在setTimeout中
          fn.apply(this, arguments);
          // 最后在setTimeout执行完毕后再把标记设置为true(关键)表示能够执行下一次循环了。当定时器没有执行的时候标记永远是false,在开头被return掉
          canRun = true;
        }, 500);
      };
    }
    function sayHi(e) {
      console.log(e.target.innerWidth, e.target.innerHeight);
    }
    window.addEventListener('resize', throttle(sayHi));

二、 get请求传参长度的误区、get和post请求在缓存方面的区别

误区:咱们常常说get请求参数的大小存在限制,而post请求的参数大小是无限制的。前端

参考答案vue

实际上HTTP 协议从未规定 GET/POST 的请求长度限制是多少。对get请求参数的限制是来源与浏览器或web服务器,浏览器或web服务器限制了url的长度。为了明确这个概念,咱们必须再次强调下面几点:java

  • HTTP 协议 未规定 GET 和POST的长度限制
  • GET的最大长度显示是由于 浏览器和 web服务器限制了 URI的长度
  • 不一样的浏览器和WEB服务器,限制的最大长度不同
  • 要支持IE,则最大长度为2083byte,若只支持Chrome,则最大长度 8182byte

补充补充一个get和post在缓存方面的区别:node

  • get请求相似于查找的过程,用户获取数据,能够不用每次都与数据库链接,因此可使用缓存。
  • post不一样,post作的通常是修改和删除的工做,因此必须与数据库交互,因此不能使用缓存。所以get请求适合于请求缓存。

三、模块化发展历程

可从IIFE、AMD、CMD、CommonJS、UMD、webpack(require.ensure)、ES Module、<script type="module"> 这几个角度考虑。react

参考答案webpack

模块化主要是用来抽离公共代码,隔离做用域,避免变量冲突等。

IIFE: 使用自执行函数来编写模块化,特色:在一个单独的函数做用域中执行代码,避免变量冲突

(function(){
  return {
    data:[]
  }
})()

AMD: 使用requireJS 来编写模块化,特色:依赖必须提早声明好

define('./index.js',function(code){
    // code 就是index.js 返回的内容
})

CMD: 使用seaJS 来编写模块化,特色:支持动态引入依赖文件

define(function(require, exports, module) {  
  var indexCode = require('./index.js');
})

CommonJS: nodejs 中自带的模块化。

var fs = require('fs');

UMD:兼容AMD,CommonJS 模块化语法。

webpack(require.ensure):webpack 2.x 版本中的代码分割。

ES Modules: ES6 引入的模块化,支持import 来引入另外一个 js 。

import a from 'a';

四、npm 模块安装机制,为何输入 npm install 就能够自动安装对应的模块?

参考答案

1. npm 模块安装机制:

  • 发出npm install命令
  • 查询node_modules目录之中是否已经存在指定模块

    • 若存在,再也不从新安装
    • 若不存在

      • npm 向 registry 查询模块压缩包的网址
      • 下载压缩包,存放在根目录下的.npm目录里
      • 解压压缩包到当前项目的node_modules目录

2. npm 实现原理

输入 npm install 命令并敲下回车后,会经历以下几个阶段(以 npm 5.5.1 为例):

  1. 执行工程自身 preinstall

    当前 npm 工程若是定义了 preinstall 钩子此时会被执行。

  2. 肯定首层依赖模块

    首先须要作的是肯定工程中的首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。

    工程自己是整棵依赖树的根节点,每一个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每一个首层依赖模块开始逐步寻找更深层级的节点。

  3. 获取模块

    获取模块是一个递归的过程,分为如下几步:

    • 获取模块信息。在下载一个模块以前,首先要肯定其版本,这是由于 package.json 中每每是 semantic version(semver,语义化版本)。此时若是版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有该模块信息直接拿便可,若是没有则从仓库获取。如 packaeg.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本。
    • 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,若是没有则从仓库下载。
    • 查找该模块依赖,若是有依赖则回到第1步,若是没有则中止。
  4. 模块扁平化(dedupe)

    上一步获取到的是一棵完整的依赖树,其中可能包含大量重复模块。好比 A 模块依赖于 loadsh,B 模块一样依赖于 lodash。在 npm3 之前会严格按照依赖树的结构进行安装,所以会形成模块冗余。

    从 npm3 开始默认加入了一个 dedupe 的过程。它会遍历全部节点,逐个将模块放在根节点下面,也就是 node-modules 的第一层。当发现有重复模块时,则将其丢弃。

    这里须要对重复模块进行一个定义,它指的是模块名相同semver 兼容。每一个 semver 都对应一段版本容许范围,若是两个模块的版本容许范围存在交集,那么就能够获得一个兼容版本,而没必要版本号彻底一致,这可使更多冗余模块在 dedupe 过程当中被去掉。

    好比 node-modules 下 foo 模块依赖 lodash@^1.0.0,bar 模块依赖 lodash@^1.1.0,则 ^1.1.0 为兼容版本。

    而当 foo 依赖 lodash@^2.0.0,bar 依赖 lodash@^1.1.0,则依据 semver 的规则,两者不存在兼容版本。会将一个版本放在 node_modules 中,另外一个仍保留在依赖树里。

    举个例子,假设一个依赖树本来是这样:

    node_modules
    -- foo
    ---- lodash@version1

    -- bar
    ---- lodash@version2

    假设 version1 和 version2 是兼容版本,则通过 dedupe 会成为下面的形式:

    node_modules
    -- foo

    -- bar

    -- lodash(保留的版本为兼容版本)

    假设 version1 和 version2 为非兼容版本,则后面的版本保留在依赖树中:

    node_modules
    -- foo
    -- lodash@version1

    -- bar
    ---- lodash@version2

  5. 安装模块

    这一步将会更新工程中的 node_modules,并执行模块中的生命周期函数(按照 preinstall、install、postinstall 的顺序)。

  6. 执行工程自身生命周期

    当前 npm 工程若是定义了钩子此时会被执行(按照 install、postinstall、prepublish、prepare 的顺序)。

    最后一步是生成或更新版本描述文件,npm install 过程完成。

五、ES5的继承和ES6的继承有什么区别?

参考答案

ES5的继承时经过prototype或构造函数机制来实现。ES5的继承实质上是先建立子类的实例对象,而后再将父类的方法添加到this上(Parent.apply(this))。

ES6的继承机制彻底不一样,实质上是先建立父类的实例对象this(因此必须先调用父类的super()方法),而后再用子类的构造函数修改this

具体的:ES6经过class关键字定义类,里面有构造方法,类之间经过extends关键字实现继承。子类必须在constructor方法中调用super方法,不然新建实例报错。由于子类没有本身的this对象,而是继承了父类的this对象,而后对其进行加工。若是不调用super方法,子类得不到this对象。

ps:super关键字指代父类的实例,即父类的this对象。在子类构造函数中,调用super后,才可以使用this关键字,不然报错。

六、setTimeout、Promise、Async/Await 的区别

参考答案

七、定时器的执行顺序或机制?

参考答案

由于js是单线程的,浏览器遇到setTimeout或者setInterval会先执行完当前的代码块,在此以前会把定时器推入浏览器的待执行事件队列里面,等到浏览器执行完当前代码以后会看一下事件队列里面有没有任务,有的话才执行定时器的代码。因此即便把定时器的时间设置为0仍是会先执行当前的一些代码。

function test(){
    var aa = 0;
    var testSet = setInterval(function(){
        aa++;
        console.log(123);
        if(aa<10){
            clearInterval(testSet);
        }
    },20);
  var testSet1 = setTimeout(function(){
    console.log(321)
  },1000);
  for(var i=0;i<10;i++){
    console.log('test');
  }
}
test()

输出结果:

test //10次
undefined
123
321

八、['1','2','3'].map(parseInt) 输出什么,为何?

参考答案

输出:[1, NaN, NaN]

  • 首先让咱们回顾一下,map函数的第一个参数callback:

var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg])
这个callback一共能够接收三个参数,其中第一个参数表明当前被处理的元素,而第二个参数表明该元素的索引。

  • 而parseInt则是用来解析字符串的,使字符串成为指定基数的整数。
    parseInt(string, radix)
    接收两个参数,第一个表示被处理的值(字符串),第二个表示为解析时的基数。
  • 了解这两个函数后,咱们能够模拟一下运行状况
  1. parseInt('1', 0) //radix为0时,且string参数不以“0x”和“0”开头时,按照10为基数处理。这个时候返回1
  2. parseInt('2', 1) //基数为1(1进制)表示的数中,最大值小于2,因此没法解析,返回NaN
  3. parseInt('3', 2) //基数为2(2进制)表示的数中,最大值小于3,因此没法解析,返回NaN
  • map函数返回的是一个数组,因此最后结果为[1, NaN, NaN]

九、Doctype做用? 严格模式与混杂模式如何区分?它们有何意义?

参考答案

Doctype声明于文档最前面,告诉浏览器以何种方式来渲染页面,这里有两种模式,严格模式和混杂模式。

  • 严格模式的排版和 JS 运做模式是 以该浏览器支持的最高标准运行。
  • 混杂模式,向后兼容,模拟老式浏览器,防止浏览器没法兼容页面。

十、fetch发送2次请求的缘由

参考答案

fetch发送post请求的时候,老是发送2次,第一次状态码是204,第二次才成功?

缘由很简单,由于你用fetch的post请求的时候,致使fetch 第一次发送了一个Options请求,询问服务器是否支持修改的请求头,若是服务器支持,则在第二次中发送真正的请求。

http、浏览器对象

一、HTTPS 握手过程当中,客户端如何验证证书的合法性

参考答案

  • 首先什么是HTTP协议?

    http协议是超文本传输协议,位于tcp/ip四层模型中的应用层;经过请求/响应的方式在客户端和服务器之间进行通讯;可是缺乏安全性,http协议信息传输是经过明文的方式传输,不作任何加密,至关于在网络上裸奔;容易被中间人恶意篡改,这种行为叫作中间人攻击;

  • 加密通讯:

    为了安全性,双方可使用对称加密的方式key进行信息交流,可是这种方式对称加密秘钥也会被拦截,也不够安全,进而仍是存在被中间人攻击风险;
    因而人们又想出来另一种方式,使用非对称加密的方式;使用公钥/私钥加解密;通讯方A发起通讯并携带本身的公钥,接收方B经过公钥来加密对称秘钥;而后发送给发起方A;A经过私钥解密;双发接下来经过对称秘钥来进行加密通讯;可是这种方式仍是会存在一种安全性;中间人虽然不知道发起方A的私钥,可是能够作到偷天换日,将拦截发起方的公钥key;并将本身生成的一对公/私钥的公钥发送给B;接收方B并不知道公钥已经被偷偷换过;按照以前的流程,B经过公钥加密本身生成的对称加密秘钥key2;发送给A;
    此次通讯再次被中间人拦截,尽管后面的通讯,二者仍是用key2通讯,可是中间人已经掌握了Key2;能够进行轻松的加解密;仍是存在被中间人攻击风险;

  • 解决困境:权威的证书颁发机构CA来解决;

    • 制做证书:做为服务端的A,首先把本身的公钥key1发给证书颁发机构,向证书颁发机构进行申请证书;证书颁发机构有一套本身的公私钥,CA经过本身的私钥来加密key1,而且经过服务端网址等信息生成一个证书签名,证书签名一样使用机构的私钥进行加密;制做完成后,机构将证书发给A;
    • 校验证书真伪:当B向服务端A发起请求通讯的时候,A再也不直接返回本身的公钥,而是返回一个证书;

说明:各大浏览器和操做系统已经维护了全部的权威证书机构的名称和公钥。B只须要知道是哪一个权威机构发的证书,使用对应的机构公钥,就能够解密出证书签名;接下来,B使用一样的规则,生成本身的证书签名,若是两个签名是一致的,说明证书是有效的;
签名验证成功后,B就能够再次利用机构的公钥,解密出A的公钥key1;接下来的操做,就是和以前同样的流程了;

  • 中间人是否会拦截发送假证书到B呢?

由于证书的签名是由服务器端网址等信息生成的,而且经过第三方机构的私钥加密中间人没法篡改; 因此最关键的问题是证书签名的真伪;

  • https主要的思想是在http基础上增长了ssl安全层,即以上认证过程;

二、TCP三次握手和四次挥手

参考答案

三次握手之因此是三次是保证client和server均让对方知道本身的接收和发送能力没问题而保证的最小次数。

第一次client => server 只能server判断出client具有发送能力
第二次 server => client client就能够判断出server具有发送和接受能力。此时client还需让server知道本身接收能力没问题因而就有了第三次
第三次 client => server 双方均保证了本身的接收和发送能力没有问题

其中,为了保证后续的握手是为了应答上一个握手,每次握手都会带一个标识 seq,后续的ACK都会对这个seq进行加一来进行确认。

三、img iframe script 来发送跨域请求有什么优缺点?

参考答案

  • iframe

优势:跨域完毕以后DOM操做和互相之间的JavaScript调用都是没有问题的

缺点:1.若结果要以URL参数传递,这就意味着在结果数据量很大的时候须要分割传递,巨烦。2.还有一个是iframe自己带来的,母页面和iframe自己的交互自己就有安全性限制。

  • script

优势:能够直接返回json格式的数据,方便处理

缺点:只接受GET请求方式

  • 图片ping

优势:能够访问任何url,通常用来进行点击追踪,作页面分析经常使用的方法

缺点:不能访问响应文本,只能监听是否响应

四、http和https的区别?

参考答案

http传输的数据都是未加密的,也就是明文的,网景公司设置了SSL协议来对http协议传输的数据进行加密处理,简单来讲https协议是由http和ssl协议构建的可进行加密传输和身份认证的网络协议,比http协议的安全性更高。 主要的区别以下:

  • Https协议须要ca证书,费用较高。
  • http是超文本传输协议,信息是明文传输,https则是具备安全性的ssl加密传输协议。
  • 使用不一样的连接方式,端口也不一样,通常而言,http协议的端口为80,https的端口为443
  • http的链接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

五、什么是Bom?有哪些经常使用的Bom属性?

参考答案

Bom是浏览器对象

location对象

  • location.href-- 返回或设置当前文档的URL
  • location.search -- 返回URL中的查询字符串部分。例如 http://www.dreamdu.com/dreamd... 返回包括(?)后面的内容?id=5&name=dreamdu
  • location.hash -- 返回URL#后面的内容,若是没有#,返回空 location.host -- 返回URL中的域名部分,例如www.dreamdu.com
  • location.hostname -- 返回URL中的主域名部分,例如dreamdu.com
  • location.pathname -- 返回URL的域名后的部分。例如 http://www.dreamdu.com/xhtml/ 返回/xhtml/
  • location.port -- 返回URL中的端口部分。例如 http://www.dreamdu.com:8080/xhtml/ 返回8080
  • location.protocol -- 返回URL中的协议部分。例如 http://www.dreamdu.com:8080/xhtml/ 返回(//)前面的内容http:
  • location.assign -- 设置当前文档的URL
  • location.replace() -- 设置当前文档的URL,而且在history对象的地址列表中移除这个URL location.replace(url);
  • location.reload() -- 重载当前页面

history对象

  • history.go() -- 前进或后退指定的页面数
  • history.go(num); history.back() -- 后退一页
  • history.forward() -- 前进一页

Navigator对象

  • navigator.userAgent -- 返回用户代理头的字符串表示(就是包括浏览器版本信息等的字符串)
  • navigator.cookieEnabled -- 返回浏览器是否支持(启用)cookie

六、Cookie、sessionStorage、localStorage的区别

参考答案

共同点:都是保存在浏览器端,而且是同源的

  • Cookie:cookie数据始终在同源的http请求中携带(即便不须要),即cookie在浏览器和服务器间来回传递。而sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。cookie数据还有路径(path)的概念,能够限制cookie只属于某个路径下,存储的大小很小只有4K左右。 (key:能够在浏览器和服务器端来回传递,存储容量小,只有大约4K左右)
  • sessionStorage:仅在当前浏览器窗口关闭前有效,天然也就不可能持久保持,localStorage:始终有效,窗口或浏览器关闭也一直保存,所以用做持久数据;cookie只在设置的cookie过时时间以前一直有效,即便窗口或浏览器关闭。(key:自己就是一个回话过程,关闭浏览器后消失,session为一个回话,当页面不一样即便是同一页面打开两次,也被视为同一次回话)
  • localStorage:localStorage 在全部同源窗口中都是共享的;cookie也是在全部同源窗口中都是共享的。(key:同源窗口都会共享,而且不会失效,无论窗口或者浏览器关闭与否都会始终生效)

补充说明一下cookie的做用:

  • 保存用户登陆状态。例如将用户id存储于一个cookie内,这样当用户下次访问该页面时就不须要从新登陆了,如今不少论坛和社区都提供这样的功能。 cookie还能够设置过时时间,当超过期间期限后,cookie就会自动消失。所以,系统每每能够提示用户保持登陆状态的时间:常见选项有一个月、三个 月、一年等。
  • 跟踪用户行为。例如一个天气预报网站,可以根据用户选择的地区显示当地的天气状况。若是每次都须要选择所在地是烦琐的,当利用了 cookie后就会显得很人性化了,系统可以记住上一次访问的地区,当下次再打开该页面时,它就会自动显示上次用户所在地区的天气状况。由于一切都是在后 台完成,因此这样的页面就像为某个用户所定制的同样,使用起来很是方便
  • 定制页面。若是网站提供了换肤或更换布局的功能,那么可使用cookie来记录用户的选项,例如:背景色、分辨率等。当用户下次访问时,仍然能够保存上一次访问的界面风格。

七、Cookie如何防范XSS攻击

参考答案

XSS(跨站脚本攻击)是指攻击者在返回的HTML中嵌入javascript脚本,为了减轻这些攻击,须要在HTTP头部配上,set-cookie:

  • httponly-这个属性能够防止XSS,它会禁止javascript脚原本访问cookie。
  • secure - 这个属性告诉浏览器仅在请求为https的时候发送cookie。

结果应该是这样的:Set-Cookie=.....

八、浏览器和 Node 事件循环的区别?

参考答案

其中一个主要的区别在于浏览器的event loop 和nodejs的event loop 在处理异步事件的顺序是不一样的,nodejs中有micro event;其中Promise属于micro event 该异步事件的处理顺序就和浏览器不一样.nodejs V11.0以上 这二者之间的顺序就相同了.

function test () {
   console.log('start')
    setTimeout(() => {
        console.log('children2')
        Promise.resolve().then(() => {console.log('children2-1')})
    }, 0)
    setTimeout(() => {
        console.log('children3')
        Promise.resolve().then(() => {console.log('children3-1')})
    }, 0)
    Promise.resolve().then(() => {console.log('children1')})
    console.log('end') 
}

test()

// 以上代码在node11如下版本的执行结果(先执行全部的宏任务,再执行微任务)
// start
// end
// children1
// children2
// children3
// children2-1
// children3-1

// 以上代码在node11及浏览器的执行结果(顺序执行宏任务和微任务)
// start
// end
// children1
// children2
// children2-1
// children3
// children3-1

九、简述HTTPS中间人攻击

参考答案

https协议由 http + ssl 协议构成,具体的连接过程可参考SSL或TLS握手的概述

中间人攻击过程以下:

  1. 服务器向客户端发送公钥。
  2. 攻击者截获公钥,保留在本身手上。
  3. 而后攻击者本身生成一个【伪造的】公钥,发给客户端。
  4. 客户端收到伪造的公钥后,生成加密hash值发给服务器。
  5. 攻击者得到加密hash值,用本身的私钥解密得到真秘钥。
  6. 同时生成假的加密hash值,发给服务器。
  7. 服务器用私钥解密得到假秘钥。
  8. 服务器用加秘钥加密传输信息

防范方法:

  1. 服务端在发送浏览器的公钥中加入CA证书,浏览器能够验证CA证书的有效性

十、说几条web前端优化策略

参考答案

(1). 减小HTTP请求数

这条策略基本上全部前端人都知道,并且也是最重要最有效的。都说要减小HTTP请求,那请求多了到底会怎么样呢?首先,每一个请求都是有成本的,既包 含时间成本也包含资源成本。一个完整的请求都须要通过DNS寻址、与服务器创建链接、发送数据、等待服务器响应、接收数据这样一个“漫长”而复杂的过程。 时间成本就是用户须要看到或者“感觉”到这个资源是必需要等待这个过程结束的,资源上因为每一个请求都须要携带数据,所以每一个请求都须要占用带宽。

另外,因为浏览器进行并发请求的请求数是有上限的,所以请求数多了之后,浏览器须要分批进行请求,所以会增长用户的等待时间,会给 用户形成站点速度慢这样一个印象,即便可能用户能看到的第一屏的资源都已经请求完了,可是浏览器的进度条会一直存在。减小HTTP请求数的主要途径包括:

(2). 从设计实现层面简化页面

若是你的页面像百度首页同样简单,那么接下来的规则基本上都用不着了。保持页面简洁、减小资源的使用时最直接的。若是不是这样,你的页面须要华丽的皮肤,则继续阅读下面的内容。

(3). 合理设置HTTP缓存

缓存的力量是强大的,恰当的缓存设置能够大大的减小HTTP请求。以有啊首页为例,当浏览器没有缓存的时候访问一共会发出78个请求,共600多K 数据(如图1.1),而当第二次访问即浏览器已缓存以后访问则仅有10个请求,共20多K数据(如图1.2)。(这里须要说明的是,若是直接F5刷新页面 的话效果是不同的,这种状况下请求数仍是同样,不过被缓存资源的请求服务器是304响应,只有Header没有Body,能够节省带宽)

怎样才算合理设置?原则很简单,能缓存越多越好,能缓存越久越好。例如,不多变化的图片资源能够直接经过HTTP Header中的Expires设置一个很长的过时头;变化不频繁而又可能会变的资源可使用Last-Modifed来作请求验证。尽量的让资源可以 在缓存中待得更久。

(4). 资源合并与压缩

若是能够的话,尽量的将外部的脚本、样式进行合并,多个合为一个。另外,CSS、Javascript、Image均可以用相应的工具进行压缩,压缩后每每能省下很多空间。

(5). CSS Sprites

合并CSS图片,减小请求数的又一个好办法。

(6). Inline Images

使用data: URL scheme的方式将图片嵌入到页面或CSS中,若是不考虑资源管理上的问题的话,不失为一个好办法。若是是嵌入页面的话换来的是增大了页面的体积,并且没法利用浏览器缓存。使用在CSS中的图片则更为理想一些。

(7). Lazy Load Images

这条策略实际上并不必定能减小HTTP请求数,可是却能在某些条件下或者页面刚加载时减小HTTP请求数。对于图片而言,在页面刚加载的时候能够只 加载第一屏,当用户继续日后滚屏的时候才加载后续的图片。这样一来,假如用户只对第一屏的内容感兴趣时,那剩余的图片请求就都节省了。有啊首页曾经的作法 是在加载的时候把第一屏以后的图片地址缓存在Textarea标签中,待用户往下滚屏的时候才“惰性”加载。

十一、你了解的浏览器的重绘和回流致使的性能问题

参考答案

重绘(Repaint)和回流(Reflow)

重绘和回流是渲染步骤中的一小节,可是这两个步骤对于性能影响很大。

  • 重绘是当节点须要更改外观而不会影响布局的,好比改变 color就叫称为重绘
  • 回流是布局或者几何属性须要改变就称为回流。

回流一定会发生重绘,重绘不必定会引起回流。回流所需的成本比重绘高的多,改变深层次的节点极可能致使父节点的一系列回流。

因此如下几个动做可能会致使性能问题:

  • 改变 window 大小
  • 改变字体
  • 添加或删除样式
  • 文字改变
  • 定位或者浮动
  • 盒模型

不少人不知道的是,重绘和回流其实和 Event loop 有关。

  1. 当 Event loop 执行完 Microtasks 后,会判断 document 是否须要更新。由于浏览器是 60Hz 的刷新率,每 16ms 才会更新一次。
  2. 而后判断是否有 resize或者 scroll,有的话会去触发事件,因此 resizescroll事件也是至少 16ms 才会触发一次,而且自带节流功能。
  3. 判断是否触发了 media query
  4. 更新动画而且发送事件
  5. 判断是否有全屏操做事件
  6. 执行 requestAnimationFrame回调
  7. 执行 IntersectionObserver回调,该方法用于判断元素是否可见,能够用于懒加载上,可是兼容性很差
  8. 更新界面
  9. 以上就是一帧中可能会作的事情。若是在一帧中有空闲时间,就会去执行 requestIdleCallback回调。

减小重绘和回流

  • 使用 translate 替代 top

    <div class="test"></div>
    <style>
        .test {
            position: absolute;
            top: 10px;
            width: 100px;
            height: 100px;
            background: red;
        }
    </style>
    <script>
        setTimeout(() => {
            // 引发回流
            document.querySelector('.test').style.top = '100px'
        }, 1000)
    </script>
  • 使用 visibility替换 display: none,由于前者只会引发重绘,后者会引起回流(改变了布局)

    把 DOM 离线后修改,好比:先把 DOM 给 display:none(有一次 Reflow),而后你修改100次,而后再把它显示出来

    不要把 DOM 结点的属性值放在一个循环里当成循环里的变量

    for(let i = 0; i < 1000; i++) {
        // 获取 offsetTop 会致使回流,由于须要去获取正确的值
        console.log(document.querySelector('.test').style.offsetTop)
    }
  • 不要使用 table 布局,可能很小的一个小改动会形成整个 table 的从新布局
  • 动画实现的速度的选择,动画速度越快,回流次数越多,也能够选择使用 requestAnimationFrame
  • CSS 选择符从右往左匹配查找,避免 DOM 深度过深
  • 将频繁运行的动画变为图层,图层可以阻止该节点回流影响别的元素。好比对于 video标签,浏览器会自动将该节点变为图层。

react、Vue

一、写 React / Vue 项目时为何要在列表组件中写 key,其做用是什么?

参考答案

vue和react都是采用diff算法来对比新旧虚拟节点,从而更新节点。在vue的diff函数中(建议先了解一下diff算法过程)。
在交叉对比中,当新节点跟旧节点头尾交叉对比没有结果时,会根据新节点的key去对比旧节点数组中的key,从而找到相应旧节点(这里对应的是一个key => index 的map映射)。若是没找到就认为是一个新增节点。而若是没有key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个map映射,另外一种是遍历查找。相比而言。map映射的速度更快。
vue部分源码以下:

// vue项目  src/core/vdom/patch.js  -488行
// 如下是为了阅读性进行格式化后的代码

// oldCh 是一个旧虚拟节点数组
if (isUndef(oldKeyToIdx)) {
  oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
if(isDef(newStartVnode.key)) {
  // map 方式获取
  idxInOld = oldKeyToIdx[newStartVnode.key]
} else {
  // 遍历方式获取
  idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
}

建立map函数

function createKeyToOldIdx (children, beginIdx, endIdx) {
  let i, key
  const map = {}
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key
    if (isDef(key)) map[key] = i
  }
  return map
}

遍历寻找

// sameVnode 是对比新旧节点是否相同的函数
 function findIdxInOld (node, oldCh, start, end) {
    for (let i = start; i < end; i++) {
      const c = oldCh[i]
      
      if (isDef(c) && sameVnode(node, c)) return i
    }
  }

二、React 中 setState 何时是同步的,何时是异步的?

参考答案

在React中,若是是由React引起的事件处理(好比经过onClick引起的事件处理),调用setState不会同步更新this.state,除此以外的setState调用会同步执行this.state。所谓“除此以外”,指的是绕过React经过addEventListener直接添加的事件处理函数,还有经过setTimeout/setInterval产生的异步调用。

缘由:在React的setState函数实现中,会根据一个变量isBatchingUpdates判断是直接更新this.state仍是放到队列中回头再说,而isBatchingUpdates默认是false,也就表示setState会同步更新this.state,可是,有一个函数batchedUpdates,这个函数会把isBatchingUpdates修改成true,而当React在调用事件处理函数以前就会调用这个batchedUpdates,形成的后果,就是由React控制的事件处理过程setState不会同步更新this.state

三、下面输出什么

class Example extends React.Component {
  constructor() {
    super();
    this.state = {
      val: 0
    };
  }
  
  componentDidMount() {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 1 次 log

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 2 次 log

    setTimeout(() => {
      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 3 次 log

      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 4 次 log
    }, 0);
  }

  render() {
    return null;
  }
};
一、第一次和第二次都是在 react 自身生命周期内,触发时 isBatchingUpdates 为 true,因此并不会直接执行更新 state,而是加入了 dirtyComponents,因此打印时获取的都是更新前的状态 0。

二、两次 setState 时,获取到 this.state.val 都是 0,因此执行时都是将 0 设置成 1,在 react 内部会被合并掉,只执行一次。设置完成后 state.val 值为 1。

三、setTimeout 中的代码,触发时 isBatchingUpdates 为 false,因此可以直接进行更新,因此连着输出 2,3。

输出: 0 0 2 3

四、为何虚拟dom会提升性能?

参考答案

虚拟dom至关于在js和真实dom中间加了一个缓存,利用dom diff算法避免了没有必要的dom操做,从而提升性能。

具体实现步骤以下:

用 JavaScript 对象结构表示 DOM 树的结构;而后用这个树构建一个真正的 DOM 树,插到文档当中

当状态变动的时候,从新构造一棵新的对象树。而后用新的树和旧的树进行比较,记录两棵树差别

把2所记录的差别应用到步骤1所构建的真正的DOM树上,视图就更新了。

css

一、分析比较 opacity: 0、visibility: hidden、display: none 优劣和适用场景

参考答案

结构:
display:none: 会让元素彻底从渲染树中消失,渲染的时候不占据任何空间, 不能点击,
visibility: hidden:不会让元素从渲染树消失,渲染元素继续占据空间,只是内容不可见,不能点击
opacity: 0: 不会让元素从渲染树消失,渲染元素继续占据空间,只是内容不可见,能够点击

继承:
display: none:是非继承属性,子孙节点消失因为元素从渲染树消失形成,经过修改子孙节点属性没法显示。
visibility: hidden:是继承属性,子孙节点消失因为继承了hidden,经过设置visibility: visible;可让子孙节点显式。

性能:
displaynone : 修改元素会形成文档回流,读屏器不会读取display: none元素内容,性能消耗较大
visibility:hidden: 修改元素只会形成本元素的重绘,性能消耗较少读屏器读取visibility: hidden元素内容
opacity: 0 : 修改元素会形成重绘,性能消耗较少

联系:它们都能让元素不可见

二、清除浮动的方式有哪些?比较好的是哪种?

参考答案

经常使用的通常为三种.clearfix, clear:both,overflow:hidden;

比较好是 .clearfix,伪元素万金油版本,后二者有局限性.

.clearfix:after {
  visibility: hidden;
  display: block;
  font-size: 0;
  content: " ";
  clear: both;
  height: 0;
}

<!--
为毛没有 zoom ,_height 这些,IE6,7这类须要 csshack 再也不咱们考虑以内了
.clearfix 还有另一种写法,
-->

.clearfix:before, .clearfix:after {
    content:"";
    display:table;
}
.clearfix:after{
    clear:both;
    overflow:hidden;
}
.clearfix{
    zoom:1;
}

<!--
用display:table 是为了不外边距margin重叠致使的margin塌陷,
内部元素默认会成为 table-cell 单元格的形式
-->

clear:both:如果用在同一个容器内相邻元素上,那是贼好的,有时候在容器外就有些问题了, 好比相邻容器的包裹层元素塌陷

overflow:hidden:这种如果用在同个容器内,能够造成 BFC避免浮动形成的元素塌陷

四、css sprite 是什么,有什么优缺点

参考答案

概念:将多个小图片拼接到一个图片中。经过 background-position 和元素尺寸调节须要显示的背景图案。

优势:

  1. 减小 HTTP 请求数,极大地提升页面加载速度
  2. 增长图片信息重复度,提升压缩比,减小图片大小
  3. 更换风格方便,只需在一张或几张图片上修改颜色或样式便可实现

缺点:

  1. 图片合并麻烦
  2. 维护麻烦,修改一个图片可能须要从新布局整个图片,样式

五、link@import的区别

参考答案

  1. link是 HTML 方式, @import是 CSS 方式
  2. link最大限度支持并行下载,@import过多嵌套致使串行下载,出现FOUC
  3. link能够经过rel="alternate stylesheet"指定候选样式
  4. 浏览器对link支持早于@import,可使用@import对老浏览器隐藏样式
  5. @import必须在样式规则以前,能够在 css 文件中引用其余文件
  6. 整体来讲:link 优于@import

六、display: block;display: inline;的区别

参考答案

block元素特色:

1.处于常规流中时,若是width没有设置,会自动填充满父容器 2.能够应用margin/padding 3.在没有设置高度的状况下会扩展高度以包含常规流中的子元素 4.处于常规流中时布局时在先后元素位置之间(独占一个水平空间) 5.忽略vertical-align

inline元素特色

1.水平方向上根据direction依次布局

2.不会在元素先后进行换行

3.受white-space控制

4.margin/padding在竖直方向上无效,水平方向上有效

5.width/height属性对非替换行内元素无效,宽度由元素内容决定

6.非替换行内元素的行框高由line-height肯定,替换行内元素的行框高由height,margin,padding,border决定
7.浮动或绝对定位时会转换为block
8.vertical-align属性生效

七、容器包含若干浮动元素时如何清理浮动

参考答案

  1. 容器元素闭合标签前添加额外元素并设置clear: both
  2. 父元素触发块级格式化上下文(见块级可视化上下文部分)
  3. 设置容器元素伪元素进行清理推荐的清理浮动方法
/**
* 在标准浏览器下使用
* 1 content内容为空格用于修复opera下文档中出现
*   contenteditable属性时在清理浮动元素上下的空白
* 2 使用display使用table而不是block:能够防止容器和
*   子元素top-margin折叠,这样能使清理效果与BFC,IE6/7
*   zoom: 1;一致
**/

.clearfix:before,
.clearfix:after {
    content: " "; /* 1 */
    display: table; /* 2 */
}

.clearfix:after {
    clear: both;
}

/**
* IE 6/7下使用
* 经过触发hasLayout实现包含浮动
**/
.clearfix {
    *zoom: 1;
}

八、PNG,GIF,JPG 的区别及如何选

参考答案

GIF:

  1. 8 位像素,256 色
  2. 无损压缩
  3. 支持简单动画
  4. 支持 boolean 透明
  5. 适合简单动画

JPEG

  1. 颜色限于 256
  2. 有损压缩
  3. 可控制压缩质量
  4. 不支持透明
  5. 适合照片

PNG

  1. 有 PNG8 和 truecolor PNG
  2. PNG8 相似 GIF 颜色上限为 256,文件小,支持 alpha 透明度,无动画
  3. 适合图标、背景、按钮

九、display,float,position 的关系

参考答案

  1. 若是display为 none,那么 position 和 float 都不起做用,这种状况下元素不产生框
  2. 不然,若是 position 值为 absolute 或者 fixed,框就是绝对定位的,float 的计算值为 none,display 根据下面的表格进行调整。
  3. 不然,若是 float 不是 none,框是浮动的,display 根据下表进行调整
  4. 不然,若是元素是根元素,display 根据下表进行调整
  5. 其余状况下 display 的值为指定值 总结起来:绝对定位、浮动、根元素都须要调整display

十、如何水平居中一个元素

参考答案

  • 若是须要居中的元素为常规流中 inline 元素,为父元素设置text-align: center;便可实现
  • 若是须要居中的元素为常规流中 block 元素,1)为元素设置宽度,2)设置左右 margin 为 auto。3)IE6 下需在父元素上设置text-align: center;,再给子元素恢复须要的值
  • <body>
        <div class="content">
        aaaaaa aaaaaa a a a a a a a a
        </div>
    </body>
    
    <style>
        body {
            background: #DDD;
            text-align: center; /* 3 */
        }
        .content {
            width: 500px;      /* 1 */
            text-align: left;  /* 3 */
            margin: 0 auto;    /* 2 */
    
            background: purple;
        }
    </style>
  • 若是须要居中的元素为浮动元素,1)为元素设置宽度,2)position: relative;,3)浮动方向偏移量(left 或者 right)设置为 50%,4)浮动方向上的 margin 设置为元素宽度一半乘以-1

    <body>
        <div class="content">
        aaaaaa aaaaaa a a a a a a a a
        </div>
    </body>
    
    <style>
        body {
            background: #DDD;
        }
        .content {
            width: 500px;         /* 1 */
            float: left;
    
            position: relative;   /* 2 */
            left: 50%;            /* 3 */
            margin-left: -250px;  /* 4 */
    
            background-color: purple;
        }
    </style>
  • 若是须要居中的元素为绝对定位元素,1)为元素设置宽度,2)偏移量设置为 50%,3)偏移方向外边距设置为元素宽度一半乘以-1

    <body>
        <div class="content">
        aaaaaa aaaaaa a a a a a a a a
        </div>
    </body>
    
    <style>
        body {
            background: #DDD;
            position: relative;
        }
        .content {
            width: 800px;
    
            position: absolute;
            left: 50%;
            margin-left: -400px;
    
            background-color: purple;
        }
    </style>
  • 若是须要居中的元素为绝对定位元素,1)为元素设置宽度,2)设置左右偏移量都为 0,3)设置左右外边距都为 auto

    <body>
        <div class="content">
        aaaaaa aaaaaa a a a a a a a a
        </div>
    </body>
    
    <style>
        body {
            background: #DDD;
            position: relative;
        }
        .content {
            width: 800px;
    
            position: absolute;
            margin: 0 auto;
            left: 0;
            right: 0;
    
            background-color: purple;
        }
    </style>

JavaScript

一、JS有几种数据类型,其中基本数据类型有哪些?

参考答案

七种数据类型

  • Boolean
  • Null
  • Undefined
  • Number
  • String
  • Symbol (ECMAScript 6 新定义)
  • Object

(ES6以前)其中5种为基本类型:string,number,boolean,null,undefined,

ES6出来的Symbol也是原始数据类型 ,表示独一无二的值

Object为引用类型(范围挺大),也包括数组、函数,

二、Promise 构造函数是同步执行仍是异步执行,那么 then 方法呢?

参考答案

const promise = new Promise((resolve, reject) => {
  console.log(1)
  resolve()
  console.log(2)
})

promise.then(() => {
  console.log(3)
})

console.log(4)

输出结果是:

1
2
4
3
promise构造函数是同步执行的,then方法是异步执行的
Promise new的时候会当即执行里面的代码 then是微任务 会在本次任务执行完的时候执行 setTimeout是宏任务 会在下次任务执行的时候执行

三、JS的四种设计模式

参考答案

工厂模式

简单的工厂模式能够理解为解决多个类似的问题;

function CreatePerson(name,age,sex) {
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.sex = sex;
    obj.sayName = function(){
        return this.name;
    }
    return obj;
}
var p1 = new CreatePerson("longen",'28','男');
var p2 = new CreatePerson("tugenhua",'27','女');
console.log(p1.name); // longen
console.log(p1.age);  // 28
console.log(p1.sex);  // 男
console.log(p1.sayName()); // longen

console.log(p2.name);  // tugenhua
console.log(p2.age);   // 27
console.log(p2.sex);   // 女
console.log(p2.sayName()); // tugenhua

单例模式

只能被实例化(构造函数给实例添加属性与方法)一次

// 单体模式
var Singleton = function(name){
    this.name = name;
};
Singleton.prototype.getName = function(){
    return this.name;
}
// 获取实例对象
var getInstance = (function() {
    var instance = null;
    return function(name) {
        if(!instance) {//至关于一个一次性阀门,只能实例化一次
            instance = new Singleton(name);
        }
        return instance;
    }
})();
// 测试单体模式的实例,因此a===b
var a = getInstance("aa");
var b = getInstance("bb");

沙箱模式

将一些函数放到自执行函数里面,但要用闭包暴露接口,用变量接收暴露的接口,再调用里面的值,不然没法使用里面的值

let sandboxModel=(function(){
    function sayName(){};
    function sayAge(){};
    return{
        sayName:sayName,
        sayAge:sayAge
    }
})()

发布者订阅模式

就例如如咱们关注了某一个公众号,而后他对应的有新的消息就会给你推送,

//发布者与订阅模式
    var shoeObj = {}; // 定义发布者
    shoeObj.list = []; // 缓存列表 存放订阅者回调函数

    // 增长订阅者
    shoeObj.listen = function(fn) {
        shoeObj.list.push(fn); // 订阅消息添加到缓存列表
    }

    // 发布消息
    shoeObj.trigger = function() {
            for (var i = 0, fn; fn = this.list[i++];) {
                fn.apply(this, arguments);//第一个参数只是改变fn的this,
            }
        }
     // 小红订阅以下消息
    shoeObj.listen(function(color, size) {
        console.log("颜色是:" + color);
        console.log("尺码是:" + size);
    });

    // 小花订阅以下消息
    shoeObj.listen(function(color, size) {
        console.log("再次打印颜色是:" + color);
        console.log("再次打印尺码是:" + size);
    });
    shoeObj.trigger("红色", 40);
    shoeObj.trigger("黑色", 42);

代码实现逻辑是用数组存贮订阅者, 发布者回调函数里面通知的方式是遍历订阅者数组,并将发布者内容传入订阅者数组

四、列举出集中建立实例的方法

参考答案

1.字面量

let obj={'name':'张三'}

2.Object构造函数建立

let Obj=new Object()
Obj.name='张三'

3.使用工厂模式建立对象

function createPerson(name){
 var o = new Object();
 o.name = name;
 };
 return o; 
}
var person1 = createPerson('张三');

4.使用构造函数建立对象

function Person(name){
 this.name = name;
}
var person1 = new Person('张三');

五、简述一下前端事件流

参考答案

HTML中与javascript交互是经过事件驱动来实现的,例如鼠标点击事件onclick、页面的滚动事件onscroll等等,能够向文档或者文档中的元素添加事件侦听器来预订事件。想要知道这些事件是在何时进行调用的,就须要了解一下“事件流”的概念。

什么是事件流:事件流描述的是从页面中接收事件的顺序,DOM2级事件流包括下面几个阶段。

  • 事件捕获阶段
  • 处于目标阶段
  • 事件冒泡阶段

addEventListeneraddEventListener是DOM2 级事件新增的指定事件处理程序的操做,这个方法接收3个参数:要处理的事件名、做为事件处理程序的函数和一个布尔值。最后这个布尔值参数若是是true,表示在捕获阶段调用事件处理程序;若是是false,表示在冒泡阶段调用事件处理程序。

IE只支持事件冒泡

六、Function._proto_(getPrototypeOf)是什么?

参考答案

获取一个对象的原型,在chrome中能够经过__proto__的形式,或者在ES6中能够经过Object.getPrototypeOf的形式。

那么Function.proto是什么么?也就是说Function由什么对象继承而来,咱们来作以下判别。

Function.__proto__==Object.prototype //false
Function.__proto__==Function.prototype//true

咱们发现Function的原型也是Function。

咱们用图能够来明确这个关系:

image-20190914235210887

七、简述一下原型 / 构造函数 / 实例

参考答案

  • 原型(prototype): 一个简单的对象,用于实现对象的 属性继承。能够简单的理解成对象的爹。在 Firefox 和 Chrome 中,每一个JavaScript对象中都包含一个__proto__(非标准)的属性指向它爹(该对象的原型),可obj.__proto__进行访问。
  • 构造函数: 能够经过new新建一个对象的函数。
  • 实例: 经过构造函数和new建立出来的对象,即是实例。 实例经过__proto__指向原型,经过constructor指向构造函数

这里来举个栗子,以Object为例,咱们经常使用的Object即是一个构造函数,所以咱们能够经过它构建实例。

// 实例
const instance = new Object()

则此时, 实例为instance, 构造函数为Object,咱们知道,构造函数拥有一个prototype的属性指向原型,所以原型为:

// 原型
const prototype = Object.prototype

这里咱们能够来看出三者的关系:

实例.__proto__ === 原型

原型.constructor === 构造函数

构造函数.prototype === 原型

// 这条线实际上是是基于原型进行获取的,能够理解成一条基于原型的映射线
// 例如: 
// const o = new Object()
// o.constructor === Object   --> true
// o.__proto__ = null;
// o.constructor === Object   --> false
实例.constructor === 构造函数

八、简述一下JS继承,并举例

参考答案

在 JS 中,继承一般指的即是 原型链继承,也就是经过指定原型,并能够经过原型链继承原型上的属性或者方法。

  • 最优化: 圣杯模式

    var inherit = (function(c,p){
        var F = function(){};
        return function(c,p){
            F.prototype = p.prototype;
            c.prototype = new F();
            c.uber = p.prototype;
            c.prototype.constructor = c;
        }
    })();
  • 使用 ES6 的语法糖 class / extends

九、函数柯里化

参考答案

在函数式编程中,函数是一等公民。那么函数柯里化是怎样的呢?

函数柯里化指的是将可以接收多个参数的函数转化为接收单一参数的函数,而且返回接收余下参数且返回结果的新函数的技术。

函数柯里化的主要做用和特色就是参数复用、提早返回和延迟执行。

在一个函数中,首先填充几个参数,而后再返回一个新的函数的技术,称为函数的柯里化。一般可用于在不侵入函数的前提下,为函数 预置通用参数,供屡次重复调用。

const add = function add(x) {
    return function (y) {
        return x + y
    }
}

const add1 = add(1)

add1(2) === 3
add1(20) === 21

十、说说bind、call、apply 区别?

参考答案

callapply 都是为了解决改变 this 的指向。做用都是相同的,只是传参的方式不一样。

除了第一个参数外,call 能够接收一个参数列表,apply 只接受一个参数数组。

let a = {
    value: 1
}
function getValue(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value)
}
getValue.call(a, 'yck', '24')
getValue.apply(a, ['yck', '24'])

bind和其余两个方法做用也是一致的,只是该方法会返回一个函数。而且咱们能够经过 bind实现柯里化。

(下面是对这三个方法的扩展介绍)

如何实现一个 bind 函数

对于实现如下几个函数,能够从几个方面思考

  • 不传入第一个参数,那么默认为 window
  • 改变了 this 指向,让新的对象能够执行该函数。那么思路是否能够变成给新的对象添加一个函数,而后在执行完之后删除?
Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  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))
  }
}

如何实现一个call函数

Function.prototype.myCall = function (context) {
  var context = context || window
  // 给 context 添加一个属性
  // getValue.call(a, 'yck', '24') => a.fn = getValue
  context.fn = this
  // 将 context 后面的参数取出来
  var args = [...arguments].slice(1)
  // getValue.call(a, 'yck', '24') => a.fn('yck', '24')
  var result = context.fn(...args)
  // 删除 fn
  delete context.fn
  return result
}

如何实现一个apply函数

Function.prototype.myApply = function (context) {
  var context = context || window
  context.fn = this

  var result
  // 须要判断是否存储第二个参数
  // 若是存在,就将第二个参数展开
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }

  delete context.fn
  return result
}

十一、箭头函数的特色

参考答案

function a() {
    return () => {
        return () => {
            console.log(this)
        }
    }
}
console.log(a()()())

箭头函数实际上是没有 this的,这个函数中的 this只取决于他外面的第一个不是箭头函数的函数的 this。在这个例子中,由于调用 a符合前面代码中的第一个状况,因此 thiswindow。而且 this一旦绑定了上下文,就不会被任何代码改变。

程序阅读题

一、下面程序输出的结果是什么?

function sayHi() {
  console.log(name);
  console.log(age);
  var name = "Lydia";
  let age = 21;
}

sayHi();
  • A: Lydiaundefined
  • B: LydiaReferenceError
  • C: ReferenceError21
  • D: undefinedReferenceError

参考答案

在函数中,咱们首先使用var关键字声明了name变量。 这意味着变量在建立阶段会被提高(JavaScript会在建立变量建立阶段为其分配内存空间),默认值为undefined,直到咱们实际执行到使用该变量的行。 咱们尚未为name变量赋值,因此它仍然保持undefined的值。

使用let关键字(和const)声明的变量也会存在变量提高,但与var不一样,初始化没有被提高。 在咱们声明(初始化)它们以前,它们是不可访问的。 这被称为“暂时死区”。 当咱们在声明变量以前尝试访问变量时,JavaScript会抛出一个ReferenceError

关于let的是否存在变量提高,咱们何以用下面的例子来验证:

let name = 'ConardLi'
{
  console.log(name) // Uncaught ReferenceError: name is not defined
  let name = 'code秘密花园'
}

let变量若是不存在变量提高,console.log(name)就会输出ConardLi,结果却抛出了ReferenceError,那么这很好的说明了,let也存在变量提高,可是它存在一个“暂时死区”,在变量未初始化或赋值前不容许访问。

变量的赋值能够分为三个阶段:

  • 建立变量,在内存中开辟空间
  • 初始化变量,将变量初始化为undefined
  • 真正赋值

关于letvarfunction

  • let的「建立」过程被提高了,可是初始化没有提高。
  • var的「建立」和「初始化」都被提高了。
  • function的「建立」「初始化」和「赋值」都被提高了。

二、下面代码输出什么

var a = 10;
(function () {
    console.log(a)
    a = 5
    console.log(window.a)
    var a = 20;
    console.log(a)
})()

依次输出:undefined -> 10 -> 20

在当即执行函数中,var a = 20; 语句定义了一个局部变量 a,因为js的变量声明提高机制,局部变量a的声明会被提高至当即执行函数的函数体最上方,且因为这样的提高并不包括赋值,所以第一条打印语句会打印undefined,最后一条语句会打印20。

因为变量声明提高,a = 5; 这条语句执行时,局部的变量a已经声明,所以它产生的效果是对局部的变量a赋值,此时window.a 依旧是最开始赋值的10,

三、下面的输出结果是什么?

class Chameleon {
  static colorChange(newColor) {
    this.newColor = newColor;
  }

  constructor({ newColor = "green" } = {}) {
    this.newColor = newColor;
  }
}

const freddie = new Chameleon({ newColor: "purple" });
freddie.colorChange("orange");
  • A: orange
  • B: purple
  • C: green
  • D: TypeError

答案: D

colorChange方法是静态的。 静态方法仅在建立它们的构造函数中存在,而且不能传递给任何子级。 因为freddie是一个子级对象,函数不会传递,因此在freddie实例上不存在freddie方法:抛出TypeError

四、下面代码中何时会输出1?

var a = ?;
if(a == 1 && a == 2 && a == 3){
     conso.log(1);
}

参考答案

由于==会进行隐式类型转换 因此咱们重写toString方法就能够了
var a = {
  i: 1,
  toString() {
    return a.i++;
  }
}

if( a == 1 && a == 2 && a == 3 ) {
  console.log(1);
}

五、下面的输出结果是什么?

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)

参考答案

1.使用第一次push,obj对象的push方法设置 obj[2]=1;obj.length+=1
2.使用第二次push,obj对象的push方法设置 obj[3]=2;obj.length+=1
3.使用console.log输出的时候,由于obj具备 length 属性和 splice 方法,故将其做为数组进行打印
4.打印时由于数组未设置下标为 0 1 处的值,故打印为empty,主动 obj[0] 获取为 undefined

六、下面代码输出的结果是什么?

var a = {n: 1};
var b = a;
a.x = a = {n: 2};

console.log(a.x)     
console.log(b.x)

参考答案

undefined
{n:2}

首先,a和b同时引用了{n:2}对象,接着执行到a.x = a = {n:2}语句,尽管赋值是从右到左的没错,可是.的优先级比=要高,因此这里首先执行a.x,至关于为a(或者b)所指向的{n:1}对象新增了一个属性x,即此时对象将变为{n:1;x:undefined}。以后按正常状况,从右到左进行赋值,此时执行a ={n:2}的时候,a的引用改变,指向了新对象{n:2},而b依然指向的是旧对象。以后执行a.x = {n:2}的时候,并不会从新解析一遍a,而是沿用最初解析a.x时候的a,也即旧对象,故此时旧对象的x的值为{n:2},旧对象为 {n:1;x:{n:2}},它被b引用着。
后面输出a.x的时候,又要解析a了,此时的a是指向新对象的a,而这个新对象是没有x属性的,故访问时输出undefined;而访问b.x的时候,将输出旧对象的x的值,即{n:2}。

七、下面代码的输出是什么?

function checkAge(data) {
  if (data === { age: 18 }) {
    console.log("You are an adult!");
  } else if (data == { age: 18 }) {
    console.log("You are still an adult.");
  } else {
    console.log(`Hmm.. You don't have an age I guess`);
  }
}

checkAge({ age: 18 });

参考答案

Hmm.. You don't have an age I guess

在比较相等性,原始类型经过它们的值进行比较,而对象经过它们的引用进行比较。JavaScript检查对象是否具备对内存中相同位置的引用。

咱们做为参数传递的对象和咱们用于检查相等性的对象在内存中位于不一样位置,因此它们的引用是不一样的。

这就是为何{ age: 18 } === { age: 18 }{ age: 18 } == { age: 18 }返回 false的缘由。

八、下面代码的输出是什么?

const obj = { 1: "a", 2: "b", 3: "c" };
const set = new Set([1, 2, 3, 4, 5]);

obj.hasOwnProperty("1");
obj.hasOwnProperty(1);
set.has("1");
set.has(1);

参考答案

true true false true

全部对象键(不包括Symbols)都会被存储为字符串,即便你没有给定字符串类型的键。 这就是为何obj.hasOwnProperty('1')也返回true

上面的说法不适用于Set。 在咱们的Set中没有“1”set.has('1')返回false。 它有数字类型1set.has(1)返回true

九、下面代码的输出是什么?

// example 1
var a={}, b='123', c=123;  
a[b]='b';
a[c]='c';  
console.log(a[b]);

---------------------
// example 2
var a={}, b=Symbol('123'), c=Symbol('123');  
a[b]='b';
a[c]='c';  
console.log(a[b]);

---------------------
// example 3
var a={}, b={key:'123'}, c={key:'456'};  
a[b]='b';
a[c]='c';  
console.log(a[b]);

参考答案

这题考察的是对象的键名的转换。

  • 对象的键名只能是字符串和 Symbol 类型。
  • 其余类型的键名会被转换成字符串类型。
  • 对象转字符串默认会调用 toString 方法。
// example 1
var a={}, b='123', c=123;
a[b]='b';
// c 的键名会被转换成字符串'123',这里会把 b 覆盖掉。
a[c]='c';  
// 输出 c
console.log(a[b]);


// example 2
var a={}, b=Symbol('123'), c=Symbol('123');  
// b 是 Symbol 类型,不须要转换。
a[b]='b';
// c 是 Symbol 类型,不须要转换。任何一个 Symbol 类型的值都是不相等的,因此不会覆盖掉 b。
a[c]='c';
// 输出 b
console.log(a[b]);


// example 3
var a={}, b={key:'123'}, c={key:'456'};  
// b 不是字符串也不是 Symbol 类型,须要转换成字符串。
// 对象类型会调用 toString 方法转换成字符串 [object Object]。
a[b]='b';
// c 不是字符串也不是 Symbol 类型,须要转换成字符串。
// 对象类型会调用 toString 方法转换成字符串 [object Object]。这里会把 b 覆盖掉。
a[c]='c';  
// 输出 c
console.log(a[b]);

十、下面代码的输出是什么?

(() => {
  let x, y;
  try {
    throw new Error();
  } catch (x) {
    (x = 1), (y = 2);
    console.log(x);
  }
  console.log(x);
  console.log(y);
})();

参考答案

1 undefined 2

catch块接收参数x。当咱们传递参数时,这与变量的x不一样。这个变量x是属于catch做用域的。

以后,咱们将这个块级做用域的变量设置为1,并设置变量y的值。 如今,咱们打印块级做用域的变量x,它等于1

catch块以外,x仍然是undefined,而y2。 当咱们想在catch块以外的console.log(x)时,它返回undefined,而y返回2

十一、下面代码的输出结果是什么?

function Foo() {
    Foo.a = function() {
        console.log(1)
    }
    this.a = function() {
        console.log(2)
    }
}
Foo.prototype.a = function() {
    console.log(3)
}
Foo.a = function() {
    console.log(4)
}
Foo.a();
let obj = new Foo();
obj.a();
Foo.a();

参考答案

输出顺序是 4 2 1
function Foo() {
    Foo.a = function() {
        console.log(1)
    }
    this.a = function() {
        console.log(2)
    }
}
// 以上只是 Foo 的构建方法,没有产生实例,此刻也没有执行

Foo.prototype.a = function() {
    console.log(3)
}
// 如今在 Foo 上挂载了原型方法 a ,方法输出值为 3

Foo.a = function() {
    console.log(4)
}
// 如今在 Foo 上挂载了直接方法 a ,输出值为 4

Foo.a();
// 马上执行了 Foo 上的 a 方法,也就是刚刚定义的,因此
// # 输出 4

let obj = new Foo();
/* 这里调用了 Foo 的构建方法。Foo 的构建方法主要作了两件事:
1. 将全局的 Foo 上的直接方法 a 替换为一个输出 1 的方法。
2. 在新对象上挂载直接方法 a ,输出值为 2。
*/

obj.a();
// 由于有直接方法 a ,不须要去访问原型链,因此使用的是构建方法里所定义的 this.a,
// # 输出 2

Foo.a();
// 构建方法里已经替换了全局 Foo 上的 a 方法,因此
// # 输出 1
相关文章
相关标签/搜索