说一下购物车的逻辑?
//vue中购物车逻辑的实现1. 购物车信息用一个数组来存储,数组中保留对象,对象中有id和count属性2. 在vuex中state中增加一个数据 cartList 用来保留这个数组3. 因为商品详情页须要用到退出购物车性能,所以咱们须要提供一个mutation, 用来将购物车信息退出 cartList中4. 退出购物车信息的时候,遵循如下规定: 如果购物车中曾经有了该商品信息,则数量累加,如果没有该商品信息,则新增一个对象5. 在商品详情页,点击退出购物车按钮的时候,调用vuex提供的addToCart这个mutation将以后的商品信息 (id count)传给addTocart this.$store.commit("addToCart", {id: , count:})// js中购物车逻辑的实现1.商品页点击“退出购物车”按钮,触发事件2.事件调用购物车“减少商品”的Js程序(函数、对象办法)3.向Js程序传递传递“商品id”、“商品数量”等数据4.存储“商品id”、“商品数量”到浏览器的localStorage中**展现购物车中的商品******1.关上购物车页面2.从localStorage中取出“商品Id”、“商品数量”等信息。3.调用服务器端“取得商品详情”的接口失去购物车中的商品信息(参数为商品Id)4.将取得的商品信息显示在购物车页面。**实现购物车中商品的购买******1.用户对购物车中的商品实现购买流程,产生购物订单2.革除localStorage中存储的曾经购买的商品信息备注1:购物车中商品存储的数据除了“商品id”、“商品数量”之外,依据产品要求还能够有其余的信息,例如残缺的商品详情(这样就不必掉服务器接口取得详情了)、购物车商品的过期工夫,超过工夫的购物车商品在下次关上网站或者购物车页面时被革除。备注2:购物车商品除了存储在localStorage中,依据产品的需要不同,也能够存储在sessionStorage、cookie、session中,或者间接向服务器接口发动申请存储在服务器上。何种状况应用哪种形式存储、有啥区别请本人剖析。
HTTPS通信(握手)过程
HTTPS的通信过程如下:
- 客户端向服务器发动申请,申请中蕴含应用的协定版本号、生成的一个随机数、以及客户端反对的加密办法。
- 服务器端接管到申请后,确认单方应用的加密办法、并给出服务器的证书、以及一个服务器生成的随机数。
- 客户端确认服务器证书无效后,生成一个新的随机数,并应用数字证书中的公钥,加密这个随机数,而后发给服 务器。并且还会提供一个后面所有内容的 hash 的值,用来供服务器测验。
- 服务器应用本人的私钥,来解密客户端发送过去的随机数。并提供后面所有内容的 hash 值来供客户端测验。
- 客户端和服务器端依据约定的加密办法应用后面的三个随机数,生成对话秘钥,当前的对话过程都应用这个秘钥来加密信息。
作用域
- 作用域: 作用域是定义变量的区域,它有一套拜访变量的规定,这套规定来治理浏览器引擎如何在以后作用域以及嵌套的作用域中依据变量(标识符)进行变量查找
- 作用域链: 作用域链的作用是保障对执行环境有权拜访的所有变量和函数的有序拜访,通过作用域链,咱们能够拜访到外层环境的变量和 函数。
作用域链的实质上是一个指向变量对象的指针列表。变量对象是一个蕴含了执行环境中所有变量和函数的对象。作用域链的前 端始终都是以后执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最初一个对象。
- 当咱们查找一个变量时,如果以后执行环境中没有找到,咱们能够沿着作用域链向后查找
- 作用域链的创立过程跟执行上下文的建设无关....
作用域能够了解为变量的可拜访性,总共分为三种类型,别离为:
- 全局作用域
- 函数作用域
- 块级作用域,ES6 中的
let
、const
就能够产生该作用域
其实看完后面的闭包、this
这部分外部的话,应该根本能理解作用域的一些利用。
一旦咱们将这些作用域嵌套起来,就变成了另外一个重要的知识点「作用域链」,也就是 JS 到底是如何拜访须要的变量或者函数的。
- 首先作用域链是在定义时就被确定下来的,和箭头函数里的 this 一样,后续不会扭转,JS 会一层层往上寻找须要的内容。
- 其实作用域链这个货色咱们在闭包小结中曾经看到过它的实体了:
[[Scopes]]
图中的 [[Scopes]]
是个数组,作用域的一层层往上寻找就等同于遍历 [[Scopes]]
。
1. 全局作用域
全局变量是挂载在 window 对象下的变量,所以在网页中的任何地位你都能够应用并且拜访到这个全局变量
var globalName = 'global';function getName() { console.log(globalName) // global var name = 'inner' console.log(name) // inner} getName();console.log(name); // console.log(globalName); //globalfunction setName(){ vName = 'setName';}setName();console.log(vName); // setName
- 从这段代码中咱们能够看到,globalName 这个变量无论在什么中央都是能够被拜访到的,所以它就是全局变量。而在 getName 函数中作为局部变量的 name 变量是不具备这种能力的
- 当然全局作用域有相应的毛病,咱们定义很多全局变量的时候,会容易引起变量命名的抵触,所以在定义变量的时候应该留神作用域的问题。
2. 函数作用域
函数中定义的变量叫作函数变量,这个时候只能在函数外部能力拜访到它,所以它的作用域也就是函数的外部,称为函数作用域
function getName () { var name = 'inner'; console.log(name); //inner}getName();console.log(name);
除了这个函数外部,其余中央都是不能拜访到它的。同时,当这个函数被执行完之后,这个局部变量也相应会被销毁。所以你会看到在 getName 函数里面的 name 是拜访不到的
3. 块级作用域
ES6 中新增了块级作用域,最间接的体现就是新增的 let 关键词,应用 let 关键词定义的变量只能在块级作用域中被拜访,有“暂时性死区”的特点,也就是说这个变量在定义之前是不能被应用的。
在 JS 编码过程中 if 语句
及 for
语句前面 {...}
这外面所包含的,就是块级作用域
console.log(a) //a is not definedif(true){ let a = '123'; console.log(a); // 123}console.log(a) //a is not defined
从这段代码能够看出,变量 a 是在if 语句{...}
中由let 关键词
进行定义的变量,所以它的作用域是 if 语句括号中的那局部,而在里面进行拜访 a 变量是会报错的,因为这里不是它的作用域。所以在 if 代码块的前后输入 a 这个变量的后果,控制台会显示 a 并没有定义
Iterator迭代器
Iterator
(迭代器)是一种接口,也能够说是一种标准。为各种不同的数据结构提供对立的拜访机制。任何数据结构只有部署Iterator
接口,就能够实现遍历操作(即顺次解决该数据结构的所有成员)。
Iterator语法:
const obj = { [Symbol.iterator]:function(){}}
[Symbol.iterator]
属性名是固定的写法,只有领有了该属性的对象,就可能用迭代器的形式进行遍历。
- 迭代器的遍历办法是首先取得一个迭代器的指针,初始时该指针指向第一条数据之前,接着通过调用 next 办法,扭转指针的指向,让其指向下一条数据
每一次的
next
都会返回一个对象,该对象有两个属性value
代表想要获取的数据done
布尔值,false示意以后指针指向的数据有值,true示意遍历曾经完结
Iterator 的作用有三个:
- 创立一个指针对象,指向以后数据结构的起始地位。也就是说,遍历器对象实质上,就是一个指针对象。
- 第一次调用指针对象的next办法,能够将指针指向数据结构的第一个成员。
- 第二次调用指针对象的next办法,指针就指向数据结构的第二个成员。
- 一直调用指针对象的next办法,直到它指向数据结构的完结地位。
每一次调用next办法,都会返回数据结构的以后成员的信息。具体来说,就是返回一个蕴含value和done两个属性的对象。其中,value属性是以后成员的值,done属性是一个布尔值,示意遍历是否完结。
let arr = [{num:1},2,3]let it = arr[Symbol.iterator]() // 获取数组中的迭代器console.log(it.next()) // { value: Object { num: 1 }, done: false }console.log(it.next()) // { value: 2, done: false }console.log(it.next()) // { value: 3, done: false }console.log(it.next()) // { value: undefined, done: true }
对象没有布局Iterator接口,无奈应用for of
遍历。上面使得对象具备Iterator接口
- 一个数据结构只有有Symbol.iterator属性,就能够认为是“可遍历的”
- 原型部署了Iterator接口的数据结构有三种,具体蕴含四种,别离是数组,相似数组的对象,Set和Map构造
为什么对象(Object)没有部署Iterator接口呢?
- 一是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,须要开发者手动指定。然而遍历遍历器是一种线性解决,对于非线性的数据结构,部署遍历器接口,就等于要部署一种线性转换
- 对对象部署
Iterator
接口并不是很必要,因为Map
补救了它的缺点,又正好有Iteraotr
接口
let obj = { id: '123', name: '张三', age: 18, gender: '男', hobbie: '睡觉'}obj[Symbol.iterator] = function () { let keyArr = Object.keys(obj) let index = 0 return { next() { return index < keyArr.length ? { value: { key: keyArr[index], val: obj[keyArr[index++]] } } : { done: true } } }}for (let key of obj) { console.log(key)}
闭包
闭包其实就是一个能够拜访其余函数外部变量的函数。创立闭包的最常见的形式就是在一个函数内创立另一个函数,创立的函数能够 拜访到以后函数的局部变量。
因为通常状况下,函数外部变量是无奈在内部拜访的(即全局变量和局部变量的区别),因而应用闭包的作用,就具备实现了能在内部拜访某个函数外部变量的性能,让这些外部变量的值始终能够保留在内存中。上面咱们通过代码先来看一个简略的例子
function fun1() { var a = 1; return function(){ console.log(a); };}fun1();var result = fun1();result(); // 1// 联合闭包的概念,咱们把这段代码放到控制台执行一下,就能够发现最初输入的后果是 1(即 a 变量的值)。那么能够很分明地发现,a 变量作为一个 fun1 函数的外部变量,失常状况下作为函数内的局部变量,是无奈被内部拜访到的。然而通过闭包,咱们最初还是能够拿到 a 变量的值
闭包有两个罕用的用处
- 闭包的第一个用处是使咱们在函数内部可能拜访到函数外部的变量。通过应用闭包,咱们能够通过在内部调用闭包函数,从而在内部拜访到函数外部的变量,能够应用这种办法来创立公有变量。
- 函数的另一个用处是使曾经运行完结的函数上下文中的变量对象持续留在内存中,因为闭包函数保留了这个变量对象的援用,所以这个变量对象不会被回收。
其实闭包的实质就是作用域链的一个非凡的利用,只有理解了作用域链的创立过程,就可能了解闭包的实现原理。
let a = 1// fn 是闭包function fn() { console.log(a);}function fn1() { let a = 1 // 这里也是闭包 return () => { console.log(a); }}const fn2 = fn1()fn2()
- 大家都晓得闭包其中一个作用是拜访公有变量,就比方上述代码中的 fn2 拜访到了 fn1 函数中的变量 a。然而此时 fn1 早已销毁,咱们是如何拜访到变量 a 的呢?不是都说原始类型是寄存在栈上的么,为什么此时却没有被销毁掉?
- 接下来笔者会依据浏览器的体现来从新了解对于原始类型寄存地位的说法。
- 先来说下数据寄存的正确规定是:部分、占用空间确定的数据,个别会寄存在栈中,否则就在堆中(也有例外)。 那么接下来咱们能够通过 Chrome 来帮忙咱们验证这个说法说法。
上图中画红框的地位咱们能看到一个外部的对象 [[Scopes]]
,其中寄存着变量 a,该对象是被寄存在堆上的,其中蕴含了闭包、全局对象等等内容,因而咱们能通过闭包拜访到本该销毁的变量。
另外最开始咱们对于闭包的定位是:如果一个函数能拜访内部的变量,那么这个函数它就是一个闭包,因而接下来咱们看看在全局下的体现是怎么样的。
let a = 1var b = 2// fn 是闭包function fn() { console.log(a, b);}
从上图咱们能发现全局下申明的变量,如果是 var 的话就间接被挂到 globe 上,如果是其余关键字申明的话就被挂到 Script 上。尽管这些内容同样还是存在 [[Scopes]]
,然而全局变量应该是寄存在动态区域的,因为全局变量无需进行垃圾回收,等须要回收的时候整个利用都没了。
只有在下图的场景中,原始类型才可能是被存储在栈上。
这里为什么要说可能,是因为 JS 是门动静类型语言,一个变量申明时能够是原始类型,马上又能够赋值为对象类型,而后又回到原始类型。这样频繁的在堆栈上切换存储地位,外部引擎是不是也会有什么优化伎俩,或者罗唆全副都丢堆上?只有 const 申明的原始类型才肯定存在栈上?当然这只是笔者的一个揣测,临时没有深究,读者能够疏忽这段瞎想
因而笔者对于原始类型存储地位的了解为:局部变量才是被存储在栈上,全局变量存在动态区域上,其它都存储在堆上。
当然这个了解是建设的 Chrome 的体现之上的,在不同的浏览器上因为引擎的不同,可能存储的形式还是有所变动的。
闭包产生的起因
咱们在后面介绍了作用域的概念,那么你还须要明确作用域链的基本概念。其实很简略,当拜访一个变量时,代码解释器会首先在以后的作用域查找,如果没找到,就去父级作用域去查找,直到找到该变量或者不存在父级作用域中,这样的链路就是作用域链
须要留神的是,每一个子函数都会拷贝下级的作用域,造成一个作用域的链条。那么咱们还是通过上面的代码来具体阐明一下作用域链
var a = 1;function fun1() { var a = 2 function fun2() { var a = 3; console.log(a);//3 }}
- 从中能够看出,fun1 函数的作用域指向全局作用域(window)和它本人自身;fun2 函数的作用域指向全局作用域 (window)、fun1 和它自身;而作用域是从最底层向上找,直到找到全局作用域 window 为止,如果全局还没有的话就会报错。
- 那么这就很形象地阐明了什么是作用域链,即以后函数个别都会存在下层函数的作用域的援用,那么他们就造成了一条作用域链。
- 由此可见,
闭包产生的实质就是:以后环境中存在指向父级作用域的援用
。那么还是拿上的代码举例。
function fun1() { var a = 2 function fun2() { console.log(a); //2 } return fun2;}var result = fun1();result();
- 从下面这段代码能够看出,这里 result 会拿到父级作用域中的变量,输入 2。因为在以后环境中,含有对 fun2 函数的援用,fun2 函数恰好援用了 window、fun1 和 fun2 的作用域。因而 fun2 函数是能够拜访到 fun1 函数的作用域的变量。
- 那是不是只有返回函数才算是产生了闭包呢?其实也不是,回到闭包的实质,咱们只须要让父级作用域的援用存在即可,因而还能够这么改代码,如下所示
var fun3;function fun1() { var a = 2 fun3 = function() { console.log(a); }}fun1();fun3();
能够看出,其中实现的后果和前一段代码的成果其实是一样的,就是在给 fun3 函数赋值后,fun3 函数就领有了 window、fun1 和 fun3 自身这几个作用域的拜访权限;而后还是从下往上查找,直到找到 fun1 的作用域中存在 a 这个变量;因而输入的后果还是 2,最初产生了闭包,模式变了,实质没有扭转。
因而最初返回的不论是不是函数,也都不能阐明没有产生闭包
闭包的表现形式
- 返回一个函数
在定时器、事件监听、Ajax 申请、Web Workers 或者任何异步中,只有应用了回调函数,实际上就是在应用闭包
。请看上面这段代码,这些都是平时开发中用到的模式
// 定时器setTimeout(function handler(){ console.log('1');},1000);// 事件监听$('#app').click(function(){ console.log('Event Listener');});
- 作为函数参数传递的模式,比方上面的例子。
var a = 1;function foo(){ var a = 2; function baz(){ console.log(a); } bar(baz);}function bar(fn){ // 这就是闭包 fn();}foo(); // 输入2,而不是1
IIFE(立刻执行函数),创立了闭包,保留了全局作用域(window)和以后函数的作用域
,因而能够输入全局的变量,如下所示
var a = 2;(function IIFE(){ console.log(a); // 输入2})();
IIFE 这个函数会略微有些非凡,算是一种自执行匿名函数,这个匿名函数领有独立的作用域。这不仅能够防止了外界拜访此 IIFE 中的变量,而且又不会净化全局作用域,咱们常常能在高级的 JavaScript 编程中看见此类函数。
如何解决循环输入问题?
在互联网大厂的面试中,解决循环输入问题是比拟高频的面试题,个别都会给一段这样的代码让你来解释
for(var i = 1; i <= 5; i ++){ setTimeout(function() { console.log(i) }, 0)}
下面这段代码执行之后,从控制台执行的后果能够看进去,后果输入的是 5 个 6,那么个别面试官都会先问为什么都是 6?我想让你实现输入 1、2、3、4、5 的话怎么办呢?
因而联合本讲所学的常识咱们来思考一下,应该怎么给面试官一个称心的解释。你能够围绕这两点来答复。
setTimeout
为宏工作,因为 JS 中单线程eventLoop 机制
,在主线程同步工作执行完后才去执行宏工作,因而循环完结后 setTimeout 中的回调才顺次执行
- 因为
setTimeout
函数也是一种闭包,往上找它的父级作用域链就是 window
,变量 i 为 window 上的全局变量
,开始执行 setTimeout 之前变量 i 曾经就是 6 了,因而最初输入的间断就都是 6。
那么咱们再来看看如何按程序顺次输入 1、2、3、4、5 呢?
- 利用 IIFE
能够利用 IIFE(立刻执行函数),当每次 for 循环时,把此时的变量 i 传递到定时器中,而后执行,革新之后的代码如下。
for(var i = 1;i <= 5;i++){ (function(j){ setTimeout(function timer(){ console.log(j) }, 0) })(i)}
- 应用 ES6 中的 let
ES6 中新增的 let 定义变量的形式,使得 ES6 之后 JS 产生革命性的变动,让 JS 有了块级作用域,代码的作用域以块级为单位进行执行。通过革新后的代码,能够实现下面想要的后果。
for(let i = 1; i <= 5; i++){ setTimeout(function() { console.log(i); },0)}
- 定时器传入第三个参数
setTimeout 作为常常应用的定时器,它是存在第三个参数的,日常工作中咱们常常应用的个别是前两个,一个是回调函数,另外一个是工夫,而第三个参数用得比拟少。那么联合第三个参数,调整完之后的代码如下。
for(var i=1;i<=5;i++){ setTimeout(function(j) { console.log(j) }, 0, i)}
从中能够看到,第三个参数的传递,能够扭转 setTimeout 的执行逻辑,从而实现咱们想要的后果,这也是一种解决循环输入问题的路径
常见考点
- 闭包能考的很多,概念和口试题都会考。
- 概念题就是考考闭包是什么了。
- 口试题的话根本都会联合上异步,比方最常见的:
for (var i = 0; i < 6; i++) { setTimeout(() => { console.log(i) })}
这道题会问输入什么,有哪几种形式能够失去想要的答案?
DNS 协定是什么
概念: DNS 是域名零碎 (Domain Name System) 的缩写,提供的是一种主机名到 IP 地址的转换服务,就是咱们常说的域名零碎。它是一个由分层的 DNS 服务器组成的分布式数据库,是定义了主机如何查问这个分布式数据库的形式的应用层协定。可能使人更不便的拜访互联网,而不必去记住可能被机器间接读取的IP数串。
作用: 将域名解析为IP地址,客户端向DNS服务器(DNS服务器有本人的IP地址)发送域名查问申请,DNS服务器告知客户机Web服务器的 IP 地址。
Generator
Generator
是ES6
中新增的语法,和Promise
一样,都能够用来异步编程。Generator函数能够说是Iterator接口的具体实现形式。Generator 最大的特点就是能够管制函数的执行。
function*
用来申明一个函数是生成器函数,它比一般的函数申明多了一个*
,*
的地位比拟随便能够挨着function
关键字,也能够挨着函数名yield
产出的意思,这个关键字只能呈现在生成器函数体内,然而生成器中也能够没有yield
关键字,函数遇到yield
的时候会暂停,并把yield
前面的表达式后果抛出去next
作用是将代码的控制权交还给生成器函数
function *foo(x) { let y = 2 * (yield (x + 1)) let z = yield (y / 3) return (x + y + z)}let it = foo(5)console.log(it.next()) // => {value: 6, done: false}console.log(it.next(12)) // => {value: 8, done: false}console.log(it.next(13)) // => {value: 42, done: true}
下面这个示例就是一个Generator函数,咱们来剖析其执行过程:
- 首先 Generator 函数调用时它会返回一个迭代器
- 当执行第一次 next 时,传参会被疏忽,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
- 当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 12,所以第二个 yield 等于 2 12 / 3 = 8
- 当执行第三次 next 时,传入的参数会传递给 z,所以 z = 13, x = 5, y = 24,相加等于 42
yield
理论就是暂缓执行的标示,每执行一次next()
,相当于指针挪动到下一个yield
地位
总结一下 ,Generator
函数是ES6
提供的一种异步编程解决方案。通过yield
标识位和next()
办法调用,实现函数的分段执行
遍历器对象生成函数,最大的特点是能够交出函数的执行权
function
关键字与函数名之间有一个星号;- 函数体外部应用
yield
表达式,定义不同的外部状态; next
指针移向下一个状态
这里你能够说说Generator
的异步编程,以及它的语法糖async
和awiat
,传统的异步编程。ES6
之前,异步编程大抵如下
- 回调函数
- 事件监听
- 公布/订阅
传统异步编程计划之一:协程,多个线程相互合作,实现异步工作。
// 应用 * 示意这是一个 Generator 函数// 外部能够通过 yield 暂停代码// 通过调用 next 复原执行function* test() { let a = 1 + 2; yield 2; yield 3;}let b = test();console.log(b.next()); // > { value: 2, done: false }console.log(b.next()); // > { value: 3, done: false }console.log(b.next()); // > { value: undefined, done: true }
从以上代码能够发现,加上*
的函数执行后领有了next
函数,也就是说函数执行后返回了一个对象。每次调用next
函数能够继续执行被暂停的代码。以下是Generator
函数的简略实现
// cb 也就是编译过的 test 函数function generator(cb) { return (function() { var object = { next: 0, stop: function() {} }; return { next: function() { var ret = cb(object); if (ret === undefined) return { value: undefined, done: true }; return { value: ret, done: false }; } }; })();}// 如果你应用 babel 编译后能够发现 test 函数变成了这样function test() { var a; return generator(function(_context) { while (1) { switch ((_context.prev = _context.next)) { // 能够发现通过 yield 将代码宰割成几块 // 每次执行 next 函数就执行一块代码 // 并且表明下次须要执行哪块代码 case 0: a = 1 + 2; _context.next = 4; return 2; case 4: _context.next = 6; return 3; // 执行结束 case 6: case "end": return _context.stop(); } } });}
DNS 记录和报文
DNS 服务器中以资源记录的模式存储信息,每一个 DNS 响应报文个别蕴含多条资源记录。一条资源记录的具体的格局为
(Name,Value,Type,TTL)
其中 TTL 是资源记录的生存工夫,它定义了资源记录可能被其余的 DNS 服务器缓存多长时间。
罕用的一共有四种 Type 的值,别离是 A、NS、CNAME 和 MX ,不同 Type 的值,对应资源记录代表的意义不同:
- 如果 Type = A,则 Name 是主机名,Value 是主机名对应的 IP 地址。因而一条记录为 A 的资源记录,提供了标 准的主机名到 IP 地址的映射。
- 如果 Type = NS,则 Name 是个域名,Value 是负责该域名的 DNS 服务器的主机名。这个记录次要用于 DNS 链式 查问时,返回下一级须要查问的 DNS 服务器的信息。
- 如果 Type = CNAME,则 Name 为别名,Value 为该主机的标准主机名。该条记录用于向查问的主机返回一个主机名 对应的标准主机名,从而通知查问主机去查问这个主机名的 IP 地址。主机别名次要是为了通过给一些简单的主机名提供 一个便于记忆的简略的别名。
- 如果 Type = MX,则 Name 为一个邮件服务器的别名,Value 为邮件服务器的标准主机名。它的作用和 CNAME 是一 样的,都是为了解决标准主机名不利于记忆的毛病。
apply/call/bind 原理
call、apply
和bind
是挂在Function
对象上的三个办法,调用这三个办法的必须是一个函数。
func.call(thisArg, param1, param2, ...)func.apply(thisArg, [param1,param2,...])func.bind(thisArg, param1, param2, ...)
- 在浏览器里,在全局范畴内this 指向window对象;
- 在函数中,this永远指向最初调用他的那个对象;
- 构造函数中,this指向new进去的那个新的对象;
call、apply、bind
中的this被强绑定在指定的那个对象上;- 箭头函数中this比拟非凡,箭头函数this为父作用域的this,不是调用时的this.要晓得前四种形式,都是调用时确定,也就是动静的,而箭头函数的this指向是动态的,申明的时候就确定了下来;
apply、call、bind
都是js给函数内置的一些API,调用他们能够为函数指定this的执行,同时也能够传参。
let a = { value: 1}function getValue(name, age) { console.log(name) console.log(age) console.log(this.value)}getValue.call(a, 'poe', '24')getValue.apply(a, ['poe', '24'])
bind
和其余两个办法作用也是统一的,只是该办法会返回一个函数。并且咱们能够通过bind
实现柯里化
办法的利用场景
上面几种利用场景,你多加领会就能够发现它们的理念都是“借用”办法的思路。咱们来看看都有哪些。
- 判断数据类型
用 Object.prototype.toString
来判断类型是最合适的,借用它咱们简直能够判断所有类型的数据
function getType(obj){ let type = typeof obj; if (type !== "object") { return type; } return Object.prototype.toString.call(obj).replace(/^$/, '$1');}
- 类数组借用办法
类数组因为不是真正的数组,所有没有数组类型上自带的种种办法,所以咱们就能够利用一些办法去借用数组的办法,比方借用数组的 push
办法,看上面的一段代码。
var arrayLike = { 0: 'java', 1: 'script', length: 2} Array.prototype.push.call(arrayLike, 'jack', 'lily'); console.log(typeof arrayLike); // 'object'console.log(arrayLike);// {0: "java", 1: "script", 2: "jack", 3: "lily", length: 4}
用call
的办法来借用Array 原型链上的 push
办法,能够实现一个类数组的 push
办法,给arrayLike
增加新的元素
- 获取数组的最大 / 最小值
咱们能够用 apply 来实现数组中判断最大 / 最小值,apply
间接传递数组作为调用办法的参数,也能够缩小一步开展数组,能够间接应用Math.max、Math.min
来获取数组的最大值 / 最小值,请看上面这段代码。
let arr = [13, 6, 10, 11, 16];const max = Math.max.apply(Math, arr); const min = Math.min.apply(Math, arr);console.log(max); // 16console.log(min); // 6
实现一个 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, 'pp', '24') => a.fn = getValue context.fn = this // 将 context 前面的参数取出来 var args = [...arguments].slice(1) // getValue.call(a, 'pp', '24') => a.fn('pp', '24') var result = context.fn(...args) // 删除 fn delete context.fn return result}
实现一个 apply 函数
Function.prototype.myApply = function(context = window, ...args) { // this-->func context--> obj args--> 传递过去的参数 // 在context上加一个惟一值不影响context上的属性 let key = Symbol('key') context[key] = this; // context为调用的上下文,this此处为函数,将这个函数作为context的办法 // let args = [...arguments].slice(1) //第一个参数为obj所以删除,伪数组转为数组 let result = context[key](...args); delete context[key]; // 不删除会导致context属性越来越多 return result;}
// 应用function f(a,b){ console.log(a,b) console.log(this.name)}let obj={ name:'张三'}f.myApply(obj,[1,2]) //arguments[1]
Promise
这里你谈promise
的时候,除了将他解决的痛点以及罕用的API
之外,最好进行拓展把eventloop
带进来好好讲一下,microtask
(微工作)、macrotask
(工作) 的执行程序,如果看过promise
源码,最好能够谈一谈 原生Promise
是如何实现的。Promise
的关键点在于callback
的两个参数,一个是resovle
,一个是reject
。还有就是Promise
的链式调用(Promise.then()
,每一个then
都是一个责任人)
Promise
是ES6
新增的语法,解决了回调天堂的问题。- 能够把
Promise
看成一个状态机。初始是pending
状态,能够通过函数resolve
和reject
,将状态转变为resolved
或者rejected
状态,状态一旦扭转就不能再次变动。 then
函数会返回一个Promise
实例,并且该返回值是一个新的实例而不是之前的实例。因为Promise
标准规定除了pending
状态,其余状态是不能够扭转的,如果返回的是一个雷同实例的话,多个then
调用就失去意义了。 对于then
来说,实质上能够把它看成是flatMap
1. Promise 的根本状况
简略来说它就是一个容器,外面保留着某个将来才会完结的事件(通常是异步操作)的后果。从语法上说,Promise 是一个对象,从它能够获取异步操作的音讯
个别 Promise 在执行过程中,必然会处于以下几种状态之一。
- 待定(
pending
):初始状态,既没有被实现,也没有被回绝。 - 已实现(
fulfilled
):操作胜利实现。 - 已回绝(
rejected
):操作失败。
待定状态的Promise
对象执行的话,最初要么会通过一个值实现,要么会通过一个起因被回绝。当其中一种状况产生时,咱们用Promise
的then
办法排列起来的相干处理程序就会被调用。因为最初Promise.prototype.then
和Promise.prototype.catch
办法返回的是一个Promise
, 所以它们能够持续被链式调用
对于 Promise 的状态流转状况,有一点值得注意的是,外部状态扭转之后不可逆,你须要在编程过程中加以留神。文字描述比拟艰涩,咱们间接通过一张图就能很清晰地看出 Promise 外部状态流转的状况
从上图能够看出,咱们最开始创立一个新的 Promise
返回给 p1
,而后开始执行,状态是 pending,当执行 resolve
之后状态就切换为 fulfilled
,执行 reject
之后就变为 rejected
的状态
2. Promise 的静态方法
all 办法
- 语法:
Promise.all(iterable)
- 参数: 一个可迭代对象,如
Array
。 形容: 此办法对于汇总多个
promise
的后果很有用,在 ES6 中能够将多个Promise.all
异步申请并行操作,返回后果个别有上面两种状况。- 当所有后果胜利返回时依照申请程序返回胜利后果。
- 当其中有一个失败办法时,则进入失败办法
- 语法:
- 咱们来看下业务的场景,对于上面这个业务场景页面的加载,将多个申请合并到一起,用 all 来实现可能成果会更好,请看代码片段
// 在一个页面中须要加载获取轮播列表、获取店铺列表、获取分类列表这三个操作,页面须要同时发出请求进行页面渲染,这样用 `Promise.all` 来实现,看起来更清晰、高深莫测。//1.获取轮播数据列表function getBannerList(){ return new Promise((resolve,reject)=>{ setTimeout(function(){ resolve('轮播数据') },300) })}//2.获取店铺列表function getStoreList(){ return new Promise((resolve,reject)=>{ setTimeout(function(){ resolve('店铺数据') },500) })}//3.获取分类列表function getCategoryList(){ return new Promise((resolve,reject)=>{ setTimeout(function(){ resolve('分类数据') },700) })}function initLoad(){ Promise.all([getBannerList(),getStoreList(),getCategoryList()]) .then(res=>{ console.log(res) }).catch(err=>{ console.log(err) })} initLoad()
allSettled
办法Promise.allSettled
的语法及参数跟Promise.all
相似,其参数承受一个Promise
的数组,返回一个新的Promise
。惟一的不同在于,执行完之后不会失败
,也就是说当Promise.allSettled
全副解决实现后,咱们能够拿到每个Promise
的状态,而不论其是否解决胜利
- 咱们来看一下用
allSettled
实现的一段代码
const resolved = Promise.resolve(2);const rejected = Promise.reject(-1);const allSettledPromise = Promise.allSettled([resolved, rejected]);allSettledPromise.then(function (results) { console.log(results);});// 返回后果:// [// { status: 'fulfilled', value: 2 },// { status: 'rejected', reason: -1 }// ]
从下面代码中能够看到,Promise.allSettled
最初返回的是一个数组,记录传进来的参数中每个 Promise 的返回值,这就是和 all 办法不太一样的中央。
any
办法- 语法:
Promise.any(iterable)
- 参数:
iterable
可迭代的对象,例如Array
。 - 形容:
any
办法返回一个Promise
,只有参数Promise
实例有一个变成fulfilled
状态,最初any
返回的实例就会变成fulfilled
状态;如果所有参数Promise
实例都变成rejected
状态,包装实例就会变成rejected
状态。
- 语法:
const resolved = Promise.resolve(2);const rejected = Promise.reject(-1);const anyPromise = Promise.any([resolved, rejected]);anyPromise.then(function (results) { console.log(results);});// 返回后果:// 2
从革新后的代码中能够看出,只有其中一个Promise
变成fulfilled
状态,那么any
最初就返回这个p romise
。因为下面resolved
这个 Promise 曾经是resolve
的了,故最初返回后果为2
race
办法- 语法:
Promise.race(iterable)
- 参数:
iterable
可迭代的对象,例如Array
。 - 形容:
race
办法返回一个Promise
,只有参数的Promise
之中有一个实例率先扭转状态,则race
办法的返回状态就跟着扭转。那个率先扭转的Promise
实例的返回值,就传递给race
办法的回调函数
- 语法:
- 咱们来看一下这个业务场景,对于图片的加载,特地适宜用 race 办法来解决,将图片申请和超时判断放到一起,用 race 来实现图片的超时判断。请看代码片段。
//申请某个图片资源function requestImg(){ var p = new Promise(function(resolve, reject){ var img = new Image(); img.onload = function(){ resolve(img); } img.src = 'http://www.baidu.com/img/flexible/logo/pc/result.png'; }); return p;}//延时函数,用于给申请计时function timeout(){ var p = new Promise(function(resolve, reject){ setTimeout(function(){ reject('图片申请超时'); }, 5000); }); return p;}Promise.race([requestImg(), timeout()]).then(function(results){ console.log(results);}).catch(function(reason){ console.log(reason);});// 从下面的代码中能够看出,采纳 Promise 的形式来判断图片是否加载胜利,也是针对 Promise.race 办法的一个比拟好的业务场景
promise手写实现,面试够用版:
function myPromise(constructor){ let self=this; self.status="pending" //定义状态扭转前的初始状态 self.value=undefined;//定义状态为resolved的时候的状态 self.reason=undefined;//定义状态为rejected的时候的状态 function resolve(value){ //两个==="pending",保障了状态的扭转是不可逆的 if(self.status==="pending"){ self.value=value; self.status="resolved"; } } function reject(reason){ //两个==="pending",保障了状态的扭转是不可逆的 if(self.status==="pending"){ self.reason=reason; self.status="rejected"; } } //捕捉结构异样 try{ constructor(resolve,reject); }catch(e){ reject(e); }}// 定义链式调用的then办法myPromise.prototype.then=function(onFullfilled,onRejected){ let self=this; switch(self.status){ case "resolved": onFullfilled(self.value); break; case "rejected": onRejected(self.reason); break; default: }}
面向对象
编程思维
- 根本思维是应用对象,类,继承,封装等基本概念来进行程序设计
长处
易保护
- 采纳面向对象思维设计的构造,可读性高,因为继承的存在,即便扭转需要,那么保护也只是在部分模块,所以保护起来是十分不便和较低成本的
- 易扩大
- 开发工作的重用性、继承性高,升高反复工作量。
- 缩短了开发周期
个别面向对象蕴含:继承,封装,多态,形象
1. 对象模式的继承
浅拷贝
var Person = { name: 'poetry', age: 18, address: { home: 'home', office: 'office', } sclools: ['x','z'],};var programer = { language: 'js',};function extend(p, c){ var c = c || {}; for( var prop in p){ c[prop] = p[prop]; }}extend(Person, programer);programer.name; // poetryprogramer.address.home; // homeprogramer.address.home = 'house'; //housePerson.address.home; // house
从下面的后果看出,浅拷贝的缺点在于批改了子对象中援用类型的值,会影响到父对象中的值,因为在浅拷贝中对援用类型的拷贝只是拷贝了地址,指向了内存中同一个正本
深拷贝
function extendDeeply(p, c){ var c = c || {}; for (var prop in p){ if(typeof p[prop] === "object"){ c[prop] = (p[prop].constructor === Array)?[]:{}; extendDeeply(p[prop], c[prop]); }else{ c[prop] = p[prop]; } }}
利用递归进行深拷贝,这样子对象的批改就不会影响到父对象
extendDeeply(Person, programer);programer.address.home = 'poetry';Person.address.home; // home
利用call和apply继承
function Parent(){ this.name = "abc"; this.address = {home: "home"};}function Child(){ Parent.call(this); this.language = "js"; }
ES5中的Object.create()
var p = { name : 'poetry'};var obj = Object.create(p);obj.name; // poetry
Object.create()
作为new操作符的代替计划是ES5之后才进去的。咱们也能够本人模仿该办法:
//模仿Object.create()办法function myCreate(o){ function F(){}; F.prototype = o; o = new F(); return o;}var p = { name : 'poetry'};var obj = myCreate(p);obj.name; // poetry
目前,各大浏览器的最新版本(包含IE9)都部署了这个办法。如果遇到老式浏览器,能够用上面的代码自行部署
if (!Object.create) { Object.create = function (o) { function F() {} F.prototype = o; return new F(); }; }
2. 类的继承
Object.create()
function Person(name, age){}Person.prototype.headCount = 1;Person.prototype.eat = function(){ console.log('eating...');}function Programmer(name, age, title){}Programmer.prototype = Object.create(Person.prototype); //建设继承关系Programmer.prototype.constructor = Programmer; // 批改constructor的指向
调用父类办法
function Person(name, age){ this.name = name; this.age = age;}Person.prototype.headCount = 1;Person.prototype.eat = function(){ console.log('eating...');}function Programmer(name, age, title){ Person.apply(this, arguments); // 调用父类的结构器}Programmer.prototype = Object.create(Person.prototype);Programmer.prototype.constructor = Programmer;Programmer.prototype.language = "js";Programmer.prototype.work = function(){ console.log('i am working code in '+ this.language); Person.prototype.eat.apply(this, arguments); // 调用父类上的办法}
3. 封装
命名空间
- js是没有命名空间的,因而能够用对象模仿
var app = {}; // 命名空间app//模块1app.module1 = { name: 'poetry', f: function(){ console.log('hi robot'); }};app.module1.name; // "poetry"app.module1.f(); // hi robot
对象的属性外界是可读可写 如何来达到封装的额目标?答:可通过闭包+局部变量
来实现
- 在构造函数外部申明局部变量 和一般办法
- 因为作用域的关系 只有构造函数内的办法
- 能力拜访局部变量 而办法对于外界是凋谢的
- 因而能够通过办法来拜访 本来外界拜访不到的局部变量 达到函数封装的目标
function Girl(name,age){ var love = '小明';//love 是局部变量 精确说不属于对象 属于这个函数的额激活对象 函数调用时必将产生一个激活对象 love在激活对象身上 激活对象有作用域的关系 有方法拜访 加一个函数提供外界拜访 this.name = name; this.age = age; this.say = function () { return love; }; this.movelove = function (){ love = '小轩'; //35 }} var g = new Girl('yinghong',22);console.log(g);console.log(g.say());//小明console.log(g.movelove());//undefined 因为35行没有返回console.log(g.say());//小轩function fn(){ function t(){ //var age = 22;//申明age变量 在t的激活对象上 age = 22;//赋值操作 t的激活对象上找age属性 ,找不到 找fn的激活对象....再找到 最终找到window.age = 22; //不加var就是操作window全局属性 } t();}console.log(fn());//undefined
4. 动态成员
面向对象中的静态方法-动态属性:没有new对象 也能援用静态方法属性
function Person(name){ var age = 100; this.name = name;}//动态成员Person.walk = function(){ console.log('static');};Person.walk(); // static
5. 公有与私有
function Person(id){ // 公有属性与办法 var name = 'poetry'; var work = function(){ console.log(this.id); }; //私有属性与办法 this.id = id; this.say = function(){ console.log('say hello'); work.call(this); };};var p1 = new Person(123);p1.name; // undefinedp1.id; // 123p1.say(); // say hello 123
6. 模块化
var moduleA;moduleA = function() { var prop = 1; function func() {} return { func: func, prop: prop };}(); // 立刻执行匿名函数
7. 多态
多态:同一个父类继承进去的子类各有各的状态
function Cat(){ this.eat = '肉';}function Tiger(){ this.color = '黑黄相间';}function Cheetah(){ this.color = '报文';}function Lion(){ this.color = '土黄色';}Tiger.prototype = Cheetah.prototype = Lion.prototype = new Cat();//共享一个先人 Catvar T = new Tiger();var C = new Cheetah();var L = new Lion();console.log(T.color);console.log(C.color);console.log(L.color);console.log(T.eat);console.log(C.eat);console.log(L.eat);
8. 抽象类
在结构器中 throw new Error('')
; 抛异样。这样避免这个类被间接调用
function DetectorBase() { throw new Error('Abstract class can not be invoked directly!');}DetectorBase.prototype.detect = function() { console.log('Detection starting...');};DetectorBase.prototype.stop = function() { console.log('Detection stopped.');};DetectorBase.prototype.init = function() { throw new Error('Error');};// var d = new DetectorBase();// Uncaught Error: Abstract class can not be invoked directly!function LinkDetector() {}LinkDetector.prototype = Object.create(DetectorBase.prototype);LinkDetector.prototype.constructor = LinkDetector;var l = new LinkDetector();console.log(l); //LinkDetector {}__proto__: LinkDetectorl.detect(); //Detection starting...l.init(); //Uncaught Error: Error
connect组件原理剖析
1. connect用法
作用:连贯React
组件与Redux store
connect([mapStateToProps], [mapDispatchToProps], [mergeProps],[options])// 这个函数容许咱们将 store 中的数据作为 props 绑定到组件上const mapStateToProps = (state) => { return { count: state.count }}
- 这个函数的第一个参数就是
Redux
的store
,咱们从中摘取了count
属性。你不用将state
中的数据一成不变地传入组件,能够依据state
中的数据,动静地输入组件须要的(最小)属性 - 函数的第二个参数
ownProps
,是组件本人的props
当state
变动,或者ownProps
变动的时候,mapStateToProps
都会被调用,计算出一个新的stateProps
,(在与ownProps merge
后)更新给组件
mapDispatchToProps(dispatch, ownProps): dispatchProps
connect
的第二个参数是mapDispatchToProps
,它的性能是,将action
作为props
绑定到组件上,也会成为MyComp
的 `props
2. 原理解析
首先connect
之所以会胜利,是因为Provider
组件
- 在原利用组件上包裹一层,使原来整个利用成为
Provider
的子组件 - 接管
Redux
的store
作为props
,通过context
对象传递给子孙组件上的connect
connect做了些什么
它真正连贯Redux
和React
,它包在咱们的容器组件的外一层,它接管下面Provider
提供的store
外面的state
和dispatch
,传给一个构造函数,返回一个对象,以属性模式传给咱们的容器组件
3. 源码
connect
是一个高阶函数,首先传入mapStateToProps
、mapDispatchToProps
,而后返回一个生产Component
的函数(wrapWithConnect
),而后再将真正的Component
作为参数传入wrapWithConnect
,这样就生产出一个通过包裹的Connect
组件,该组件具备如下特点
- 通过
props.store
获取先人Component
的store props
包含stateProps
、dispatchProps
、parentProps
,合并在一起失去nextState
,作为props
传给真正的Component
componentDidMount
时,增加事件this.store.subscribe(this.handleChange)
,实现页面交互shouldComponentUpdate
时判断是否有防止进行渲染,晋升页面性能,并失去nextState
componentWillUnmount
时移除注册的事件this.handleChange
// 次要逻辑export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) { return function wrapWithConnect(WrappedComponent) { class Connect extends Component { constructor(props, context) { // 从先人Component处取得store this.store = props.store || context.store this.stateProps = computeStateProps(this.store, props) this.dispatchProps = computeDispatchProps(this.store, props) this.state = { storeState: null } // 对stateProps、dispatchProps、parentProps进行合并 this.updateState() } shouldComponentUpdate(nextProps, nextState) { // 进行判断,当数据产生扭转时,Component从新渲染 if (propsChanged || mapStateProducedChange || dispatchPropsChanged) { this.updateState(nextProps) return true } } componentDidMount() { // 扭转Component的state this.store.subscribe(() = { this.setState({ storeState: this.store.getState() }) }) } render() { // 生成包裹组件Connect return ( <WrappedComponent {...this.nextState} /> ) } } Connect.contextTypes = { store: storeShape } return Connect; }}
继承
涉及面试题:原型如何实现继承?Class
如何实现继承?Class
实质是什么?
首先先来讲下 class
,其实在 JS
中并不存在类,class
只是语法糖,实质还是函数
class Person {}Person instanceof Function // true
组合继承
组合继承是最罕用的继承形式
function Parent(value) { this.val = value}Parent.prototype.getValue = function() { console.log(this.val)}function Child(value) { Parent.call(this, value)}Child.prototype = new Parent()const child = new Child(1)child.getValue() // 1child instanceof Parent // true
- 以上继承的形式外围是在子类的构造函数中通过
Parent.call(this)
继承父类的属性,而后扭转子类的原型为new Parent()
来继承父类的函数。 - 这种继承形式长处在于构造函数能够传参,不会与父类援用属性共享,能够复用父类的函数,然而也存在一个毛病就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不须要的父类属性,存在内存上的节约
寄生组合继承
这种继承形式对组合继承进行了优化,组合继承毛病在于继承父类函数时调用了构造函数,咱们只须要优化掉这点就行了
function Parent(value) { this.val = value}Parent.prototype.getValue = function() { console.log(this.val)}function Child(value) { Parent.call(this, value)}Child.prototype = Object.create(Parent.prototype, { constructor: { value: Child, enumerable: false, writable: true, configurable: true }})const child = new Child(1)child.getValue() // 1child instanceof Parent // true
以上继承实现的外围就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
Class 继承
以上两种继承形式都是通过原型去解决的,在 ES6 中,咱们能够应用 class 去实现继承,并且实现起来很简略
class Parent { constructor(value) { this.val = value } getValue() { console.log(this.val) }}class Child extends Parent { constructor(value) { super(value) this.val = value }}let child = new Child(1)child.getValue() // 1child instanceof Parent // true
class
实现继承的外围在于应用extends
表明继承自哪个父类,并且在子类构造函数中必须调用super
,因为这段代码能够看成Parent.call(this, value)
。
ES5 和 ES6 继承的区别:
- ES6 继承的子类须要调用
super()
能力拿到子类,ES5 的话是通过apply
这种绑定的形式 - 类申明不会晋升,和
let
这些统一
function Super() {}Super.prototype.getNumber = function() { return 1}function Sub() {}Sub.prototype = Object.create(Super.prototype, { constructor: { value: Sub, enumerable: false, writable: true, configurable: true }})let s = new Sub()s.getNumber()
以下具体解说几种常见的继承形式
1. 形式1: 借助call
function Parent1(){ this.name = 'parent1'; } function Child1(){ Parent1.call(this); this.type = 'child1' } console.log(new Child1);
这样写的时候子类尽管可能拿到父类的属性值,然而问题是父类原型对象中一旦存在办法那么子类无奈继承。那么引出上面的办法。
2. 形式2: 借助原型链
function Parent2() { this.name = 'parent2'; this.play = [1, 2, 3] } function Child2() { this.type = 'child2'; } Child2.prototype = new Parent2(); console.log(new Child2());
看似没有问题,父类的办法和属性都可能拜访,但实际上有一个潜在的有余。举个例子:
var s1 = new Child2();var s2 = new Child2();s1.play.push(4);console.log(s1.play, s2.play);
能够看到控制台:
明明我只扭转了s1的play属性,为什么s2也跟着变了呢?很简略,因为两个实例应用的是同一个原型对象。
那么还有更好的形式么?
3. 形式3:将前两种组合
function Parent3 () { this.name = 'parent3'; this.play = [1, 2, 3]; } function Child3() { Parent3.call(this); this.type = 'child3'; } Child3.prototype = new Parent3(); var s3 = new Child3(); var s4 = new Child3(); s3.play.push(4); console.log(s3.play, s4.play);
能够看到控制台:
之前的问题都得以解决。然而这里又徒增了一个新问题,那就是Parent3
的构造函数会多执行了一次(Child3.prototype = new Parent3();
)。这是咱们不愿看到的。那么如何解决这个问题?
4. 形式4: 组合继承的优化1
function Parent4 () { this.name = 'parent4'; this.play = [1, 2, 3]; } function Child4() { Parent4.call(this); this.type = 'child4'; } Child4.prototype = Parent4.prototype;
这里让将父类原型对象间接给到子类,父类构造函数只执行一次,而且父类属性和办法均能拜访,然而咱们来测试一下:
var s3 = new Child4();var s4 = new Child4();console.log(s3)
子类实例的构造函数是Parent4,显然这是不对的,应该是Child4。
5. 形式5(最举荐应用): 组合继承的优化2
function Parent5 () { this.name = 'parent5'; this.play = [1, 2, 3]; } function Child5() { Parent5.call(this); this.type = 'child5'; } Child5.prototype = Object.create(Parent5.prototype); Child5.prototype.constructor = Child5;
这是最举荐的一种形式,靠近完满的继承,它的名字也叫做寄生组合继承。
6. ES6的extends被编译后的JavaScript代码
ES6的代码最初都是要在浏览器上可能跑起来的,这两头就利用了babel这个编译工具,将ES6的代码编译成ES5让一些不反对新语法的浏览器也能运行。
那最初编译成了什么样子呢?
function _possibleConstructorReturn(self, call) { // ... return call && (typeof call === 'object' || typeof call === 'function') ? call : self;}function _inherits(subClass, superClass) { // ... //看到没有 subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;}var Parent = function Parent() { // 验证是否是 Parent 结构进去的 this _classCallCheck(this, Parent);};var Child = (function (_Parent) { _inherits(Child, _Parent); function Child() { _classCallCheck(this, Child); return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments)); } return Child;}(Parent));
外围是_inherits
函数,能够看到它采纳的仍然也是第五种形式————寄生组合继承形式,同时证实了这种形式的胜利。不过这里加了一个Object.setPrototypeOf(subClass, superClass)
,这是用来干啥的呢?
答案是用来继承父类的静态方法。这也是原来的继承形式忽略掉的中央。
诘问: 面向对象的设计肯定是好的设计吗?
不肯定。从继承的角度说,这一设计是存在微小隐患的。
工程化
介绍一下 webpack 的构建流程
外围概念
entry
:入口。webpack是基于模块的,应用webpack首先须要指定模块解析入口(entry),webpack从入口开始依据模块间依赖关系递归解析和解决所有资源文件。output
:输入。源代码通过webpack解决之后的最终产物。loader
:模块转换器。实质就是一个函数,在该函数中对接管到的内容进行转换,返回转换后的后果。因为 Webpack 只意识 JavaScript,所以 Loader 就成了翻译官,对其余类型的资源进行转译的预处理工作。plugin
:扩大插件。基于事件流框架Tapable
,插件能够扩大 Webpack 的性能,在 Webpack 运行的生命周期中会播送出许多事件,Plugin 能够监听这些事件,在适合的机会通过 Webpack 提供的 API 扭转输入后果。module
:模块。除了js领域内的es module、commonJs、AMD
等,css @import、url(...)
、图片、字体等在webpack中都被视为模块。
解释几个 webpack 中的术语
module
:指在模块化编程中咱们把应用程序宰割成的独立性能的代码模块chunk
:指模块间依照援用关系组合成的代码块,一个chunk
中能够蕴含多个module
chunk group
:指通过配置入口点(entry point
)辨别的块组,一个chunk group
中可蕴含一到多个 chunkbundling
:webpack 打包的过程asset/bundle
:打包产物
webpack 的打包思维能够简化为 3 点:
- 所有源代码文件均可通过各种
Loader
转换为 JS 模块 (module
),模块之间能够相互援用。 - webpack 通过入口点(
entry point
)递归解决各模块援用关系,最初输入为一个或多个产物包js(bundle)
文件。 - 每一个入口点都是一个块组(
chunk group
),在不思考分包的状况下,一个chunk group
中只有一个chunk
,该 chunk 蕴含递归剖析后的所有模块。每一个chunk
都有对应的一个打包后的输入文件(asset/bundle
)
打包流程
- 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置参数。
- 开始编译:从上一步失去的参数初始化
Compiler
对象,加载所有配置的插件,执行对象的run
办法开始执行编译。 - 确定入口:依据配置中的
entry
找出所有的入口文件。 - 编译模块:从入口文件登程,调用所有配置的
loader
对模块进行翻译,再找出该模块依赖的模块,这个步骤是递归执行的,直至所有入口依赖的模块文件都通过本步骤的解决。 - 实现模块编译:通过第 4 步应用 loader 翻译完所有模块后,失去了每个模块被翻译后的最终内容以及它们之间的依赖关系。
- 输入资源:依据入口和模块之间的依赖关系,组装成一个个蕴含多个模块的
chunk
,再把每个chunk
转换成一个独自的文件退出到输入列表,这一步是能够批改输入内容的最初机会。 - 输入实现:在确定好输入内容后,依据配置确定输入的门路和文件名,把文件内容写入到文件系统。
简版
- Webpack CLI 启动打包流程;
- 载入 Webpack 外围模块,创立
Compiler
对象; - 应用
Compiler
对象开始编译整个我的项目; - 从入口文件开始,解析模块依赖,造成依赖关系树;
- 递归依赖树,将每个模块交给对应的 Loader 解决;
- 合并 Loader 解决完的后果,将打包后果输入到 dist 目录。
在以上过程中,Webpack 会在特定的工夫点播送出特定的事件
,插件在监听到相干事件后会执行特定的逻辑,并且插件能够调用 Webpack 提供的 API 扭转 Webpack 的运行后果
构建流程外围概念:
Tapable
:一个基于公布订阅的事件流工具类,Compiler
和Compilation
对象都继承于Tapable
Compiler
:compiler对象是一个全局单例,他负责把控整个webpack打包的构建流程。在编译初始化阶段被创立的全局单例,蕴含残缺配置信息、loaders
、plugins以及各种工具办法Compilation
:代表一次 webpack 构建和生成编译资源的的过程,在watch
模式下每一次文件变更触发的从新编译都会生成新的Compilation
对象,蕴含了以后编译的模块module
, 编译生成的资源,变动的文件, 依赖的状态等- 而每个模块间的依赖关系,则依赖于
AST
语法树。每个模块文件在通过Loader解析实现之后,会通过acorn
库生成模块代码的AST语法树,通过语法树就能够剖析这个模块是否还有依赖的模块,进而持续循环执行下一个模块的编译解析。
最终Webpack
打包进去的bundle
文件是一个IIFE
的执行函数。
// webpack 5 打包的bundle文件内容(() => { // webpackBootstrap var __webpack_modules__ = ({ 'file-A-path': ((modules) => { // ... }) 'index-file-path': ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... }) }) // The module cache var __webpack_module_cache__ = {}; // The require function function __webpack_require__(moduleId) { // Check if module is in cache var cachedModule = __webpack_module_cache__[moduleId]; if (cachedModule !== undefined) { return cachedModule.exports; } // Create a new module (and put it into the cache) var module = __webpack_module_cache__[moduleId] = { // no module.id needed // no module.loaded needed exports: {} }; // Execute the module function __webpack_modules__[moduleId](module, module.exports, __webpack_require__); // Return the exports of the module return module.exports; } // startup // Load entry module and return exports // This entry module can't be inlined because the eval devtool is used. var __webpack_exports__ = __webpack_require__("./src/index.js");})
webpack具体工作流程
template预编译是什么
对于 Vue 组件来说,模板编译只会在组件实例化的时候编译一次,生成渲染函数之后在也不会进行编译。因而,编译对组件的 runtime 是一种性能损耗。
而模板编译的目标仅仅是将template转化为render function,这个过程,正好能够在我的项目构建的过程中实现,这样能够让理论组件在 runtime 时间接跳过模板渲染,进而晋升性能,这个在我的项目构建的编译template的过程,就是预编译。
React组件和渲染更新过程
渲染和更新过程
- jsx如何渲染为页面
- setState之后如何更新页面
- 面试考查全流程
JSX实质和vdom
- JSX即
createElement
函数 - 执行生成vnode
patch(elem,vnode)
和patch(vnode,newNode)
组件渲染过程
props state
render()
生成vnode
patch(elem, vnode)
组件更新过程
setState-->dirtyComponents
(可能有子组件)render
生成newVnode
patch(vnode, newVnode)
如何解决 1px 问题?
1px 问题指的是:在一些 Retina屏幕
的机型上,挪动端页面的 1px 会变得很粗,呈现出不止 1px 的成果。起因很简略——CSS 中的 1px 并不能和挪动设施上的 1px 划等号。它们之间的比例关系有一个专门的属性来形容:
window.devicePixelRatio = 设施的物理像素 / CSS像素。
关上 Chrome 浏览器,启动挪动端调试模式,在控制台去输入这个 devicePixelRatio
的值。这里选中 iPhone6/7/8 这系列的机型,输入的后果就是2: 这就意味着设置的 1px CSS 像素,在这个设施上理论会用 2 个物理像素单元来进行渲染,所以理论看到的肯定会比 1px 粗一些。 解决1px 问题的三种思路:
思路一:间接写 0.5px
如果之前 1px 的款式这样写:
border:1px solid #333
能够先在 JS 中拿到 window.devicePixelRatio 的值,而后把这个值通过 JSX 或者模板语法给到 CSS 的 data 里,达到这样的成果(这里用 JSX 语法做示范):
<div id="container" data-device={{window.devicePixelRatio}}></div>
而后就能够在 CSS 中用属性选择器来命中 devicePixelRatio 为某一值的状况,比如说这里尝试命中 devicePixelRatio 为2的状况:
#container[data-device="2"] { border:0.5px solid #333}
间接把 1px 改成 1/devicePixelRatio 后的值,这是目前为止最简略的一种办法。这种办法的缺点在于兼容性不行,IOS 零碎须要8及以上的版本,安卓零碎则间接不兼容。
思路二:伪元素先放大后放大
这个办法的可行性会更高,兼容性也更好。惟一的毛病是代码会变多。
思路是先放大、后放大:在指标元素的前面追加一个 ::after 伪元素,让这个元素布局为 absolute 之后、整个伸开展铺在指标元素上,而后把它的宽和高都设置为指标元素的两倍,border值设为 1px。接着借助 CSS 动画特效中的放缩能力,把整个伪元素放大为原来的 50%。此时,伪元素的宽高刚好能够和原有的指标元素对齐,而 border 也放大为了 1px 的二分之一,间接地实现了 0.5px 的成果。
代码如下:
#container[data-device="2"] { position: relative;}#container[data-device="2"]::after{ position:absolute; top: 0; left: 0; width: 200%; height: 200%; content:""; transform: scale(0.5); transform-origin: left top; box-sizing: border-box; border: 1px solid #333; }}
思路三:viewport 缩放来解决
这个思路就是对 meta 标签里几个要害属性下手:
<meta name="viewport" content="initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no">
这里针对像素比为2的页面,把整个页面缩放为了原来的1/2大小。这样,原本占用2个物理像素的 1px 款式,当初占用的就是规范的一个物理像素。依据像素比的不同,这个缩放比例能够被计算为不同的值,用 js 代码实现如下:
const scale = 1 / window.devicePixelRatio;// 这里 metaEl 指的是 meta 标签对应的 DommetaEl.setAttribute('content', `width=device-width,user-scalable=no,initial-scale=${scale},maximum-scale=${scale},minimum-scale=${scale}`);
这样解决了,但这样做的副作用也很大,整个页面被缩放了。这时 1px 曾经被解决成物理像素大小,这样的大小在手机上显示边框很适合。然而,一些本来不须要被放大的内容,比方文字、图片等,也被无差别放大掉了。
对象继承的形式有哪些?
(1)第一种是以原型链的形式来实现继承,然而这种实现形式存在的毛病是,在蕴含有援用类型的数据时,会被所有的实例对象所共享,容易造成批改的凌乱。还有就是在创立子类型的时候不能向超类型传递参数。
(2)第二种形式是应用借用构造函数的形式,这种形式是通过在子类型的函数中调用超类型的构造函数来实现的,这一种办法解决了不能向超类型传递参数的毛病,然而它存在的一个问题就是无奈实现函数办法的复用,并且超类型原型定义的办法子类型也没有方法拜访到。
(3)第三种形式是组合继承,组合继承是将原型链和借用构造函数组合起来应用的一种形式。通过借用构造函数的形式来实现类型的属性的继承,通过将子类型的原型设置为超类型的实例来实现办法的继承。这种形式解决了下面的两种模式独自应用时的问题,然而因为咱们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性。
(4)第四种形式是原型式继承,原型式继承的次要思路就是基于已有的对象来创立新的对象,实现的原理是,向函数中传入一个对象,而后返回一个以这个对象为原型的对象。这种继承的思路次要不是为了实现发明一种新的类型,只是对某个对象实现一种简略继承,ES5 中定义的 Object.create() 办法就是原型式继承的实现。毛病与原型链形式雷同。
(5)第五种形式是寄生式继承,寄生式继承的思路是创立一个用于封装继承过程的函数,通过传入一个对象,而后复制一个对象的正本,而后对象进行扩大,最初返回这个对象。这个扩大的过程就能够了解是一种继承。这种继承的长处就是对一个简略对象实现继承,如果这个对象不是自定义类型时。毛病是没有方法实现函数的复用。
(6)第六种形式是寄生式组合继承,组合继承的毛病就是应用超类型的实例做为子类型的原型,导致增加了不必要的原型属性。寄生式组合继承的形式是应用超类型的原型的副原本作为子类型的原型,这样就防止了创立不必要的属性。
左右居中计划
- 行内元素:
text-align: center
- 定宽块状元素: 左右
margin
值为auto
- 不定宽块状元素:
table
布局,position + transform
/* 计划1 */.wrap { text-align: center}.center { display: inline; /* or */ /* display: inline-block; */}/* 计划2 */.center { width: 100px; margin: 0 auto;}/* 计划2 */.wrap { position: relative;}.center { position: absulote; left: 50%; transform: translateX(-50%);}
JavaScript为什么要进行变量晋升,它导致了什么问题?
变量晋升的体现是,无论在函数中何处地位申明的变量,如同都被晋升到了函数的首部,能够在变量申明前拜访到而不会报错。
造成变量申明晋升的实质起因是 js 引擎在代码执行前有一个解析的过程,创立了执行上下文,初始化了一些代码执行时须要用到的对象。当拜访一个变量时,会到以后执行上下文中的作用域链中去查找,而作用域链的首端指向的是以后执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它蕴含了函数的形参、所有的函数和变量申明,这个对象的是在代码解析的时候创立的。
首先要晓得,JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行。
在解析阶段,JS会查看语法,并对函数进行预编译。解析的时候会先创立一个全局执行上下文环境,先把代码中行将执行的变量、函数申明都拿进去,变量先赋值为undefined,函数先申明好可应用。在一个函数执行之前,也会创立一个函数执行上下文环境,跟全局执行上下文相似,不过函数执行上下文会多出this、arguments和函数的参数。
- 全局上下文:变量定义,函数申明
- 函数上下文:变量定义,函数申明,this,arguments
- 在执行阶段,就是依照代码的程序顺次执行。
那为什么会进行变量晋升呢?次要有以下两个起因:
- 进步性能
- 容错性更好
(1)进步性能 在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了进步性能,如果没有这一步,那么每次执行代码前都必须从新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会扭转,解析一遍就够了。
在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计申明了哪些变量、创立了哪些函数,并对函数的代码进行压缩,去除正文、不必要的空白等。这样做的益处就是每次执行函数时都能够间接为该函数调配栈空间(不须要再解析一遍去获取代码中申明了哪些变量,创立了哪些函数),并且因为代码压缩的起因,代码执行也更快了。
(2)容错性更好
变量晋升能够在肯定水平上进步JS的容错性,看上面的代码:
a = 1;var a;console.log(a);
如果没有变量晋升,这两行代码就会报错,然而因为有了变量晋升,这段代码就能够失常执行。
尽管,在能够开发过程中,能够完全避免这样写,然而有时代码很简单的时候。可能因为忽略而先应用后定义了,这样也不会影响失常应用。因为变量晋升的存在,而会失常运行。
总结:
- 解析和预编译过程中的申明晋升能够进步性能,让函数能够在执行时事后为变量调配栈空间
- 申明晋升还能够进步JS代码的容错性,使一些不标准的代码也能够失常执行
变量晋升尽管有一些长处,然而他也会造成肯定的问题,在ES6中提出了let、const来定义变量,它们就没有变量晋升的机制。上面看一下变量晋升可能会导致的问题:
var tmp = new Date();function fn(){ console.log(tmp); if(false){ var tmp = 'hello world'; }}fn(); // undefined
在这个函数中,本来是要打印出外层的tmp变量,然而因为变量晋升的问题,内层定义的tmp被提到函数外部的最顶部,相当于笼罩了外层的tmp,所以打印后果为undefined。
var tmp = 'hello world';for (var i = 0; i < tmp.length; i++) { console.log(tmp[i]);}console.log(i); // 11
因为遍历时定义的i会变量晋升成为一个全局变量,在函数完结之后不会被销毁,所以打印进去11。
懒加载与预加载的区别
这两种形式都是进步网页性能的形式,两者次要区别是一个是提前加载,一个是缓慢甚至不加载。懒加载对服务器前端有肯定的缓解压力作用,预加载则会减少服务器前端压力。
- 懒加载也叫提早加载,指的是在长网页中提早加载图片的机会,当用户须要拜访时,再去加载,这样能够进步网站的首屏加载速度,晋升用户的体验,并且能够缩小服务器的压力。它实用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的实在门路保留在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出实在门路赋值给图片的 src 属性,以此来实现图片的提早加载。
- 预加载指的是将所需的资源提前申请加载到本地,这样前面在须要用到时就间接从缓存取资源。 通过预加载可能缩小用户的等待时间,进步用户的体验。我理解的预加载的最罕用的形式是应用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。
Number() 的存储空间是多大?如果后盾发送了一个超过最大本人的数字怎么办
Math.pow(2, 53) ,53 为有效数字,会产生截断,等于 JS 能反对的最大数字。
说一下数组如何去重,你有几种办法?
let arr = [1,1,"1","1",true,true,"true",{},{},"{}",null,null,undefined,undefined]// 办法1let uniqueOne = Array.from(new Set(arr)) console.log(uniqueOne)// 办法2let uniqueTwo = arr => { let map = new Map(); //或者用空对象 let obj = {} 利用对象属性不能反复得个性 let brr = [] arr.forEach( item => { if(!map.has(item)) { //如果是对象得话就判断 !obj[item] map.set(item,true) //如果是对象得话就obj[item] =true 其余一样 brr.push(item) } }) return brr}console.log(uniqueTwo(arr))//办法3let uniqueThree = arr => { let brr = [] arr.forEach(item => { // 应用indexOf 返回数组是否蕴含某个值 没有就返回-1 有就返回下标 if(brr.indexOf(item) === -1) brr.push(item) // 或者应用includes 返回数组是否蕴含某个值 没有就返回false 有就返回true if(!brr.includes(item)) brr.push(item) }) return brr}console.log(uniqueThree(arr))//办法4let uniqueFour = arr => { // 应用 filter 返回符合条件的汇合 let brr = arr.filter((item,index) => { return arr.indexOf(item) === index }) return brr}console.log(uniqueFour(arr))
实现有并行限度的 Promise 调度器
题目形容:JS 实现一个带并发限度的异步调度器 Scheduler,保障同时运行的工作最多有两个
addTask(1000,"1"); addTask(500,"2"); addTask(300,"3"); addTask(400,"4"); 的输入程序是:2 3 1 4 整个的残缺执行流程:一开始1、2两个工作开始执行500ms时,2工作执行结束,输入2,工作3开始执行800ms时,3工作执行结束,输入3,工作4开始执行1000ms时,1工作执行结束,输入1,此时只剩下4工作在执行1200ms时,4工作执行结束,输入4
实现代码如下:
class Scheduler { constructor(limit) { this.queue = []; this.maxCount = limit; this.runCounts = 0; } add(time, order) { const promiseCreator = () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log(order); resolve(); }, time); }); }; this.queue.push(promiseCreator); } taskStart() { for (let i = 0; i < this.maxCount; i++) { this.request(); } } request() { if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) { return; } this.runCounts++; this.queue .shift()() .then(() => { this.runCounts--; this.request(); }); }}const scheduler = new Scheduler(2);const addTask = (time, order) => { scheduler.add(time, order);};addTask(1000, "1");addTask(500, "2");addTask(300, "3");addTask(400, "4");scheduler.taskStart();
如何解释 React 的渲染流程
- React 的渲染过程大抵统一,但协调并不相同,以
React 16
为分界线,分为Stack Reconciler
和Fiber Reconciler
。这里的协调从广义上来讲,特指 React 的 diff 算法,狭义上来讲,有时候也指 React 的reconciler
模块,它通常蕴含了diff
算法和一些公共逻辑。 - 回到
Stack Reconciler
中,Stack Reconciler
的外围调度形式是递归
。调度的根本解决单位是事务
,它的事务基类是Transaction
,这里的事务是 React 团队从后端开发中退出的概念
。在 React 16 以前,挂载次要通过 ReactMount 模块实现
,更新通过ReactUpdate
模块实现,模块之间互相拆散,落脚执行点也是事务。 - 在
React 16
及当前,协调改为了Fiber Reconciler
。它的调度形式次要有两个特点,第一个是合作式多任务模式
,在这个模式下,线程会定时放弃本人的运行权力,交还给主线程,通过requestIdleCallback
实现。第二个特点是策略优先级
,调度工作通过标记tag
的形式分优先级执行,比方动画,或者标记为high
的工作能够优先执行。Fiber Reconciler
的根本单位是Fiber
,Fiber
基于过来的React Element
提供了二次封装,提供了指向父、子、兄弟节点的援用,为diff
工作的双链表实现提供了根底。 - 在新的架构下,整个生命周期被划分为
Render 和 Commit 两个阶段
。Render 阶段的执行特点是可中断、可进行、无副作用
,次要是通过结构workInProgress
树计算出diff
。以current
树为根底,将每个Fiber
作为一个根本单位,自下而上一一节点查看并结构 workInProgress 树。这个过程不再是递归,而是基于循环来实现 - 在执行上通过
requestIdleCallback
来调度执行每组工作,每组中的每个计算工作被称为work
,每个work
实现后确认是否有优先级更高的work
须要插入,如果有就让位,没有就持续。优先级通常是标记为动画或者high
的会先解决。每实现一组后,将调度权交回主线程,直到下一次requestIdleCallback
调用,再持续构建workInProgress
树 - 在
commit
阶段须要解决effect
列表,这里的effect
列表蕴含了依据diff 更新 DOM 树
、回调生命周期
、响应 ref
等。 - 但肯定要留神,这个阶段是同步执行的,不可中断暂停,所以不要在
componentDidMount
、componentDidUpdate
、componentWiilUnmount
中去执行重度耗费算力的工作 - 如果只是个别的利用场景,比方治理后盾、H5 展现页等,两者性能差距并不大,但在动画、画布及手势等场景下,
Stack Reconciler
的设计会占用占主线程,造成卡顿,而fiber reconciler
的设计则能带来高性能的体现
箭头函数和一般函数有啥区别?箭头函数能当构造函数吗?
- 一般函数通过 function 关键字定义, this 无奈联合词法作用域应用,在运行时绑定,只取决于函数的调用形式,在哪里被调用,调用地位。(取决于调用者,和是否独立运行)
箭头函数应用被称为 “胖箭头” 的操作
=>
定义,箭头函数不利用一般函数 this 绑定的四种规定,而是依据外层(函数或全局)的作用域来决定 this,且箭头函数的绑定无奈被批改(new 也不行)。- 箭头函数罕用于回调函数中,包含事件处理器或定时器
- 箭头函数和 var self = this,都试图取代传统的 this 运行机制,将 this 的绑定拉回到词法作用域
- 没有原型、没有 this、没有 super,没有 arguments,没有 new.target
不能通过 new 关键字调用
- 一个函数外部有两个办法:[[Call]] 和 [[Construct]],在通过 new 进行函数调用时,会执行 [[construct]] 办法,创立一个实例对象,而后再执行这个函数体,将函数的 this 绑定在这个实例对象上
- 当间接调用时,执行 [[Call]] 办法,间接执行函数体
- 箭头函数没有 [[Construct]] 办法,不能被用作结构函数调用,当应用 new 进行函数调用时会报错。
function foo() { return (a) => { console.log(this.a); }}var obj1 = { a: 2}var obj2 = { a: 3 }var bar = foo.call(obj1);bar.call(obj2);
参考 前端进阶面试题具体解答
代码输入后果
// afunction Foo () { getName = function () { console.log(1); } return this;}// bFoo.getName = function () { console.log(2);}// cFoo.prototype.getName = function () { console.log(3);}// dvar getName = function () { console.log(4);}// efunction getName () { console.log(5);}Foo.getName(); // 2getName(); // 4Foo().getName(); // 1getName(); // 1 new Foo.getName(); // 2new Foo().getName(); // 3new new Foo().getName(); // 3
输入后果:2 4 1 1 2 3 3
解析:
- Foo.getName(), Foo为一个函数对象,对象都能够有属性,b 处定义Foo的getName属性为函数,输入2;
- getName(), 这里看d、e处,d为函数表达式,e为函数申明,两者区别在于变量晋升,函数申明的 5 会被后边函数表达式的 4 笼罩;
- Foo().getName(), 这里要看a处,在Foo外部将全局的getName从新赋值为 console.log(1) 的函数,执行Foo()返回 this,这个this指向window,Foo().getName() 即为window.getName(),输入 1;
- getName(), 下面3中,全局的getName曾经被从新赋值,所以这里仍然输入 1;
- new Foo.getName(), 这里等价于 new (Foo.getName()),先执行 Foo.getName(),输入 2,而后new一个实例;
- new Foo().getName(), 这 里等价于 (new Foo()).getName(), 先new一个Foo的实例,再执行这个实例的getName办法,然而这个实例自身没有这个办法,所以去原型链__protot__上边找,实例.protot === Foo.prototype,所以输入 3;
- new new Foo().getName(), 这里等价于new (new Foo().getName()),如上述6,先输入 3,而后new 一个 new Foo().getName() 的实例。
CDN的作用
CDN个别会用来托管Web资源(包含文本、图片和脚本等),可供下载的资源(媒体文件、软件、文档等),应用程序(门户网站等)。应用CDN来减速这些资源的拜访。
(1)在性能方面,引入CDN的作用在于:
- 用户收到的内容来自最近的数据中心,提早更低,内容加载更快
- 局部资源申请调配给了CDN,缩小了服务器的负载
(2)在平安方面,CDN有助于进攻DDoS、MITM等网络攻击:
- 针对DDoS:通过监控剖析异样流量,限度其申请频率
- 针对MITM:从源服务器到 CDN 节点到 ISP(Internet Service Provider),全链路 HTTPS 通信
除此之外,CDN作为一种根底的云服务,同样具备资源托管、按需扩大(可能应答流量顶峰)等方面的劣势。
如何解决逾越问题
(1)CORS
上面是MDN对于CORS的定义:
跨域资源共享(CORS) 是一种机制,它应用额定的 HTTP 头来通知浏览器 让运行在一个 origin (domain)上的Web利用被准许拜访来自不同源服务器上的指定的资源。当一个资源从与该资源自身所在的服务器不同的域、协定或端口申请一个资源时,资源会发动一个跨域HTTP 申请。
CORS须要浏览器和服务器同时反对,整个CORS过程都是浏览器实现的,无需用户参加。因而实现CORS的要害就是服务器,只有服务器实现了CORS申请,就能够跨源通信了。
浏览器将CORS分为简略申请和非简略申请:
简略申请不会触发CORS预检申请。若该申请满足以下两个条件,就能够看作是简略申请:
1)申请办法是以下三种办法之一:
- HEAD
- GET
- POST
2)HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
若不满足以上条件,就属于非简略申请了。
(1)简略申请过程:
对于简略申请,浏览器会间接收回CORS申请,它会在申请的头信息中减少一个Orign字段,该字段用来阐明本次申请来自哪个源(协定+端口+域名),服务器会依据这个值来决定是否批准这次申请。如果Orign指定的域名在许可范畴之内,服务器返回的响应就会多出以下信息头:
Access-Control-Allow-Origin: http://api.bob.com // 和Orign始终Access-Control-Allow-Credentials: true // 示意是否容许发送CookieAccess-Control-Expose-Headers: FooBar // 指定返回其余字段的值Content-Type: text/html; charset=utf-8 // 示意文档类型
如果Orign指定的域名不在许可范畴之内,服务器会返回一个失常的HTTP回应,浏览器发现没有下面的Access-Control-Allow-Origin头部信息,就晓得出错了。这个谬误无奈通过状态码辨认,因为返回的状态码可能是200。
在简略申请中,在服务器内,至多须要设置字段:Access-Control-Allow-Origin
(2)非简略申请过程
非简略申请是对服务器有特殊要求的申请,比方申请办法为DELETE或者PUT等。非简略申请的CORS申请会在正式通信之前进行一次HTTP查问申请,称为预检申请。
浏览器会询问服务器,以后所在的网页是否在服务器容许拜访的范畴内,以及能够应用哪些HTTP申请形式和头信息字段,只有失去必定的回复,才会进行正式的HTTP申请,否则就会报错。
预检申请应用的申请办法是OPTIONS,示意这个申请是来询问的。他的头信息中的关键字段是Orign,示意申请来自哪个源。除此之外,头信息中还包含两个字段:
- Access-Control-Request-Method:该字段是必须的,用来列出浏览器的CORS申请会用到哪些HTTP办法。
- Access-Control-Request-Headers: 该字段是一个逗号分隔的字符串,指定浏览器CORS申请会额定发送的头信息字段。
服务器在收到浏览器的预检申请之后,会依据头信息的三个字段来进行判断,如果返回的头信息在中有Access-Control-Allow-Origin这个字段就是容许跨域申请,如果没有,就是不批准这个预检申请,就会报错。
服务器回应的CORS的字段如下:
Access-Control-Allow-Origin: http://api.bob.com // 容许跨域的源地址Access-Control-Allow-Methods: GET, POST, PUT // 服务器反对的所有跨域申请的办法Access-Control-Allow-Headers: X-Custom-Header // 服务器反对的所有头信息字段Access-Control-Allow-Credentials: true // 示意是否容许发送CookieAccess-Control-Max-Age: 1728000 // 用来指定本次预检申请的有效期,单位为秒
只有服务器通过了预检申请,在当前每次的CORS申请都会自带一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。
在非简略申请中,至多须要设置以下字段:
'Access-Control-Allow-Origin' 'Access-Control-Allow-Methods''Access-Control-Allow-Headers'
缩小OPTIONS申请次数:
OPTIONS申请次数过多就会损耗页面加载的性能,升高用户体验度。所以尽量要缩小OPTIONS申请次数,能够后端在申请的返回头部增加:Access-Control-Max-Age:number。它示意预检申请的返回后果能够被缓存多久,单位是秒。该字段只对齐全一样的URL的缓存设置失效,所以设置了缓存工夫,在这个工夫范畴内,再次发送申请就不须要进行预检申请了。
CORS中Cookie相干问题:
在CORS申请中,如果想要传递Cookie,就要满足以下三个条件:
- 在申请中设置
withCredentials
默认状况下在跨域申请,浏览器是不带 cookie 的。然而咱们能够通过设置 withCredentials 来进行传递 cookie.
// 原生 xml 的设置形式var xhr = new XMLHttpRequest();xhr.withCredentials = true;// axios 设置形式axios.defaults.withCredentials = true;
- Access-Control-Allow-Credentials 设置为 true
- Access-Control-Allow-Origin 设置为非
*
(2)JSONP
jsonp的原理就是利用<script>
标签没有跨域限度,通过<script>
标签src属性,发送带有callback参数的GET申请,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。
1)原生JS实现:
<script> var script = document.createElement('script'); script.type = 'text/javascript'; // 传参一个回调函数名给后端,不便后端返回时执行这个在前端定义的回调函数 script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback'; document.head.appendChild(script); // 回调执行函数 function handleCallback(res) { alert(JSON.stringify(res)); } </script>
服务端返回如下(返回时即执行全局函数):
handleCallback({"success": true, "user": "admin"})
2)Vue axios实现:
this.$http = axios;this.$http.jsonp('http://www.domain2.com:8080/login', { params: {}, jsonp: 'handleCallback'}).then((res) => { console.log(res); })
后端node.js代码:
var querystring = require('querystring');var http = require('http');var server = http.createServer();server.on('request', function(req, res) { var params = querystring.parse(req.url.split('?')[1]); var fn = params.callback; // jsonp返回设置 res.writeHead(200, { 'Content-Type': 'text/javascript' }); res.write(fn + '(' + JSON.stringify(params) + ')'); res.end();});server.listen('8080');console.log('Server is running at port 8080...');
JSONP的毛病:
- 具备局限性, 仅反对get办法
- 不平安,可能会蒙受XSS攻打
(3)postMessage 跨域
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多能够跨域操作的window属性之一,它可用于解决以下方面的问题:
- 页面和其关上的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的iframe消息传递
- 下面三个场景的跨域数据传递
用法:postMessage(data,origin)办法承受两个参数:
- data: html5标准反对任意根本类型或可复制的对象,但局部浏览器只反对字符串,所以传参时最好用JSON.stringify()序列化。
- origin: 协定+主机+端口号,也能够设置为"*",示意能够传递给任意窗口,如果要指定和以后窗口同源的话设置为"/"。
1)a.html:(domain1.com/a.html)
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe><script> var iframe = document.getElementById('iframe'); iframe.onload = function() { var data = { name: 'aym' }; // 向domain2传送跨域数据 iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com'); }; // 承受domain2返回数据 window.addEventListener('message', function(e) { alert('data from domain2 ---> ' + e.data); }, false);</script>
2)b.html:(domain2.com/b.html)
<script> // 接管domain1的数据 window.addEventListener('message', function(e) { alert('data from domain1 ---> ' + e.data); var data = JSON.parse(e.data); if (data) { data.number = 16; // 解决后再发回domain1 window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com'); } }, false);</script>
(4)nginx代理跨域
nginx代理跨域,本质和CORS跨域原理一样,通过配置文件设置申请响应头Access-Control-Allow-Origin…等字段。
1)nginx配置解决iconfont跨域
浏览器跨域拜访js、css、img等惯例动态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的动态资源服务器中退出以下配置。
location / { add_header Access-Control-Allow-Origin *;}
2)nginx反向代理接口跨域
跨域问题:同源策略仅是针对浏览器的安全策略。服务器端调用HTTP接口只是应用HTTP协定,不须要同源策略,也就不存在跨域问题。
实现思路:通过Nginx配置一个代理服务器域名与domain1雷同,端口不同)做跳板机,反向代理拜访domain2接口,并且能够顺便批改cookie中domain信息,不便以后域cookie写入,实现跨域拜访。
nginx具体配置:
#proxy服务器server { listen 81; server_name www.domain1.com; location / { proxy_pass http://www.domain2.com:8080; #反向代理 proxy_cookie_domain www.domain2.com www.domain1.com; #批改cookie里域名 index index.html index.htm; # 当用webpack-dev-server等中间件代理接口拜访nignx时,此时无浏览器参加,故没有同源限度,上面的跨域配置可不启用 add_header Access-Control-Allow-Origin http://www.domain1.com; #以后端只跨域不带cookie时,可为* add_header Access-Control-Allow-Credentials true; }}
(5)nodejs 中间件代理跨域
node中间件实现跨域代理,原理大抵与nginx雷同,都是通过启一个代理服务器,实现数据的转发,也能够通过设置cookieDomainRewrite参数批改响应头中cookie中域名,实现以后域的cookie写入,不便接口登录认证。
1)非vue框架的跨域 应用node + express + http-proxy-middleware搭建一个proxy服务器。
- 前端代码:
var xhr = new XMLHttpRequest();// 前端开关:浏览器是否读写cookiexhr.withCredentials = true;// 拜访http-proxy-middleware代理服务器xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);xhr.send();
- 中间件服务器代码:
var express = require('express');var proxy = require('http-proxy-middleware');var app = express();app.use('/', proxy({ // 代理跨域指标接口 target: 'http://www.domain2.com:8080', changeOrigin: true, // 批改响应头信息,实现跨域并容许带cookie onProxyRes: function(proxyRes, req, res) { res.header('Access-Control-Allow-Origin', 'http://www.domain1.com'); res.header('Access-Control-Allow-Credentials', 'true'); }, // 批改响应信息中的cookie域名 cookieDomainRewrite: 'www.domain1.com' // 能够为false,示意不批改}));app.listen(3000);console.log('Proxy server is listen at port 3000...');
2)vue框架的跨域
node + vue + webpack + webpack-dev-server搭建的我的项目,跨域申请接口,间接批改webpack.config.js配置。开发环境下,vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域。
webpack.config.js局部配置:
module.exports = { entry: {}, module: {}, ... devServer: { historyApiFallback: true, proxy: [{ context: '/login', target: 'http://www.domain2.com:8080', // 代理跨域指标接口 changeOrigin: true, secure: false, // 当代理某些https服务报错时用 cookieDomainRewrite: 'www.domain1.com' // 能够为false,示意不批改 }], noInfo: true }}
(6)document.domain + iframe跨域
此计划仅限主域雷同,子域不同的跨域利用场景。实现原理:两个页面都通过js强制设置document.domain为根底主域,就实现了同域。
1)父窗口:(domain.com/a.html)
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe><script> document.domain = 'domain.com'; var user = 'admin';</script>
1)子窗口:(child.domain.com/a.html)
<script> document.domain = 'domain.com'; // 获取父窗口中变量 console.log('get js data from parent ---> ' + window.parent.user);</script>
(7)location.hash + iframe跨域
实现原理:a欲与b跨域互相通信,通过两头页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,雷同域之间间接js拜访来通信。
具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent拜访a页面所有对象。
1)a.html:(domain1.com/a.html)
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe><script> var iframe = document.getElementById('iframe'); // 向b.html传hash值 setTimeout(function() { iframe.src = iframe.src + '#user=admin'; }, 1000); // 凋谢给同域c.html的回调办法 function onCallback(res) { alert('data from c.html ---> ' + res); }</script>
2)b.html:(.domain2.com/b.html)
<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe><script> var iframe = document.getElementById('iframe'); // 监听a.html传来的hash值,再传给c.html window.onhashchange = function () { iframe.src = iframe.src + location.hash; };</script>
<script> // 监听b.html传来的hash值 window.onhashchange = function () { // 再通过操作同域a.html的js回调,将后果传回 window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', '')); };</script>
(8)window.name + iframe跨域
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后仍旧存在,并且能够反对十分长的 name 值(2MB)。
1)a.html:(domain1.com/a.html)
var proxy = function(url, callback) { var state = 0; var iframe = document.createElement('iframe'); // 加载跨域页面 iframe.src = url; // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name iframe.onload = function() { if (state === 1) { // 第2次onload(同域proxy页)胜利后,读取同域window.name中数据 callback(iframe.contentWindow.name); destoryFrame(); } else if (state === 0) { // 第1次onload(跨域页)胜利后,切换到同域代理页面 iframe.contentWindow.location = 'http://www.domain1.com/proxy.html'; state = 1; } }; document.body.appendChild(iframe); // 获取数据当前销毁这个iframe,开释内存;这也保障了平安(不被其余域frame js拜访) function destoryFrame() { iframe.contentWindow.document.write(''); iframe.contentWindow.close(); document.body.removeChild(iframe); }};// 申请跨域b页面数据proxy('http://www.domain2.com/b.html', function(data){ alert(data);});
2)proxy.html:(domain1.com/proxy.html)
两头代理页,与a.html同域,内容为空即可。
3)b.html:(domain2.com/b.html)
<script> window.name = 'This is domain2 data!';</script>
通过iframe的src属性由外域转向本地区,跨域数据即由iframe的window.name从外域传递到本地区。这个就奇妙地绕过了浏览器的跨域拜访限度,但同时它又是平安操作。
(9)WebSocket协定跨域
WebSocket protocol是HTML5一种新的协定。它实现了浏览器与服务器全双工通信,同时容许跨域通信,是server push技术的一种很好的实现。
原生WebSocket API应用起来不太不便,咱们应用Socket.io,它很好地封装了webSocket接口,提供了更简略、灵便的接口,也对不反对webSocket的浏览器提供了向下兼容。
1)前端代码:
<div>user input:<input type="text"></div><script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script><script>var socket = io('http://www.domain2.com:8080');// 连贯胜利解决socket.on('connect', function() { // 监听服务端音讯 socket.on('message', function(msg) { console.log('data from server: ---> ' + msg); }); // 监听服务端敞开 socket.on('disconnect', function() { console.log('Server socket has closed.'); });});document.getElementsByTagName('input')[0].onblur = function() { socket.send(this.value);};</script>
2)Nodejs socket后盾:
var http = require('http');var socket = require('socket.io');// 启http服务var server = http.createServer(function(req, res) { res.writeHead(200, { 'Content-type': 'text/html' }); res.end();});server.listen('8080');console.log('Server is running at port 8080...');// 监听socket连贯socket.listen(server).on('connection', function(client) { // 接管信息 client.on('message', function(msg) { client.send('hello:' + msg); console.log('data from client: ---> ' + msg); }); // 断开解决 client.on('disconnect', function() { console.log('Client socket has closed.'); });});
事件循环机制 (Event Loop)
事件循环机制从整体上通知了咱们 JavaScript 代码的执行程序 Event Loop
即事件循环,是指浏览器或Node
的一种解决javaScript
单线程运行时不会阻塞的一种机制,也就是咱们常常应用异步的原理。
先执行 Script 脚本,而后清空微工作队列,而后开始下一轮事件循环,持续先执行宏工作,再清空微工作队列,如此往返。
- 宏工作:Script/setTimeout/setInterval/setImmediate/ I/O / UI Rendering
- 微工作:process.nextTick()/Promise
上诉的 setTimeout 和 setInterval 等都是工作源,真正进入工作队列的是他们散发的工作。
优先级
- setTimeout = setInterval 一个队列
- setTimeout > setImmediate
- process.nextTick > Promise
for (const macroTask of macroTaskQueue) { handleMacroTask(); for (const microTask of microTaskQueue) { handleMicroTask(microTask); }}
代码输入后果
f = function() {return true;}; g = function() {return false;}; (function() { if (g() && [] == ![]) { f = function f() {return false;}; function g() {return true;} } })(); console.log(f());
输入后果: false
这里首先定义了两个变量f和g,咱们晓得变量是能够从新赋值的。前面是一个匿名自执行函数,在 if 条件中调用了函数 g(),因为在匿名函数中,又从新定义了函数g,就笼罩了内部定义的变量g,所以,这里调用的是外部函数 g 办法,返回为 true。第一个条件通过,进入第二个条件。
第二个条件是[] == ![],先看 ![] ,在 JavaScript 中,当用于布尔运算时,比方在这里,对象的非空援用被视为 true,空援用 null 则被视为 false。因为这里不是一个 null, 而是一个没有元素的数组,所以 [] 被视为 true, 而 ![] 的后果就是 false 了。当一个布尔值参加到条件运算的时候,true 会被看作 1, 而 false 会被看作 0。当初条件变成了 [] == 0 的问题了,当一个对象参加条件比拟的时候,它会被求值,求值的后果是数组成为一个字符串,[] 的后果就是 '' ,而 '' 会被当作 0 ,所以,条件成立。
两个条件都成立,所以会执行条件中的代码, f 在定义是没有应用var,所以他是一个全局变量。因而,这里会通过闭包拜访到内部的变量 f, 从新赋值,当初执行 f 函数返回值曾经成为 false 了。而 g 则不会有这个问题,这里是一个函数内定义的 g,不会影响到内部的 g 函数。所以最初的后果就是 false。
变量晋升
函数在运行的时候,会首先创立执行上下文,而后将执行上下文入栈,而后当此执行上下文处于栈顶时,开始运行执行上下文。
在创立执行上下文的过程中会做三件事:创立变量对象,创立作用域链,确定 this 指向,其中创立变量对象的过程中,首先会为 arguments 创立一个属性,值为 arguments,而后会扫码 function 函数申明,创立一个同名属性,值为函数的援用,接着会扫码 var 变量申明,创立一个同名属性,值为 undefined,这就是变量晋升。
代码输入后果
const first = () => (new Promise((resolve, reject) => { console.log(3); let p = new Promise((resolve, reject) => { console.log(7); setTimeout(() => { console.log(5); resolve(6); console.log(p) }, 0) resolve(1); }); resolve(2); p.then((arg) => { console.log(arg); });}));first().then((arg) => { console.log(arg);});console.log(4);
输入后果如下:
374125Promise{<resolved>: 1}
代码的执行过程如下:
- 首先会进入Promise,打印出3,之后进入上面的Promise,打印出7;
- 遇到了定时器,将其退出宏工作队列;
- 执行Promise p中的resolve,状态变为resolved,返回值为1;
- 执行Promise first中的resolve,状态变为resolved,返回值为2;
- 遇到p.then,将其退出微工作队列,遇到first().then,将其退出工作队列;
- 执行里面的代码,打印出4;
- 这样第一轮宏工作就执行完了,开始执行微工作队列中的工作,先后打印出1和2;
- 这样微工作就执行完了,开始执行下一轮宏工作,宏工作队列中有一个定时器,执行它,打印出5,因为执行曾经变为resolved状态,所以
resolve(6)
不会再执行; - 最初
console.log(p)
打印出Promise{<resolved>: 1}
;
如何判断一个对象是不是空对象?
Object.keys(obj).length === 0
手写题:在线编程,getUrlParams(url,key); 就是很简略的获取url的某个参数的问题,但要思考边界状况,多个返回值等等
代码输入后果
var a, b(function () { console.log(a); console.log(b); var a = (b = 3); console.log(a); console.log(b); })()console.log(a);console.log(b);
输入后果:
undefined undefined 3 3 undefined 3
这个题目和下面题目考查的知识点相似,b赋值为3,b此时是一个全局变量,而将3赋值给a,a是一个局部变量,所以最初打印的时候,a仍旧是undefined。
字符串模板
function render(template, data) { const reg = /\{\{(\w+)\}\}/; // 模板字符串正则 if (reg.test(template)) { // 判断模板里是否有模板字符串 const name = reg.exec(template)[1]; // 查找以后模板里第一个模板字符串的字段 template = template.replace(reg, data[name]); // 将第一个模板字符串渲染 return render(template, data); // 递归的渲染并返回渲染后的构造 } return template; // 如果模板没有模板字符串间接返回}
测试:
let template = '我是{{name}},年龄{{age}},性别{{sex}}';let person = { name: '布兰', age: 12}render(template, person); // 我是布兰,年龄12,性别undefined
数组扁平化
题目形容:实现一个办法使多维数组变成一维数组
最常见的递归版本如下:
function flatter(arr) { if (!arr.length) return; return arr.reduce( (pre, cur) => Array.isArray(cur) ? [...pre, ...flatter(cur)] : [...pre, cur], [] );}// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));
扩大思考:能用迭代的思路去实现吗?
实现代码如下:
function flatter(arr) { if (!arr.length) return; while (arr.some((item) => Array.isArray(item))) { arr = [].concat(...arr); } return arr;}// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));
代码输入后果
function runAsync (x) { const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000)) return p}Promise.all([runAsync(1), runAsync(2), runAsync(3)]).then(res => console.log(res))
输入后果如下:
123[1, 2, 3]
首先,定义了一个Promise,来异步执行函数runAsync,该函数传入一个值x,而后距离一秒后打印出这个x。
之后再应用Promise.all
来执行这个函数,执行的时候,看到一秒之后输入了1,2,3,同时输入了数组[1, 2, 3],三个函数是同步执行的,并且在一个回调函数中返回了所有的后果。并且后果和函数的执行程序是统一的。
let 闭包
let 会产生临时性死区,在以后的执行上下文中,会进行变量晋升,然而未被初始化,所以在执行上下文执行阶段,执行代码如果还没有执行到变量赋值,就援用此变量就会报错,此变量未初始化。
手写题:数组扁平化
function flatten(arr) { let result = []; for (let i = 0; i < arr.length; i++) { if (Array.isArray(arr[i])) { result = result.concat(flatten(arr[i])); } else { result = result.concat(arr[i]); } } return result;}const a = [1, [2, [3, 4]]];console.log(flatten(a));