乐趣区

关于前端:滴滴前端一面必会面试题附答案

OSI 七层模型

ISO为了更好的使网络应用更为遍及,推出了 OSI 参考模型。

(1)应用层

OSI参考模型中最靠近用户的一层,是为计算机用户提供利用接口,也为用户间接提供各种网络服务。咱们常见应用层的网络服务协定有:HTTPHTTPSFTPPOP3SMTP等。

  • 在客户端与服务器中常常会有数据的申请,这个时候就是会用到 http(hyper text transfer protocol)(超文本传输协定) 或者https. 在后端设计数据接口时,咱们经常应用到这个协定。
  • FTP是文件传输协定,在开发过程中,集体并没有波及到,然而我想,在一些资源网站,比方 百度网盘` 迅雷 ` 应该是基于此协定的。
  • SMTPsimple mail transfer protocol(简略邮件传输协定)。在一个我的项目中,在用户邮箱验证码登录的性能时,应用到了这个协定。

(2)表示层

表示层提供各种用于应用层数据的编码和转换性能, 确保一个零碎的应用层发送的数据能被另一个零碎的应用层辨认。如果必要,该层可提供一种规范示意模式,用于将计算机外部的多种数据格式转换成通信中采纳的规范示意模式。数据压缩和加密也是表示层可提供的转换性能之一。

在我的项目开发中,为了不便数据传输,能够应用 base64 对数据进行编解码。如果按性能来划分,base64应该是工作在表示层。

(3)会话层

会话层就是负责建设、治理和终止表示层实体之间的通信会话。该层的通信由不同设施中的应用程序之间的服务申请和响应组成。

(4)传输层

传输层建设了主机端到端的链接,传输层的作用是为下层协定提供端到端的牢靠和通明的数据传输服务,包含解决差错控制和流量管制等问题。该层向高层屏蔽了上层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户管制和设定的、牢靠的数据通路。咱们通常说的,TCP UDP就是在这一层。端口号既是这里的“端”。

(5)网络层

本层通过 IP 寻址来建设两个节点之间的连贯,为源端的运输层送来的分组,抉择适合的路由和替换节点,正确无误地依照地址传送给目标端的运输层。就是通常说的 IP 层。这一层就是咱们常常说的 IP 协定层。IP协定是 Internet 的根底。咱们能够这样了解,网络层规定了数据包的传输路线,而传输层则规定了数据包的传输方式。

(6)数据链路层

将比特组合成字节, 再将字节组合成帧, 应用链路层地址 (以太网应用 MAC 地址)来拜访介质, 并进行过错检测。
网络层与数据链路层的比照,通过下面的形容,咱们或者能够这样了解,网络层是布局了数据包的传输路线,而数据链路层就是传输路线。不过,在数据链路层上还减少了差错控制的性能。

(7)物理层

理论最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电缆针脚。罕用设施有(各种物理设施)集线器、中继器、调制解调器、网线、双绞线、同轴电缆。这些都是物理层的传输介质。

OSI 七层模型通信特点:对等通信 对等通信,为了使数据分组从源传送到目的地,源端 OSI 模型的每一层都必须与目标端的对等层进行通信,这种通信形式称为对等层通信。在每一层通信过程中,应用本层本人协定进行通信。

px、em、rem 的区别及应用场景

三者的区别:

  • px 是固定的像素,一旦设置了就无奈因为适应页面大小而扭转。
  • em 和 rem 绝对于 px 更具备灵活性,他们是绝对长度单位,其长度不是固定的,更实用于响应式布局。
  • em 是绝对于其父元素来设置字体大小,这样就会存在一个问题,进行任何元素设置,都有可能须要晓得他父元素的大小。而 rem 是绝对于根元素,这样就意味着,只须要在根元素确定一个参考值。

应用场景:

  • 对于只须要适配少部分挪动设施,且分辨率对页面影响不大的,应用 px 即可。
  • 对于须要适配各种挪动设施,应用 rem,例如须要适配 iPhone 和 iPad 等分辨率差异比拟挺大的设施。

数组扁平化

数组扁平化就是将 [1, [2, [3]]] 这种多层的数组拍平成一层 [1, 2, 3]。应用 Array.prototype.flat 能够间接将多层数组拍平成一层:

[1, [2, [3]]].flat(2)  // [1, 2, 3]

当初就是要实现 flat 这种成果。

ES5 实现:递归。

function flatten(arr) {var result = [];
    for (var i = 0, len = arr.length; i < len; i++) {if (Array.isArray(arr[i])) {result = result.concat(flatten(arr[i]))
        } else {result.push(arr[i])
        }
    }
    return result;
}

ES6 实现:

function flatten(arr) {while (arr.some(item => Array.isArray(item))) {arr = [].concat(...arr);
    }
    return arr;
}

什么是执行栈

能够把执行栈认为是一个存储函数调用的 栈构造,遵循先进后出的准则。当开始执行 JS 代码时,依据先进后出的准则,后执行的函数会先弹出栈,能够看到,foo 函数后执行,当执行结束后就从栈中弹出了。

平时在开发中,能够在报错中找到执行栈的痕迹:

function foo() {throw new Error('error')
}
function bar() {foo()
}
bar()

能够看到报错在 foo 函数,foo 函数又是在 bar 函数中调用的。当应用递归时,因为栈可寄存的函数是有 限度 的,一旦寄存了过多的函数且没有失去开释的话,就会呈现爆栈的问题

function bar() {  bar()}bar()

原函数形参定长(此时 fn.length 是个不变的常数)

// 写法 1 - 不保留参数, 递归部分函数
function curry(fn) {let judge = (...args) => {
        // 递归完结条件
        if(args.length === fn.length) return fn(...args);
        return (...arg) => judge(...args, ...arg);
    }
    return judge;
}

// 写法 2 - 保留参数, 递归整体函数
function curry(fn) {
    // 保留参数,除去第一个函数参数
    let presentArgs = [].slice.call(arguments, 1);
    // 返回一个新函数
    return function(){
        // 新函数调用时会持续传参
        let allArgs = [...presentArgs, ...arguments];
        // 递归完结条件
        if(allArgs.length === fn.length) {
            // 如果参数够了,就执行原函数
            return fn(,,,allArgs);
        }
        // 否则持续柯里化
        else return curry(fn, ...allArgs);
    }
}

// 测试
function add(a, b, c, d) {return a + b + c + d;}
console.log(add(1, 2, 3, 4));
let addCurry = curry(add);
// 以下后果都返回 10
console.log(addCurry(1)(2)(3)(4));  
console.log(addCurry(1)(2, 3, 4));
console.log(addCurry(1, 2)(3)(4));
console.log(addCurry(1, 2)(3, 4));
console.log(addCurry(1, 2, 3)(4));
console.log(addCurry(1, 2, 3, 4));

AJAX

const getJSON = function(url) {return new Promise((resolve, reject) => {const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
        xhr.open('GET', url, false);
        xhr.setRequestHeader('Accept', 'application/json');
        xhr.onreadystatechange = function() {if (xhr.readyState !== 4) return;
            if (xhr.status === 200 || xhr.status === 304) {resolve(xhr.responseText);
            } else {reject(new Error(xhr.responseText));
            }
        }
        xhr.send();})
}

实现数组原型办法

forEach

Array.prototype.forEach2 = function(callback, thisArg) {if (this == null) {throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {throw new TypeError(callback + 'is not a function')
    }
    const O = Object(this)  // this 就是以后的数组
    const len = O.length >>> 0  // 前面有解释
    let k = 0
    while (k < len) {if (k in O) {callback.call(thisArg, O[k], k, O);
        }
        k++;
    }
}

O.length >>> 0 是什么操作?就是无符号右移 0 位,那有什么意义嘛?就是为了保障转换后的值为正整数。其实底层做了 2 层转换,第一是非 number 转成 number 类型,第二是将 number 转成 Uint32 类型

map

基于 forEach 的实现可能很容易写出 map 的实现:

- Array.prototype.forEach2 = function(callback, thisArg) {+ Array.prototype.map2 = function(callback, thisArg) {if (this == null) {throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {throw new TypeError(callback + 'is not a function')
    }
    const O = Object(this)
    const len = O.length >>> 0
-   let k = 0
+   let k = 0, res = []
    while (k < len) {if (k in O) {-           callback.call(thisArg, O[k], k, O);
+           res[k] = callback.call(thisArg, O[k], k, O);
        }
        k++;
    }
+   return res
}

filter

同样,基于 forEach 的实现可能很容易写出 filter 的实现:

- Array.prototype.forEach2 = function(callback, thisArg) {+ Array.prototype.filter2 = function(callback, thisArg) {if (this == null) {throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {throw new TypeError(callback + 'is not a function')
    }
    const O = Object(this)
    const len = O.length >>> 0
-   let k = 0
+   let k = 0, res = []
    while (k < len) {if (k in O) {-           callback.call(thisArg, O[k], k, O);
+           if (callback.call(thisArg, O[k], k, O)) {+               res.push(O[k])                
+           }
        }
        k++;
    }
+   return res
}

some

同样,基于 forEach 的实现可能很容易写出 some 的实现:

- Array.prototype.forEach2 = function(callback, thisArg) {+ Array.prototype.some2 = function(callback, thisArg) {if (this == null) {throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {throw new TypeError(callback + 'is not a function')
    }
    const O = Object(this)
    const len = O.length >>> 0
    let k = 0
    while (k < len) {if (k in O) {-           callback.call(thisArg, O[k], k, O);
+           if (callback.call(thisArg, O[k], k, O)) {
+               return true
+           }
        }
        k++;
    }
+   return false
}

reduce

Array.prototype.reduce2 = function(callback, initialValue) {if (this == null) {throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {throw new TypeError(callback + 'is not a function')
    }
    const O = Object(this)
    const len = O.length >>> 0
    let k = 0, acc

    if (arguments.length > 1) {acc = initialValue} else {
        // 没传入初始值的时候,取数组中第一个非 empty 的值为初始值
        while (k < len && !(k in O)) {k++}
        if (k > len) {throw new TypeError( 'Reduce of empty array with no initial value');
        }
        acc = O[k++]
    }
    while (k < len) {if (k in O) {acc = callback(acc, O[k], k, O)
        }
        k++
    }
    return acc
}

代码输入后果

var obj = { 
  name : 'cuggz', 
  fun : function(){console.log(this.name); 
  } 
} 
obj.fun()     // cuggz
new obj.fun() // undefined

应用 new 构造函数时,其 this 指向的是全局环境 window。

如何判断元素是否达到可视区域

以图片显示为例:

  • window.innerHeight 是浏览器可视区的高度;
  • document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的间隔;
  • imgs.offsetTop 是元素顶部间隔文档顶部的高度(包含滚动条的间隔);
  • 内容达到显示区域的:img.offsetTop < window.innerHeight + document.body.scrollTop;

手写题:数组去重

Array.from(new Set([1, 1, 2, 2]))

Unicode、UTF-8、UTF-16、UTF-32 的区别?

(1)Unicode

在说 Unicode 之前须要先理解一下 ASCII 码:ASCII 码(American Standard Code for Information Interchange)称为美国规范信息替换码。

  • 它是基于拉丁字母的一套电脑编码零碎。
  • 它定义了一个用于代表常见字符的字典。
  • 它蕴含了 ”A-Z”(蕴含大小写),数据 ”0-9″ 以及一些常见的符号。
  • 它是专门为英语而设计的,有 128 个编码,对其余语言无能为力

ASCII码能够示意的编码无限,要想示意其余语言的编码,还是要应用 Unicode 来示意,能够说 UnicodeASCII 的超集。

Unicode全称 Unicode Translation Format,又叫做对立码、万国码、繁多码。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了对立并且惟一的二进制编码,以满足跨语言、跨平台进行文本转换、解决的要求。

Unicode的实现形式(也就是编码方式)有很多种,常见的是 UTF-8UTF-16UTF-32USC-2

(2)UTF-8

UTF-8是应用最宽泛的 Unicode 编码方式,它是一种可变长的编码方式,能够是 1—4 个字节不等,它能够齐全兼容 ASCII 码的 128 个字符。

留神: UTF-8 是一种编码方式,Unicode是一个字符汇合。

UTF-8的编码规定:

  • 对于 单字节 的符号,字节的第一位为 0,前面的 7 位为这个字符的 Unicode 编码,因而对于英文字母,它的 Unicode 编码和 ACSII 编码一样。
  • 对于 n 字节 的符号,第一个字节的前 n 位都是 1,第 n + 1 位设为 0,前面字节的前两位一律设为 10,剩下的没有提及的二进制位,全副为这个符号的 Unicode 码。

来看一下具体的 Unicode 编号范畴与对应的 UTF-8 二进制格局:

编码范畴(编号对应的十进制数) 二进制格局
0x00—0x7F(0-127) 0xxxxxxx
0x80—0x7FF(128-2047) 110xxxxx 10xxxxxx
0x800—0xFFFF(2048-65535) 1110xxxx 10xxxxxx 10xxxxxx
0x10000—0x10FFFF(65536 以上) 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

那该如何通过具体的 Unicode 编码,进行具体的 UTF-8 编码呢?步骤如下:

  • 找到该 Unicode 编码的所在的编号范畴,进而找到与之对应的二进制格局
  • Unicode 编码转换为二进制数(去掉最高位的 0)
  • 将二进制数从右往左一次填入二进制格局的 X 中,如果有 X 未填,就设为 0

来看一个理论的例子:
”字的Unicode 编码是:0x9A6C,整数编号是39532(1)首选确定了该字符在第三个范畴内,它的格局是 1110xxxx 10xxxxxx 10xxxxxx(2)39532 对应的二进制数为1001 1010 0110 1100(3)将二进制数填入 X 中,后果是:11101001 10101001 10101100

(3)UTF-16

1. 立体的概念

在理解 UTF-16 之前,先看一下 立体 的概念:Unicode编码中有很多很多的字符,它并不是一次性定义的,而是分区进行定义的,每个区寄存 65536(216)个字符,这称为一个 立体,目前总共有 17 个立体。

最后面的一个立体称为 根本立体 ,它的码点从0 — 216-1,写成 16 进制就是U+0000 — U+FFFF,那剩下的 16 个立体就是 辅助立体,码点范畴是 U+10000—U+10FFFF

2. UTF-16 概念:

UTF-16也是 Unicode 编码集的一种编码模式,把 Unicode 字符集的形象码位映射为 16 位长的整数(即码元)的序列,用于数据存储或传递。Unicode字符的码位须要 1 个或者 2 个 16 位长的码元来示意,因而 UTF-16 也是用变长字节示意的。

3. UTF-16 编码规定:

  • 编号在 U+0000—U+FFFF 的字符(罕用字符集),间接用两个字节示意。
  • 编号在 U+10000—U+10FFFF 之间的字符,须要用四个字节示意。

4. 编码辨认

那么问题来了,当遇到两个字节时,怎么晓得是把它当做一个字符还是和前面的两个字节一起当做一个字符呢?

UTF-16 编码必定也思考到了这个问题,在根本立体内,从 U+D800 — U+DFFF 是一个空段,也就是说这个区间的码点不对应任何的字符,因而这些空段就能够用来映射辅助立体的字符。

辅助立体共有 220 个字符位,因而示意这些字符至多须要 20 个二进制位。UTF-16 将这 20 个二进制位分成两半,前 10 位映射在 U+D800 — U+DBFF,称为 高位 (H),后 10 位映射在 U+DC00 — U+DFFF,称为 低位(L)。这就相当于,将一个辅助立体的字符拆成了两个根本立体的字符来示意。

因而,当遇到两个字节时,发现它的码点在 U+D800 —U+DBFF之间,就能够晓得,它前面的两个字节的码点应该在 U+DC00 — U+DFFF 之间,这四个字节必须放在一起进行解读。

5. 举例说明

以 “𡠀” 字为例,它的 Unicode 码点为 0x21800,该码点超出了根本立体的范畴,因而须要用四个字节来示意,步骤如下:

  • 首先计算超出局部的后果:0x21800 - 0x10000
  • 将下面的计算结果转为 20 位的二进制数,有余 20 位就在后面补 0,后果为:0001000110 0000000000
  • 将失去的两个 10 位二进制数别离对应到两个区间中
  • U+D800 对应的二进制数为 1101100000000000,将 0001000110 填充在它的后 10 个二进制位,失去 1101100001000110,转成 16 进制数为 0xD846。同理,低位为 0xDC00,所以这个字的UTF-16 编码为 0xD846 0xDC00

(4)UTF-32

UTF-32 就是字符所对应编号的整数二进制模式,每个字符占四个字节,这个是间接进行转换的。该编码方式占用的贮存空间较多,所以应用较少。

比方“”字的 Unicode 编号是:U+9A6C,整数编号是39532,间接转化为二进制:1001 1010 0110 1100,这就是它的 UTF-32 编码。

(5)总结

Unicode、UTF-8、UTF-16、UTF-32 有什么区别?

  • Unicode 是编码字符集(字符集),而 UTF-8UTF-16UTF-32 是字符集编码(编码规定);
  • UTF-16 应用变长码元序列的编码方式,相较于定长码元序列的 UTF-32 算法更简单,甚至比同样是变长码元序列的 UTF-8 也更为简单,因为其引入了独特的 代理对 这样的代理机制;
  • UTF-8须要判断每个字节中的结尾标记信息,所以如果某个字节在传送过程中出错了,就会导致前面的字节也会解析出错;而 UTF-16 不会判断结尾标记,即便错也只会错一个字符,所以容错能力教强;
  • 如果字符内容全副英文或英文与其余文字混合,但英文占绝大部分,那么用 UTF-8 就比 UTF-16 节俭了很多空间;而如果字符内容全副是中文这样相似的字符或者混合字符中中文占绝大多数,那么 UTF-16 就占优势了,能够节俭很多空间;

HTML5 有哪些更新

1. 语义化标签

  • header:定义文档的页眉(头部);
  • nav:定义导航链接的局部;
  • footer:定义文档或节的页脚(底部);
  • article:定义文章内容;
  • section:定义文档中的节(section、区段);
  • aside:定义其所处内容之外的内容(侧边);

2. 媒体标签

(1)audio:音频

<audio src=''controls autoplay loop='true'></audio>

属性:

  • controls 控制面板
  • autoplay 自动播放
  • loop=‘true’循环播放

(2)video 视频

<video src=''poster='imgs/aa.jpg' controls></video>

属性:

  • poster:指定视频还没有齐全下载结束,或者用户还没有点击播放前显示的封面。默认显示以后视频文件的第一针画面,当然通过 poster 也能够本人指定。
  • controls 控制面板
  • width
  • height

(3)source 标签
因为浏览器对视频格式反对水平不一样,为了可能兼容不同的浏览器,能够通过 source 来指定视频源。

<video>
     <source src='aa.flv' type='video/flv'></source>
     <source src='aa.mp4' type='video/mp4'></source>
</video>

3. 表单

表单类型:

  • email:可能验证以后输出的邮箱地址是否非法
  • url:验证 URL
  • number:只能输出数字,其余输出不了,而且自带高低增大减小箭头,max 属性能够设置为最大值,min 能够设置为最小值,value 为默认值。
  • search:输入框前面会给提供一个小叉,能够删除输出的内容,更加人性化。
  • range:能够提供给一个范畴,其中能够设置 max 和 min 以及 value,其中 value 属性能够设置为默认值
  • color:提供了一个色彩拾取器
  • time:时分秒
  • data:日期抉择年月日
  • datatime:工夫和日期(目前只有 Safari 反对)
  • datatime-local:日期工夫控件
  • week:周控件
  • month:月控件

表单属性:

  • placeholder:提示信息
  • autofocus:主动获取焦点
  • autocomplete=“on”或者 autocomplete=“off”应用这个属性须要有两个前提:

    • 表单必须提交过
    • 必须有 name 属性。
  • required:要求输入框不能为空,必须有值才可能提交。
  • pattern=” ” 外面写入想要的正则模式,例如手机号 patte=”^(+86)?\d{10}$”
  • multiple:能够抉择多个文件或者多个邮箱
  • form=” form 表单的 ID”

表单事件:

  • oninput 每当 input 里的输入框内容发生变化都会触发此事件。
  • oninvalid 当验证不通过时触发此事件。

4. 进度条、度量器

  • progress 标签:用来示意工作的进度(IE、Safari 不反对),max 用来示意工作的进度,value 示意已实现多少
  • meter 属性:用来显示残余容量或残余库存(IE、Safari 不反对)

    • high/low:规定被视作高 / 低的范畴
    • max/min:规定最大 / 小值
    • value:规定以后度量值

设置规定:min < low < high < max

5.DOM 查问操作

  • document.querySelector()
  • document.querySelectorAll()

它们抉择的对象能够是标签,能够是类(须要加点),能够是 ID(须要加 #)

6. Web 存储

HTML5 提供了两种在客户端存储数据的新办法:

  • localStorage – 没有工夫限度的数据存储
  • sessionStorage – 针对一个 session 的数据存储

7. 其余

  • 拖放:拖放是一种常见的个性,即抓取对象当前拖到另一个地位。设置元素可拖放:
<img draggable="true" />
  • 画布(canvas):canvas 元素应用 JavaScript 在网页上绘制图像。画布是一个矩形区域,能够管制其每一像素。canvas 领有多种绘制门路、矩形、圆形、字符以及增加图像的办法。
<canvas id="myCanvas" width="200" height="100"></canvas>
  • SVG:SVG 指可伸缩矢量图形,用于定义用于网络的基于矢量的图形,应用 XML 格局定义图形,图像在放大或扭转尺寸的状况下其图形品质不会有损失,它是万维网联盟的规范
  • 天文定位:Geolocation(天文定位)用于定位用户的地位。‘

总结:(1)新增语义化标签:nav、header、footer、aside、section、article
(2)音频、视频标签:audio、video
(3)数据存储:localStorage、sessionStorage
(4)canvas(画布)、Geolocation(天文定位)、websocket(通信协议)
(5)input 标签新增属性:placeholder、autocomplete、autofocus、required
(6)history API:go、forward、back、pushstate

移除的元素有:

  • 纯体现的元素:basefont,big,center,font, s,strike,tt,u;
  • 对可用性产生负面影响的元素:frame,frameset,noframes;

JS 闭包,你理解多少?

应该有面试官问过你:

  1. 什么是闭包?
  2. 闭包有哪些理论使用场景?
  3. 闭包是如何产生的?
  4. 闭包产生的变量如何被回收?

这些问题其实都能够被看作是同一个问题,那就是面试官在问你:你对 JS 闭包理解多少?

来总结一下我听到过的答案,尽量齐全还原候选人面试的时候说的原话。

答案 1: 就是一个 function 外面 return 了一个子函数,子函数拜访了里面那个函数的变量。

答案 2: for 循环外面能够用闭包来解决问题。

for(var i = 0; i < 10; i++){setTimeout(()=>console.log(i),0)
}
// 控制台输入 10 遍 10.
for(var i = 0; i < 10; i++){(function(a){setTimeout(()=>console.log(a),0)
    })(i)
}
 // 控制台输入 0 -9

答案 3: 以后作用域产产生了对父作用域的援用。

答案 4: 不晓得。是跟浏览器的垃圾回收机制无关吗?

开杠了。请问,小伙伴的答案和以上的内容有多少类似水平?

其实,拿着这些问题好好想想,你就会发现这些问题都只是为了最终那一个问题。

闭包的底层实现原理

1. JS 执行上下文

咱们都晓得,咱们手写的 js 代码是要通过浏览器 V8 进行预编译后能力真正的被执行。例如变量晋升、函数晋升。举个栗子。

// 栗子:var d = 'abc';
function a(){console.log("函数 a");
};
console.log(a);   // ƒ a(){ console.log("函数 a"); }
a();              // '函数 a'
var a = "变量 a";  
console.log(a);   // '变量 a'
a();              // a is not a function
var c = 123;

// 输入后果及程序:// ƒ a(){ console.log("函数 a"); }
// '函数 a'
// '变量 a'
// a is not a function

// 栗子预编后相当于:function a(){console.log("函数 a");
};
var d;
console.log(a);  // ƒ a(){ console.log("函数 a"); }
a();              // '函数 a'

a = "变量 a";     // 此时变量 a 赋值,函数申明被笼罩

console.log(a); // "变量 a"
a();         // a is not a function

那么问题来了。请问是谁来执行预编译操作的?那这个谁又是在哪里进行预编译的?

是的,你的纳闷没有错。js 代码运行须要一个运行环境,那这个环境就是 执行上下文。是的,js 运行前的预编译也是在这个环境中进行。

js 执行上下文分三种:

  • 全局执行上下文:代码开始执行时首先进入的环境。
  • 函数执行上下文:函数调用时,会开始执行函数中的代码。
  • eval 执行上下文:不倡议应用,可疏忽。

那么,执行上下文的周期,分为两个阶段:

  • 创立阶段

    • 创立词法环境
    • 生成变量对象 (VO),建设 作用域链 作用域链 作用域链(重要的事说三遍)
    • 确认 this 指向,并绑定this
  • 执行阶段。这个阶段进行变量赋值,函数援用及执行代码。

你当初猜猜看,预编译是产生在什么时候?

噢,我遗记说了,其实与编译还有另一个称说:执行期上下文

预编译产生在函数执行之前。预编译四部曲为:

  1. 创立 AO 对象
  2. 找形参和变量申明,将变量和形参作为 AO 属性名,值为undefined
  3. 将实参和形参相对立
  4. 在函数体里找到函数申明,值赋予函数体。最初程序输入变量值的时候,就是从 AO 对象中拿。

所以,预编译真正的后果是:

var AO = {a = function a(){console.log("函数 a");};
    d = 'abc'
}

咱们从新来。

1. 什么叫变量对象?

变量对象是 js 代码在进入执行上下文时,js 引擎在内存中建设的一个对象,用来寄存以后执行环境中的变量。

2. 变量对象 (VO) 的创立过程

变量对象的创立,是在执行上下文创立阶段,顺次通过以下三个过程:

  • 创立 arguments 对象。

    对于函数执行环境,首先查问是否有传入的实参,如果有,则会将参数名是实参值组成的键值对放入 arguments 对象中。否则,将参数名和 undefined 组成的键值对放入 arguments 对象中。

// 举个栗子 
function bar(a, b, c) {console.log(arguments);  // [1, 2]
    console.log(arguments[2]); // undefined
}
bar(1,2)
  • 当遇到同名的函数时,前面的会笼罩后面的。
console.log(a); // function a() {console.log('Is a ?') }
function a() {console.log('Is a');
}
function a() {console.log('Is a ?')
}

/**ps: 在执行第一行代码之前,函数申明曾经创立实现. 前面的对之前的申明进行了笼罩。**/
  • 查看以后环境中的变量申明并赋值为 undefined。当遇到同名的函数申明, 为了防止函数被赋值为 undefined , 会疏忽此申明
console.log(a); // function a() {console.log('Is a ?') }
console.log(b); // undefined
function a() {console.log('Is a');
}
function a() {console.log('Is a ?');
}
var b = 'Is b';
var a = 10086;

/** 这段代码执行一下,你会发现 a 打印后果仍旧是一个函数,而 b 则是 undefined。**/

依据以上三个步骤,对于变量晋升也就晓得是怎么回事了。

3. 变量对象变为流动对象

执行上下文的第二个阶段,称为执行阶段,在此时,会进行变量赋值,函数援用并执行其余代码,此时,变量对象变为流动对象。

咱们还是举下面的例子:

console.log(a); // function a() {console.log('fjdsfs') }
console.log(b); // undefined
function a() {console.log('Is a');
}
function a() {console.log('Is a?');
}
var b = 'Is b';
console.log(b); // 'Is b'
var a = 10086; 
console.log(a);  // 10086
var b = 'Is b?';
console.log(b); // 'Is b?'

在下面的代码中,代码真正开始执行是从第一行 console.log() 开始的,自这之前,执行上下文是这样的:

// 创立过程
EC= {VO:{}; // 创立变量对象
  scopeChain: {}; // 作用域链}
VO = {argument: {...}; // 以后为全局上下文,所以这个属性值是空的
  a: <a reference> // 函数 a  的援用地址  b: undefiend  // 见上文创立变量对象的第三步}

词法作用域(Lexical scope

这里想阐明,咱们在函数执行上下文中有变量,在全局执行上下文中有变量。JavaScript的一个简单之处在于它如何查找变量,如果在函数执行上下文中找不到变量,它将在调用上下文中寻找它,如果在它的调用上下文中没有找到,就始终往上一级,直到它在全局执行上下文中查找为止。(如果最初找不到,它就是 undefined)。

再来举个栗子:

 1: let top = 0; // 
 2: function createWarp() {3:   function add(a, b) {
 4:     let ret = a + b
 5:     return ret
 6:   }
 7:   return add
 8: }
 9: let sum = createWarp()
10: let result = sum(top, 8)
11: console.log('result:',result)


剖析过程如下:

  • 在全局上下文中申明变量top 并赋值为 0.
  • 2 – 8 行。在全局执行上下文中申明了一个名为 createWarp 的变量,并为其调配了一个函数定义。其中第 3 - 7 行形容了其函数定义,并将函数定义存储到那个变量 (createWarp) 中。
  • 第 9 行。咱们在全局执行上下文中申明了一个名为 sum 的新变量,临时,值为 undefined
  • 第 9 行。遇到 (),表明须要执行或调用一个函数。 那么查找全局执行上下文的内存并查找名为 createWarp 的变量。 显著,曾经在步骤 2 中创立结束。接着,调用它。
  • 调用函数时,回到第 2 行。创立一个新的 createWarp 执行上下文。咱们能够在 createWarp 的执行上下文中创立自有变量。js 引擎createWarp 的上下文增加到调用堆栈(call stack)。因为这个函数没有参数,间接跳到它的主体局部.
  • 3 – 6 行。咱们有一个新的函数申明,createWarp 执行上下文中创立一个变量 addadd 只存在于 createWarp 执行上下文中, 其函数定义存储在名为 add 的自有变量中。
  • 第 7 行,咱们返回变量 add 的内容。js 引擎查找一个名为 add 的变量并找到它. 第 4 行和第 5 行括号之间的内容形成该函数定义。
  • createWarp 调用结束,createWarp 执行上下文将被销毁。add 变量也跟着被销毁。add 函数定义依然存在,因为它返回并赋值给了 sum 变量。(ps: 这才是闭包产生的变量存于内存当中的假相
  • 接下来就是简略的执行过程,不再赘述。。
  • ……
  • 代码执行结束,全局执行上下文被销毁。sum 和 result 也跟着被销毁。

小结一下

当初,如果再让你答复什么是闭包,你能答出多少?

其实,大家说的都对。不论是函数返回一个函数,还是产生了内部作用域的援用,都是有情理的。

所以,什么是闭包?

  • 解释一下作用域链是如何产生的。
  • 解释一下 js 执行上下文的创立、执行过程。
  • 解释一下闭包所产生的变量放在哪了。
  • 最初请把以上 3 点联合起来说给面试官听。

display 的属性值及其作用

属性值 作用
none 元素不显示,并且会从文档流中移除。
block 块类型。默认宽度为父元素宽度,可设置宽高,换行显示。
inline 行内元素类型。默认宽度为内容宽度,不可设置宽高,同行显示。
inline-block 默认宽度为内容宽度,能够设置宽高,同行显示。
list-item 像块类型元素一样显示,并增加款式列表标记。
table 此元素会作为块级表格来显示。
inherit 规定应该从父元素继承 display 属性的值。

将虚构 Dom 转化为实在 Dom

题目形容:JSON 格局的虚构 Dom 怎么转换成实在 Dom

{
  tag: 'DIV',
  attrs:{id:'app'},
  children: [
    {
      tag: 'SPAN',
      children: [{ tag: 'A', children: [] }
      ]
    },
    {
      tag: 'SPAN',
      children: [{ tag: 'A', children: [] },
        {tag: 'A', children: [] }
      ]
    }
  ]
}
把上诉虚构 Dom 转化成下方实在 Dom
<div id="app">
  <span>
    <a></a>
  </span>
  <span>
    <a></a>
    <a></a>
  </span>
</div>

实现代码如下:

// 真正的渲染函数
function _render(vnode) {
  // 如果是数字类型转化为字符串
  if (typeof vnode === "number") {vnode = String(vnode);
  }
  // 字符串类型间接就是文本节点
  if (typeof vnode === "string") {return document.createTextNode(vnode);
  }
  // 一般 DOM
  const dom = document.createElement(vnode.tag);
  if (vnode.attrs) {
    // 遍历属性
    Object.keys(vnode.attrs).forEach((key) => {const value = vnode.attrs[key];
      dom.setAttribute(key, value);
    });
  }
  // 子数组进行递归操作
  vnode.children.forEach((child) => dom.appendChild(_render(child)));
  return dom;
}

哪些状况会导致内存透露

1、意外的全局变量:因为应用未声明的变量, 而意外的创立了一个全局变量, 而使这个变量始终留在内存中无奈被回收
2、被忘记的计时器或回调函数:设置了 setInterval 定时器,而遗记勾销它,如果循环函数有对外部变量的援用的话,那么这个变量会被始终留在内存中,而无奈被回收。3、脱离 DOM 的援用:获取一个 DOM 元素的援用,而前面这个元素被删除,因为始终保留了对这个元素的援用,所以它也无奈被回收。4、闭包:不合理的应用闭包,从而导致某些变量始终被留在内存当中。

如何⽤ webpack 来优化前端性能?

⽤ webpack 优化前端性能是指优化 webpack 的输入后果,让打包的最终后果在浏览器运⾏疾速⾼效。

  • 压缩代码:删除多余的代码、正文、简化代码的写法等等⽅式。能够利⽤ webpack 的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩 JS ⽂件,利⽤ cssnano(css-loader?minimize)来压缩 css
  • 利⽤ CDN 减速: 在构建过程中,将引⽤的动态资源门路批改为 CDN 上对应的门路。能够利⽤ webpack 对于 output 参数和各 loader 的 publicPath 参数来批改资源门路
  • Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。能够通过在启动 webpack 时追加参数 –optimize-minimize 来实现
  • Code Splitting: 将代码按路由维度或者组件分块(chunk), 这样做到按需加载, 同时能够充沛利⽤浏览器缓存
  • 提取公共第三⽅库: SplitChunksPlugin 插件来进⾏公共模块抽取, 利⽤浏览器缓存能够⻓期缓存这些⽆需频繁变动的公共代码

== 操作符的强制类型转换规定?

对于 == 来说,如果比照单方的类型 不一样 ,就会进行 类型转换。如果比照 xy 是否雷同,就会进行如下判断流程:

  1. 首先会判断两者类型是否 雷同,雷同的话就比拟两者的大小;
  2. 类型不雷同的话,就会进行类型转换;
  3. 会先判断是否在比照 nullundefined,是的话就会返回 true
  4. 判断两者类型是否为 stringnumber,是的话就会将字符串转换为 number
1 == '1'
      ↓
1 ==  1
  1. 判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断
'1' == true
        ↓
'1' ==  1
        ↓
 1  ==  1
  1. 判断其中一方是否为 object 且另一方为 stringnumber 或者 symbol,是的话就会把 object 转为原始类型再进行判断
'1' == {name: 'js'}        ↓'1' == '[object Object]'

手写题:实现柯里化

事后设置一些参数

柯里化是什么:是指这样一个函数,它接管函数 A,并且能返回一个新的函数,这个新的函数可能处理函数 A 的残余参数

function createCurry(func, args) {
  var argity = func.length;
  var args = args || [];

  return function () {var _args = [].slice.apply(arguments);
    args.push(..._args);

    if (args.length < argity) {return createCurry.call(this, func, args);
    }

    return func.apply(this, args);
  }
}

Vue 的父子组件生命周期钩子函数执行程序?

<!-- 加载渲染过程 -->
    <!-- 父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created ->
    子 beforeMount -> 子 mounted -> 父 mounted -->
    <!-- 子组件更新过程 -->
    <!-- 父 beforeUpdate -> 子 beforeUpdate -> 子 updaed -> 父 updated -->
    <!-- 父组件跟新过程 -->
    <!-- 父 beforeUpdate -> 父 updated -->
    <!-- 销毁过程 -->
    <!-- 父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed -->

await 到底在等啥?

await 在期待什么呢? 一般来说,都认为 await 是在期待一个 async 函数实现。不过按语法阐明,await 期待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有非凡限定)。

因为 async 函数返回一个 Promise 对象,所以 await 能够用于期待一个 async 函数的返回值——这也能够说是 await 在等 async 函数,但要分明,它等的理论是一个返回值。留神到 await 不仅仅用于等 Promise 对象,它能够等任意表达式的后果,所以,await 前面理论是能够接一般函数调用或者间接量的。所以上面这个示例齐全能够正确运行:

function getSomething() {return "something";}
async function testAsync() {return Promise.resolve("hello async");
}
async function test() {const v1 = await getSomething();
    const v2 = await testAsync();
    console.log(v1, v2);
}
test();

await 表达式的运算后果取决于它等的是什么。

  • 如果它等到的不是一个 Promise 对象,那 await 表达式的运算后果就是它等到的货色。
  • 如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞前面的代码,等着 Promise 对象 resolve,而后失去 resolve 的值,作为 await 表达式的运算后果。

来看一个例子:

function testAsy(x){return new Promise(resolve=>{setTimeout(() => {resolve(x);
     }, 3000)
    }
   )
}
async function testAwt(){let result =  await testAsy('hello world');
  console.log(result);    // 3 秒钟之后呈现 hello world
  console.log('cuger')   // 3 秒钟之后呈现 cug
}
testAwt();
console.log('cug')  // 立刻输入 cug

这就是 await 必须用在 async 函数中的起因。async 函数调用不会造成阻塞,它外部所有的阻塞都被封装在一个 Promise 对象中异步执行。await 暂停以后 async 的执行,所以 ’cug” 最先输入,hello world’ 和‘cuger’是 3 秒钟后同时呈现的。

退出移动版