问题集锦
1、 对闭包的理解,以及哪些地方用过闭包,以及闭包的缺点
function A() {
var a = '我可以在函数外部被访问';
b = function () {
console.log(a);
}
}
//一般形式
function A() {
var a = '我可以在函数外部被访问';
return function () {
console.log(a);
}
}
闭包要点:
1、函数声明的时候,会生成一个独立的作用域
2、同一作用域的对象可以互相访问
3、作用域呈层级包含状态,形成作用域链,子作用域的对象可以访问父作用域的对象,反之不能;另外子作用域会使用最近的父作用域的对象。
当一个函数调用时,引用了不是自己作用域内定义的变量,就形成了闭包;它始终对该变量保持引用,即使该变量所在函数执行完毕,变量也不会被垃圾回收机制回收,会一直保持在内存中,除非主动释放。
作用:
1、读取一个函数内部的变量。
2、让变量始终保持在内存中。
缺点:
1、闭包会使得函数中的局部变量都被保存在内存中,内存消耗很大。
2、闭包会在父函数外部,改变父函数内部变量的值。如果父函数还与其他函数有关联,就会间接产生影响。
2、 position 属性有哪些值,分别什么含义
1、static
position属性的默认值,元素以标准文档流排列,块级元素独占一行,行内元素从左到右依次排列,直到一行排满才会切换到下一行。
2、relative
当position属性设置为relative时,该元素相对于该元素在标准文档流中的位置进行定位,不会脱离标准文档流。
3、absolute
当position属性设置为absolute时,该元素相对于第一个position为非static属性的父元素进行定位。会脱离标准文档流。
4、fixed
当position属性设置为absolute时,该元素相对于window固定,滚动浏览器窗口并不会使其移动,会脱离标准文档流。
3、 const 和 let 区别,可以改变 const 定义的某个对象的属性吗
let与const都是只在声明所在的块级作用域内有效。
let声明的变量可以随意改变。
let不存在变量提升
let存在暂存性死区(先声明再使用)
let与const不允许重复声明
const是用来定义常量的,而且定义的时候必须初始化,且定义后不可以修改。
为什么引用类型可以修改?例子:
const xiaofang = {sex:fmale,age:18}
xiaofang.age = 19;
对象是引用类型的,xiaofang中保存的仅是对象的指针,这就意味着,const仅保证指针不发生改变,修改对象的属性不会改变对象的指针,所以是被允许的。也就是说const定义的引用类型只要指针不发生改变,其他的不论如何改变都是允许的。例如:
xiaofang = {sex:male,age:20}
这样相当于修改了指针,所以会报错。
4、事件循环机制
首先讲一下单线程:
javascript只有一个线程,即同一时间片段只能执行单个任务。这就意味着所有任务都需要排队,前一个任务执行完毕后才会执行下一个任务。如果前一个任务执行时间很长,后一个任务就要一直等着,这就会导致IO操作时造成性能的浪费。
如何解决单线程带来的问题:
答案是异步,主线程完全可以不管IO操作,暂时挂起等待中的任务,先运行排在后面的任务。等到IO操作返回了结果,再回过头,把挂起的任务继续执行下去。于是,任务可以分为两种,同步任务和异步任务。
事件循环:
同步和异步任务分别进入不同的执行“场所”,同步的进入主线程,一步的进入Event Table(事件列表)并注册回调函数。当指定的事情完成时,Event Table(事件列表)会将这个函数移入Event Queue(事件队列)。主线程的任务执行完毕,回去Event Queue读取对应的函数,进入主线程执行。上述过程会不断重复,也就是常说的事件循环。
let data = [];
$.ajax({
url:www.javascript.com,
data:data,
success:() => {
console.log('发送成功!');
}
})
console.log('代码执行结束');
上面是一段简易的ajax请求代码:
- ajax进入Event Table,注册回调函数success。
- 执行console.log(‘代码执行结束’)。
- ajax事件完成,回调函数success进入Event Queue。
- 主线程从Event Queue读取回调函数success并执行。
setTimeout
再看一个例子:
setTimeout(() => {
task()
},3000)
sleep(10000000)
- task()进入Event Table并注册,计时开始。
- 执行sleep函数,很慢,非常慢,计时仍在继续。
- 3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep也太慢了吧,还没执行完,只好等着。
- sleep终于执行完了,task()终于从Event Queue进入了主线程执行。
上述的流程走完,我们知道setTimeout这个函数,是经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。
我们还经常遇到setTimeout(fn,0)这样的代码,0秒后执行又是什么意思呢?是不是可以立即执行呢?
答案是不会的,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。
setInterval
上面说完了setTimeout,当然不能错过它的孪生兄弟setInterval。他俩差不多,只不过后者是循环的执行。对于执行顺序来说,setInterval会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。
唯一需要注意的一点是,对于setInterval(fn,ms)来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。(执行的时候还在往Event Queue里面推)。
Promise与process.nextTick(callback)
- macro-task(宏任务):包括整体代码script,setTimeout,setInterval
- micro-task(微任务):Promise,process.nextTick
不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。 Promise会进入另一个Event Queue。
事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。听起来有点绕,我们用文章最开始的一段代码说明:
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
resolve();
}).then(function() {
console.log('then');
})
console.log('console');
- 这段代码作为宏任务进入主线程。
- 遇到setTimeout将其放入Event Table中注册回调函数,然后立即放入宏任务Event Queue(因为这里没有延时,“事情”立马就做完了)。
- 遇到Promise,new Promise立即执行console.log,将then放入微任务Event Queue中。
- 执行console.log(‘console’)。
- 整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。
- 主线程执行完毕,从微任务队列取出then执行,再从宏任务队列取出setTimeout执行。
5、Dom事件
Dom事件流:
1. 捕获阶段:先由文档的根节点document往事件触发对象,从外向内捕获事件对象;
2. 目标阶段:到达目标事件位置(事发地),触发事件;
3. 冒泡阶段:再从目标事件位置往文档的根节点方向回溯,从内向外冒泡事件对象。
5.1 事件冒泡:
如果一个元素的事件被触发,那么它所有的父级元素的同名事件都会被触发。
注意点:只有当父级拥有同名事件的时候才会被触发.
5.2 事件捕获:
从最顶级的父元素一级一级往下找子元素触发同名事件,直到触发事件的元素为止.
注意点1:是去寻找与与父元素注册的同名事件的子元素。
注意点2:因为事件捕获,只能通过addEventListener并且参数写true才是事件捕获。
注意点3:其他情况都是冒泡(不是通过addEventListener添加、addEventListener参数为false)。
5.3 阻止捕获与冒泡:
1.阻止事件冒泡:点击一个元素只会触发当前元素的事件,不会触发父元素的同名事件
语法: 事件对象.stopPropagation() IE8及之前不支持
事件对象.cancelBubble = true IE8之前支持
注意:如果想要阻止事件冒泡,一定要在触发事件的函数中接收事件对象
2.阻止事件捕获:
事件对象.stopPropagation() 除了可以阻止冒泡还可以阻止捕获
5.4 事件委托:
利用的就是前面讲的冒泡原理,本该子元素做的事情委托给父元素去做。父元素可以判断事件源,然后让执行相关操作。
<div id="box">
<input type="button" id="add" value="添加" />
<input type="button" id="remove" value="删除" />
<input type="button" id="move" value="移动" />
<input type="button" id="select" value="选择" />
</div>
//js代码
window.onload = function(){
//父元素
var Box = document.getElementById("box");
Box.onclick = function (e) {
var e = e || window.event;
var target = e.target;
if(target.nodeName.toLocaleLowerCase() == 'input'){
switch(target.id){
case 'add' :
alert('添加');
break;
case 'remove' :
alert('删除');
break;
case 'move' :
alert('移动');
break;
case 'select' :
alert('选择');
break;
}
}
}
}
6、原型与原型链
new操作符做了什么?
function a(){}
var object = new a()
var obj = {};
obj.__proto__ = a.prototype
var result = a.call(obj);
if(typeof(result) == 'Object'){
return result'
} else {
return obj;
}
Object.create操作符做了什么?
var object = Object.create(o)
//可以传入对象或者函数
Object.create = function (obj) {
function F() {}
F.prototype = obj;
return new F();
//object沿着__proto__去查可以查到obj,即object.__proto__ = F.prototype = o;
};
原型对象
1、每一个函数对象都有一个prototype属性,prototype下面又有个construetor,指向这个函数。Func.prototype所对应的就是原型对象,原型对象的用途就是为通过构造函数实例化的对象提供共有的方法和属性。
2、每个对象都有一个名为_proto_的内部属性,指向它所对应的构造函数的原型对象,即Func.prototype = obj._proto_。
原型链
原型链是由原型对象组成的链(要先讲一下原型对象,将原型对象与原型链放在一起讲)
当我们访问一个对象属性时,如果这个对象不存在就会去__proto__ 指向的原型对象里面找,一层一层找下去,找到后返回,找不到返回undefined(Object.prototype._proto_.prototype)。Object.prototype在原型链最顶端,他的__proto__属性为null。
[外链图片转存失败(img-Gk07pvYr-1569200701224)(http://cdn8.sm-tc.cn/?src=l4uLj4zF0NCWkp6YmozNz87H0ZyRnZOQmIzRnJCS0J2TkJjQzs3JyszGydDNz87Izs7Qzs3JyszGydLNz87Izs7NyM/HzcfNzs/JytLOys/Jy8nGzsrK0Y%2bRmA==&restype=3&from=derive&pi=&v=1)]
function Person(name){
this.name = name;
}
var p = new Person();
//p ---> Person.prototype --->Object.prototype---->null
JS中所有的东西都是对象,所有的东西都由Object衍生而来, 即所有东西原型链的终点指向null。
原型链实现继承
原型链继承是实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针,实例都包含一个指向原型对象的内部指针。如果让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着指向另一个构造函数的指针。加入另一个原型有事另一个类型的实例,那么上述关系依然确立,如此层层递进,就构成了实例与原型的链条。这就是所谓的原型链。
prototype和 proto 的概念(区别)
prototype是函数的一个属性(每个函数都有一个prototype属性),这个属性是一个指针,指向一个对象,就是原型对象,该对象中包括了一些实例对象共有的属性和方法。
_ proto_ 是一个对象拥有的内置属性(请注意:prototype是函数的内置属性,_ proto_ 是对象的内置属性),_ proto_ 是JS内部使用寻找原型链的属性。简单来说,在 javascript 中每个对象都会有一个 _ proto _ 属性,当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去 _ proto _ 里找这个属性,这个 _ proto _ 又会有自己的 _ proto _,于是就这样一直找下去,也就是我们平时所说的原型链的概念。
prototype用来设置实例对象的属性和方法,__proto__用来进行原型链属性查找。
A instanceof B的实现原理
function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
var O = R.prototype; // 取 R 的显示原型
L = L.__proto__; // 取 L 的隐式原型
while (true) {
if (L === null)
return false;
if (O === L) // 当 O 显式原型 严格等于 L隐式原型 时,返回true
return true;
L = L.__proto__;
}
}
检测a的原型链(proto)上是否有B.prototype,若有返回true,否则false。规则简单来说就是 L的 proto 是不是强等于 R.prototype,不等于再找 L.proto .proto 直到 proto 为 null
7、es6 Promise的链式调用
下面通过样例作为演示,我们定义做饭、吃饭、洗碗(cook、eat、wash)这三个方法,它们是层层依赖的关系,下一步的的操作需要使用上一部操作的结果。(这里使用 setTimeout 模拟异步操作)
//做饭
function cook(){
console.log('开始做饭。');
var p = new Promise(function(resolve, reject){ //做一些异步操作
setTimeout(function(){
console.log('做饭完毕!');
resolve('鸡蛋炒饭');
}, 1000);
});
return p;
}
//吃饭
function eat(data){
console.log('开始吃饭:' + data);
var p = new Promise(function(resolve, reject){ //做一些异步操作
setTimeout(function(){
console.log('吃饭完毕!');
resolve('一块碗和一双筷子');
}, 2000);
});
return p;
}
function wash(data){
console.log('开始洗碗:' + data);
var p = new Promise(function(resolve, reject){ //做一些异步操作
setTimeout(function(){
console.log('洗碗完毕!');
resolve('干净的碗筷');
}, 2000);
});
return p;
}
cook()
.then(function(data){
return eat(data);
})
.then(function(data){
return wash(data);
})
.then(function(data){
console.log(data);
});
cook()
.then(eat)
.then(wash)
.then(function(data){
console.log(data);
});
解释:上述构造方法中的两个参数resolve, reject即是改变promise的状态,resolve 方法把 Promise 的状态置为完成态(Resolved),这时 then 方法就能捕捉到变化,并执行“成功”情况的回调,resolve, reject可抛出结果,作为then方法中函数的参数。then可接受两个参数,第一个处理Resolved状态的函数,第二个处理Rejected函数。如果只想处理成功,或者失败,对应参数可设置为null,只有一个参数则表示状态改变就执行,不区分状态结果。
另外
catch()方法,它可以和 then 的第二个参数一样,用来指定 reject 的回调,另一个作用是,当执行 resolve 的回调(也就是上面 then 中的第一个参数)时,如果抛出异常了(代码出错了),那么也不会报错卡死 js,而是会进到这个 catch 方法中。
非Promise实现链式调用的方法:
var Chain = function () {};
Chain.prototype = {
c: function () { return this; },
b: function () { return this; }
};
var a = function () {
return new Chain();
};
a().b().c()
8、数组方法汇总
- forEach与map
let arr = [1, 2, 3, 4, 5];
arr.forEach((num, index) => {
return arr[index] = num * 2;
});
//undefined 无返回值 可改变原数组
let arr = [1, 2, 3, 4, 5];
arr.forEach((num, index) => {
return num * 2;
});
//undefined 无返回值 不可改变原数组
forEach执行过程中无法被打断
let doubled = arr.map(num => {
return arr[index] = num * 2;
});
//[2,4,6,8,10] 有返回值 可改变原数组
let doubled = arr.map(num => {
return num * 2;
});
//[2,4,6,8,10] 有返回值 不可改变原数组
- 其他方法
Array.isArray() //判断变量是否为数组,弥补了typeof的不足
valueOf() //输出原数组
toString() //将数组转化为字符串,每一项用“,”隔开
push() //在数组后添加一个元素,返回新数组长度
pop() //删除数组最后一个元素,返回删除的元素
shift() //删除数组第一个元素,返回删除后的元素
unshift() //在数组头部插入元素,返回新数组长度
join()
//以指定的参数作为分隔符,将所有数组成员连接为一个字符串返回,如果不提供参数,默认用逗号分隔
//如果数组成员是undefined或null或空位,会被转化为空字符串
//返回拼接后字符串
concat() //concat用于将多个新数组添加到原数组后,组成一个新数组返回,原数组不变
reverse() //reverse方法用于颠倒排列数组元素,返回改变后的数组。注意,该方法将改变原数组
slice()
//slice方法用于提取目标数组的一部分,返回一个新数组,原数组不变。
//slice(start,end) 包不包括临界值如下[start,end)
//没有参数的情况下返回原数组的拷贝
//一个参数时,end为数组.length。
//slice方法的重要应用:将类似数组的对象转化为真正的数组。
//Array.prototype.slice.call(arguments);
splice()
//splice方法用于删除原数组的一部分成员,并可以在删除的位置添加新的数组成员,返回值是被删除的元素。注意,该方法会改变原数组
//splice(start,count,addElement1,addElement2)
//能否改变原数组:能
//返回值:返回值是被删除的元素
sort()
//sort方法对数组成员进行排序,默认是按照字典顺序排序,排序后,原数组将改变。sort可以按照自定义的方式排序,可以传入一个函数作为参数,这个函数拥有两个参数,如果返回值大于0,表示第一个成员排在第二个成员后面,其他情况下,都是第一个元素排在第二个元素前面。
//能否改变原数组:能
//返回值:返回值为排序后的数组
fliter()
//filter用于过滤数组成员,满足条件的成员组成一个新数组返回。它的参数是一个函数(函数的参数为elem,index,arr—–依次是当前值,当前位置,整个数组),所有数组成员依次执行该函数,返回结果为true的成员组成一个新数组返回,该方法不会改变原数组。
//能否改变原数组:能不能也取决于操作,毕竟参数函数的参数中有整个数组这个参数。
//返回值:返回值为能使参数函数返回true的数组成员。
some()和every()
//这两个方法类似‘断言’,返回一个布尔值,表示判断数组成员是否符合某种条件。
//他们接受一个函数作为参数,所有数组成员,一次执行该函数,该函数接受三个参数:当前成员,当前位置和整个数组,然后返回一个布尔值。
//some方法只要一个成员返回值是true,则整个some方法返回值就是true,否则返回false。
//那么every方法就是所有成员都返回true,整个every方法才返回true,否则返回false
//能否改变原数组:不能
//返回值:true||false
indexOf与lastIndexOf()
//indexOf方法返回给定元素在数组中第一次出现的位置,如果没有出现则返回-1。
//indexOf方法还可以接受第二个参数,表示搜索的开始位置。
//lastIndexOf方法返回给定元素在数组中最后一次出现的位置,如果没有出现则返回-1。
//这两个方法不能用来搜索NaN的位置,即它们无法确定数组成员是否包含NaN。
但是:es6中的新方法,Array.find()和ArrayfindIndex()都可以找到NaN。
reduce()用法详解
函数形式:arr.reduce(function(prev,cur,index,arr){
...
}, init);
var sum = arr.reduce(function (prev, cur) {
return prev + cur;
},0);
2. 求数组项最大值
var max = arr.reduce(function (prev, cur) {
return Math.max(prev,cur);
});
3. 数组去重
var newArr = arr.reduce(function (prev, cur) {
prev.indexOf(cur) === -1 && prev.push(cur);
return prev;
},[]);
9、剩余参数rest
我们知道JS函数内部有个arguments对象,可以拿到全部实参。现在ES6给我们带来了一个新的对象,可以拿到除开始参数外的参数,即剩余参数。
这个新的对象和arguments不一样,它是程序员自定义的一个普通标识符,只是需要在前面加上三个点:…
function func(a, ...rest) {
console.log(a)
console.log(rest)
}
func(1) //1 []
func(1, 2, 3, 4) //1 [2,3,4]
剩余参数嘛,所以后面就不要再跟其它的参数了,不然会报错
function func(a, ...rest, b) {
}
剩余操作符和扩展运算符相反,扩展运算符会“展开”数组变成多个元素,剩余操作符会收集多个元素和“压缩”成一个单一的元素。
扩展运算和rest参数注意
对于三个点号...,三点放在形参或者等号左边为rest运算符; 放在实参或者等号右边为spread运算符,或者说,放在被赋值一方为rest运算符,放在赋值一方为扩展运算符。
10、HTML中的attribute属性和JavaScript中的property属性(基础)
基本概念区别
attribute:是HTML标签上的某个属性,如id、class、value等以及自定义属性,它的值只能是字符串,关于这个属性一共有三个相关的方法,setAttribute、getAttribute、removeAttribute;
注意:在使用setAttribute的时候,该函数一定接收两个参数,setAttribute(attributeName,value),无论value的值是什么类型都会编译为字符串类型。在html标签中添加属性,本质上是跟在标签里面写属性时一样的,所以属性值最终都会编译为字符串类型。
property:是js获取的DOM对象上的属性值,比如a,你可以将它看作为一个基本的js对象。这个节点包括很多property,比如value,className以及一些方法onclik等方法。
一个js对象有很多property,该集合名字为properties,properties里面有其他property以及attributies,attributies里面有很多attribute。
而常用的attribute,id、class、name等一般都会作为property附加到js对象上,可以和property一样取值、赋值。
11、flex-grow、flex-shrink、flex-basis三兄弟
首先是 flex-basis ,basis英文意思是<主要成分>,所以他和width放在一起时,肯定把width干掉,basis遇到width时就会说我才是最主要的成分,你是次要成分,所以见到我的时候你要靠边站。
其次是 flex-grow,grow英文意思是<扩大,扩展,增加>,这就代表当父元素的宽度大于子元素宽度之和时,并且父元素有剩余,这时,flex-grow就会说我要成长,我要长大,怎么样才能成长呢,当然是分享父元素的空间了。见下面第二个属性的内容
最后是 flex-shrink, shrink英文意思是<收缩,>,这就代表当父元素的宽度小于子元素宽度之和时,并且超出了父元素的宽度,这时,flex-shrink就会说外面的世界太苦了,我还是回到父亲的怀抱中去吧!因此,flex-shrink就会按照一定的比例进行收缩。见下面第三个属性的内容
第一个属性:flex-basis
该属性用来设置元素的宽度,其实,width也可以设置宽度。如果元素上同时设置了width和flex-basis,那么width 的值就会被flex-basis覆盖掉。
<style type="text/css" media="screen">
.box{
display: flex;
margin:100px auto;
width:400px;
height:200px;
}
.inner{
width:200px;
height:100px;
flex-basis:300px;
background:pink;
}
</style>
</head>
<body>
<div class="box">
<div class="inner">
</div>
</div>
</body>
见下图:小编我把宽度设置为width:200px; flex-basis:300px;结果显示表明子元素.inner应用了属性flex-basis;
第二个属性:flex-grow
该属性用来设置当父元素的宽度大于所有子元素的宽度的和时(即父元素会有剩余空间),子元素如何分配父元素的剩余空间。 flex-grow的默认值为0,意思是该元素不索取父元素的剩余空间,如果值大于0,表示索取。值越大,索取的越厉害。
举个例子: 父元素宽400px,有两个子元素:A和B。A宽为100px,B宽为200px。 则空余空间为 400-(100+200)= 100px。 如果A,B都不索取剩余空间,则有100px的空余空间。
<body>
<div class="box">
<div class="inner">
</div>
<div class="inner1">
</div>
</div>
</body>
.box{
display: flex;
flex-direction: row;
margin:100px auto;
width:400px;
height:200px;
border:1px solid red;
}
.inner{
flex-basis:100px;
height:100px;
background:pink;
}
.inner1{
flex-basis:200px;
height:100px;
background:blue;
}
见下图:
如果A索取剩余空间:设置flex-grow为1,B不索取。则最终A的大小为 自身宽度(100px)+ 剩余空间的宽度(100px)= 200px 。
.inner{
flex-basis:100px;
height:100px;
background:pink;
flex-grow:1;
}
.inner1{
flex-basis:200px;
height:100px;
background:blue;
}
见下图:
如果A,B都设索取剩余空间,A设置flex-grow为1,B设置flex-grow为2。则最终A的大小为 自身宽度(100px)+ A获得的剩余空间的宽度(100px (1/(1+2))),最终B的大小为 自身宽度(200px)+ B获得的剩余空间的宽度(100px (2/(1+2)))(这里呢小编只给了公式,小伙伴们可以自己去算一下)
.inner{
flex-basis:100px;
height:100px;
background:pink;
flex-grow:1;
}
.inner1{
flex-basis:200px;
height:100px;
background:blue;
flex-grow:2;
}
第三个属性:flex-shrink
该属性用来设置,当父元素的宽度小于所有子元素的宽度的和时(即子元素会超出父元素),子元素如何缩小自己的宽度的。 flex-shrink的默认值为1,当父元素的宽度小于所有子元素的宽度的和时,子元素的宽度会减小。值越大,减小的越厉害。如果值为0,表示不减小。
举个例子: 父元素宽400px,有两子元素:A和B。A宽为200px,B宽为300px。 则A,B总共超出父元素的宽度为(200+300)- 400 = 100px。 如果A,B都不减小宽度,即都设置flex-shrink为0,则会有100px的宽度超出父元素。
.box{
display: flex;
flex-direction: row;
margin:100px auto;
width:400px;
height:200px;
border:1px solid red;
}
.inner{
flex-basis:200px;
height:100px;
background:black;
flex-shrink:0;
}
.inner1{
flex-basis:300px;
height:100px;
background:blue;
flex-shrink:0;
}
如果A不减小宽度:设置flex-shrink为0,B减小。则最终B的大小为 自身宽度(300px)- 总共超出父元素的宽度(100px)= 200px
.inner{
flex-basis:200px;
height:100px;
background:black;
flex-shrink:0;
}
.inner1{
flex-basis:300px;
height:100px;
background:blue;
flex-shrink:1;
}
如果A,B都减小宽度,A设置flex-shirk为3,B设置flex-shirk为2。则最终A的大小为 自身宽度(200px)- A减小的宽度(100px * (200px * 3/(200 * 3 + 300 * 2))) = 150px,最终B的大小为 自身宽度(300px)- B减小的宽度(100px * (300px * 2/(200 * 3 + 300 * 2))) = 250px
.inner{
flex-basis:200px;
height:100px;
background:black;
flex-shrink:3;
}
.inner1{
flex-basis:300px;
height:100px;
background:blue;
flex-shrink:2;
}
.item {flex: 1;}
.item {flex-grow: 1; flex-shrink: 1; flex-basis: 0%;}
最后,常见的flex:1代表占满剩余空间。
12、伪类与伪元素
伪类:
伪类用于当已有元素处于的某个状态时,为其添加对应的样式,这个状态是根据用户行为而动态变化的。比如说,当用户悬停在指定的元素时,我们可以通过:hover来描述这个元素的状态。虽然它和普通的css类相似,可以为已有的元素添加样式,但是它只有处于dom树无法描述的状态下才能为元素添加样式,所以将其称为伪类。
伪元素:
伪元素用于创建一些不在文档树中的元素,并为其添加样式。比如说,我们可以通过:before来在一个元素前增加一些文本,并为这些文本添加样式。虽然用户可以看到这些文本,但是这些文本实际上不在文档树中。
13、Promise中return、resolve、reject、then、catch之间的关系
执行了resolve()后一定进入then的第一个回调函数中,肯定不会进入catch
var p1=new Promise((resolve,rej) => {
console.log('resolve')
//throw new Error('手动返回错误')
resolve('成功了')
})
p1.then(data =>{
console.log('data::',data);
}).catch(
res => {
console.log('catch data::', res)
})
结果:
resolve
data:: 成功了
不会进入catch的情况
var p1=new Promise((resolve,rej) => {
console.log('resolve')
//throw new Error('手动返回错误')
resolve('成功了')
})
p1.catch(
res => {
console.log('catch data::', res)
})
结果:
resolve
reject后的东西,一定会进入then中的第二个回调,如果then中没有写第二个回调,则进入catch。第二个回调函数无法捕获错误,错误只能被catch捕获。
var p1=new Promise((resolve,rej) => {
console.log('没有resolve')
//throw new Error('手动返回错误')
rej('失败了')
})
p1.then(data =>{
console.log('data::',data);
},err=> {
console.log('err::',err)
}).catch(
res => {
console.log('catch data::', res)
})
结果:
没有resolve
err:: 失败了
then中没有第二个回调的情况
var p1=new Promise((resolve,rej) => {
console.log('没有resolve')
//throw new Error('手动返回错误')
rej('失败了')
})
p1.then(data =>{
console.log('data::',data);
}).catch(
res => {
console.log('catch data::', res)
})
结果:
没有resolve
catch data:: 失败了
如果没有then, 也可以直接进入catch
var p1=new Promise((resolve,rej) => {
console.log('没有 resolve')
//throw new Error('手动返回错误')
rej('失败了')
})
p1.catch(
res => {
console.log('catch data::', res)
})
结果:
没有resolve
catch data:: 失败了
- throw new Error 的情况和rej一样,但是他俩只会有一个发生
- 另外,网络异常(比如断网),会直接进入catch而不会进入then的第二个回调
- return 是为了实现链式调用,只能出现在then里面,resolve()不能出现在then里面
var p = new Promise(function(resolve, reject){
resolve(1);
});
p.then(function(value){ //第一个then
console.log(value);
return value*2;
}).then(function(value){ //第二个then
console.log(value);
}).then(function(value){ //第三个then
console.log(value);
return Promise.resolve('resolve');
}).then(function(value){ //第四个then
console.log(value);
return Promise.reject('reject');
}).then(function(value){ //第五个then
console.log('resolve: '+ value);
}, function(err){
console.log('reject: ' + err);
})
14、提交表单的方式
无刷新页面提交表单
表单可实现无刷新页面提交,无需页面跳转,如下,通过一个隐藏的iframe实现,form表单的target设置为iframe的name名称,
form提交目标位当前页面iframe则不会刷新页面
<form action="/url.do" method="post" target="targetIfr">
<input type="text" name="name"/>
</form>
<iframe name="targetIfr" style="display:none"></iframe>
通过type=submit提交
一般表单提交通过type=submit实现,input type=“submit”,浏览器显示为button按钮,通过点击这个按钮提交表单数据跳转到/url.do
<form action="/url.do" method="post">
<input type="text" name="name"/>
<input type="submit" value="提交">
</form>
js提交form表单
js事件触发表单提交,通过button、链接等触发事件,js调用submit()方法提交表单数据,jquery通过submit()方法
<form id="form" action="/url.do" method="post">
<input type="text" name="name"/>
</form>
js: document.getElementById(“form”).submit();
jquery: $("#form").submit();
ajax异步提交表单数据
采用ajax异步方式,通过js获取form中所有input、select等组件的值,将这些值组成Json格式,通过异步的方式与服务器端进行交互,
一般将表单数据传送给服务器端,服务器端处理数据并返回结果信息等
<form id="form" method="post">
<input type="text" name="name" id="name"/>
</form>
var params = {"name", $("#name").val()}
$.ajax({
type: "POST",
url: "/url.do",
data: params,
dataType : "json",
success: function(respMsg){
}
});
页面无跳转
如果通过form表单提交请求服务端去下载文件,这时当前页面不会发生跳转,服务端返回void,通过response 去写文件数据,
页面会显示下载文件。
<form action="/url.do" method="post">
<input type="text" name="name"/>
<input type="submit" value="提交">
</form>
@RequestMapping(value = "/url")
public void exportFile(HttpServletRequest req, HttpServletResponse response, String rptId)
throws Exception {
OutputStream out = null;
try {
String rptName = "file";
String fileName = new String((rptName + excelAble.getFileSuffix()).getBytes("GBK"),
"8859_1");
response.reset();
response.setContentType("application/octec-stream");
response.setHeader("Content-disposition", "attachment; filename=" + fileName);
out = response.getOutputStream();
excelAble.exportFile(out);
} catch (Exception e) {
logger.error(e);
} finally {
if (out != null) {
out.close();
}
}
}
form表单上传文件
使用form表单进行上传文件需要为form添加enctype=“multipart/form-data” 属性,除此之外还需要将表单的提交方法改成post,
如下 method=“post”, input type的类型需要设置为file
<form action="/url.do" enctype="multipart/form-data" method="post">
<input type="file" name="name"/>
<input type="submit" value="提交">
</form>
15、函数防抖和函数节流
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
body{
height: 5000px;
}
</style>
</head>
<body>
<script>
//节流函数 定时器版本
function throttle(func, wait) {
let timeout;
return function() {
let context = this;
let args = arguments;
if (!timeout) {
timeout = setTimeout(() => {
timeout = null; //如果不清空下次无法进入
func.apply(context, args)
}, wait)
}
}
}
//节流函数 时间戳版本
function throttle(func, delay) {
var prev = Date.now();
return function() {
var context = this;
var args = arguments;
var now = Date.now();
if (now - prev >= delay) {
func.apply(context, args);
prev = Date.now();
}
}
}
//防抖函数
function debounce(func, wait) {
var timeout;
return function() {
var context = this;
var args = arguments;
if(timeout)
clearTimeout(timeout);
timeout = setTimeout(function(){func.apply(context, args)}, wait);
}
}
// 处理函数
function handle() {
console.log(Math.random());
}
// 滚动事件
window.addEventListener('scroll', debounce(handle, 1000));
</script>
</body>
</html>
函数防抖:将几次操作合并为一此操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
函数节流:使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。
区别: 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。
16、跨域的方式及其原理
通过XHR对象实现Ajax通信的一个主要限制,来源于跨域安全策略。默认情况下,XHR对象只能访问与包含他的页面位于同一域中的资源。
同源策略:所谓的同源,指的是协议,域名,端口相同。浏览器出于安全方面的考虑,只允许本域名下的接口交互,不同源的客户端脚本,在没有明确授权的情况下,不能读写对方的资源。
<script>、<img>、<iframe>、<link>这些包含 src 属性的标签可以加载跨域资源。但是通过src加载的资源,浏览器限制了javascript的权限,不能进行各种的读写。从而,即使请求发了,敏感数据回来了,也是取不到的。
jsonp跨域
原理:html标签中存在一些如img、script等标签球可以通过src属性进行资源的请求。JSONP就是通过script标签来进行跨域资源的请求的。当发起JSONP请求的时候,服务器就会返回一段js脚本,请求方通过返回的该段脚本就可以获得请求的资源了。
例子:
如:http://localhost:8080/webcontent/test.html页面存在
一个 http ://localhost:8888/jsonp/test?callback=Test 的请求,那么服务器就会返回Test({“test”:”this is json”})的字符串,该字符串可看做是一个名为Test的function,其参数为一个json object。当调用方得到该字符串时就会自动执行Test方法,来进行后续的处理。
调用方的JS代码:
<script>
function Test(i) {// 该方法会在结果返回时被自动调用
console.log(i.test);
console.log(JSON.stringify(i));
}
</script>
<script src="http://127.0.0.1:8888/SpringmvcDemo/jsonp/msg?callBack=Test"></script>
服务器端代码:
@Controller
@RequestMapping("/jsonp")
public class JsonpTestController {
@ResponseBody
@RequestMapping("/msg")
public String responseJsonp(HttpServletRequest request,
@RequestParam String callBack) {
String testJson = "{\"test\":\"this is json\"}";
String jsonp = callBack + "(" + testJson + ");";
return jsonp;
}
}
若直接请求服务器,会返回如下结果:
JSONP跨域请求返回的结果(开发者工具控制台)
通过以上演示可以看出,jsonp其实就是json外面包裹一个回调函数,通过该回掉函数就可以对json信息进行后续的处理解析。
在实际项目中通常使用JQUERY进行JSONP调用
$.ajax({
async:false,
type:'get',
url: 'http://127.0.0.1:8888/SpringmvcDemo/jsonp/msg',
cache : false,
dataType: "jsonp",
success: function(data) {
// 进行相应处理
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
alert(errorThrown);
}
});
JQUERY会在请求的Url后面加上callback=动态生成的回调函数名,服务器返回的数据就是生成的动态回调函数包裹json。JQUERY会默认使用callback,当然也可以指定callback为其他值,也可以指定回调函数名。
JQuery Ajax Jsonp原理
后端要配合使用jsonp,那么首先得了解Jquery Ajax jsonp的一个特点:
Jquery在发送一个Ajax jsonp请求时,会在访问链接的后面自动加上一个验证参数,这个参数是Jquery随机生成的,例如链接
http://www.deardull.com:9090/getMySeat?callback=jQuery31106628680598769732_1512186387045&=1512186387046
中,参数callback=jQuery31106628680598769732_1512186387045&=1512186387046就是jquery自动添加的。
添加这个参数的目的是唯一标识这次请求。当服务器端接收到该请求时,需要将该参数的值与实际要返回的json值进行构造(如何构造下面讲解),并且返回,而前端会验证这个参数,如果是它之前发出的参数,那么就会接收并解析数据,如果不是这个参数,那么就拒绝接受。
需要特别注意的是这个验证参数的名字,这个名字来源于前端的jsonp参数的值。如果把前端jsonp参数的值改为“aaa”,那么相应的参数就应该是 aaa=jQuery31106628680598769732_1512186387045&_=1512186387046
注意点:
- 前端注意与后端沟通约定jsonp的值,通常默认都是用callback。
- 后端根据jsonp参数名获取到参数后要与本来要返回的json数据按“callback(json)”的方式构造。
JSONP很方便的解决了跨域问题,但是由于本身只能采用get方式进行请求,所以在请求参数过多的情况下就无法使用了。
Cors跨域
CORS:全称"跨域资源共享"(Cross-origin resource sharing)
CORS分为简单请求和非简单请求(需预检请求)两类
符合以下条件的,为简单请求
请求方式使用下列方法之一:
GET
HEAD
POST
Content-Type 的值仅限于下列三者之一:
text/plain
multipart/form-data
application/x-www-form-urlencoded
对于简单请求,浏览器会直接发送CORS请求,具体说来就是在header中加入origin请求头字段。同样,在响应头中,返回服务器设置的相关CORS头部字段,Access-Control-Allow-Origin字段为允许跨域请求的源。请求时浏览器在请求头的Origin中说明请求的源,服务器收到后发现允许该源跨域请求,则会成功返回,具体如下:
在这里,http://localhost:3001为我们当前发送请求的源,如果服务器发现请求在指定的源范围内,则会返回响应的请求结果, 否则会在控制台报错,如下图所示,在这里需要注意的是,尽管请求失败,但返回的状态码依然可能为200。所以在做处理时需要格外注意。
非简单请求(预检请求)
使用了下面任一 HTTP 方法:
PUT
DELETE
CONNECT
OPTIONS
TRACE
PATCH
Content-Type 的值不属于下列之一:
application/x-www-form-urlencoded
multipart/form-data
text/plain
当发生符合非简单请求(预检请求)的条件时,浏览器会自动先发送一个options请求,如果发现服务器支持该请求,则会将真正的请求发送到后端,反之,如果浏览器发现服务端并不支持该请求,则会在控制台抛出错误,如下:
如果非简单请求(预检请求)发送成功,则会在头部多返回以下字段
Access-Control-Allow-Origin: http://localhost:3001 //该字段表明可供那个源跨域
Access-Control-Allow-Methods: GET, POST, PUT // 该字段表明服务端支持的请求方法
Access-Control-Allow-Headers: X-Custom-Header // 实际请求将携带的自定义请求首部字段
下图为一个预检请求实例:
红框标注的为预检请求发送的查询,服务端支持我们的请求后,将会发送我们真正的请求,图中绿框所示。可以看到,真正的请求响应头字段多处蓝框中所圈字段。这为服务器所支持cors请求类型和允许的自定义请求首部字段,以及支持跨域的源。
前端代码与发送普通请求没有差异,我们只需在服务端设置即可,以node为例:
var express = require('express');
var app = express();
var allowCrossDomain = function (req, res, next) {
res.header('Access-Control-Allow-Origin', 'http://localhost:3001');
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type');
next();
}
app.use(allowCrossDomain);
postMessage
如果两个网页不同源,就无法拿到对方的DOM。典型的例子是iframe窗口和window.open方法打开的窗口,它们与父窗口无法通信。HTML5为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即"协议 + 域名 + 端口"。也可以设为*,表示不限制域名,向所有窗口发送。
window.postMessage(msg,targetOrigin)
发送信息页面 http://localhost:63342/index.html
<html lang="en">
<head>
<meta charset="UTF-8">
<title>跨域请求</title>
</head>
<body>
<iframe src="http://localhost:3000/users/reg" id="frm"></iframe>
<input type="button" value="OK" onclick="run()">
</body>
</html>
<script>
function run(){
var frm=document.getElementById("frm");
frm.contentWindow.postMessage("跨域请求信息","http://localhost:3000"); }
</script>
接收信息页面 http://localhost:3000/message.html
window.addEventListener("message",function(e){
//通过监听message事件,可以监听对方发送的消息。
console.log(e.data);
},false);
document.domain
前提条件:这两个域名必须属于同一个基础域名!而且所用的协议,端口都要一致,否则无法利用document.domain进行跨域
我们可以给document.domain属性赋值,不过是有限制的,你只能赋成当前的域名或者基础域名。比如:
www.haha.com
javascript:alert(document.domain = "haha.com"); //haha.com
javascript:alert(document.domain = "www.haha.com"); //www.haha.com
上面的赋值都是成功的,因为www.jb51.net是当前的域名,而jb51.net是基础域名。
aaa里的一个网页(a.html)引入了bbb 里的一个网页(b.html),这时a.html里同样是不能操作b.html里面的内容的。因为document.domain不一样,一个是aaa.xxx.com,另一个是bbb.xxx.com。
这时我们就可以通过Javascript,将两个页面的domain改成一样的,需要在a.html里与b.html里都加入:
document.domain = "xxx.com";
websocket
Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
前端代码:
<div>user input:<input type="text"></div>
<script src="./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>
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.'); }); });
17、transform的使用方法
transform:rotate():
含义:旋转;其中“deg”是“度”的意思,如“10deg”表示“10度”下同。
共一个参数“角度”,单位deg为度的意思,正数为顺时针旋转,负数为逆时针旋转,上述代码作用是顺时针旋转45度。
transform:skew():
含义:倾斜;
参数表示倾斜角度,单位deg
一个参数时:表示水平方向的倾斜角度;
两个参数时:第一个参数表示水平方向的倾斜角度,第二个参数表示垂直方向的倾斜角度。
transform:scale():
含义:比例;“1.5”表示以1.5的比例放大,如果要放大2倍,须写成“2.0”,缩小则为负“-”。
一个参数时:表示水平和垂直同时缩放该倍率
两个参数时:第一个参数指定水平方向的缩放倍率,第二个参数指定垂直方向的缩放倍率。
transform:translate():
含义:变动,位移;如下表示向右位移120像素,如果向上位移,把后面的“0”改个值就行,向左向下位移则为负“-”。
一个参数时:表示水平方向的移动距离;
两个参数时:第一个参数表示水平方向的移动距离,第二个参数表示垂直方向的移动距离。
18、函数柯里化
概念:
是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下参数而且返回结果的新函数。
只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
// 普通的add函数
function add(x, y) {
return x + y
}
// Currying后
function curryingAdd(x) {
return function (y) {
return x + y
}
}
add(1, 2) // 3
curryingAdd(1)(2) // 3
函数柯里化的好处:
1.参数复用
// 正常正则验证字符串 reg.test(txt)
// 函数封装后
function check(reg, txt) {
return reg.test(txt)
}
check(/\d+/g, 'test') //false
check(/[a-z]+/g, 'test') //true
// Currying后
function curryingCheck(reg) {
return function(txt) {
return reg.test(txt)
}
}
var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)
hasNumber('test1') // true
hasNumber('testtest') // false
hasLetter('21212') // false
上面的示例是一个正则的校验,正常来说直接调用check函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数reg进行复用,这样别的地方就能够直接调用hasNumber,hasLetter等函数,让参数能够复用,调用起来也更方便。
2.延迟执行
Function.prototype.bind = function (context) {
var _this = this
var args = Array.prototype.slice.call(arguments, 1)
return function() {
return _this.apply(context, args)
}
}
像我们js中经常使用的bind,实现的机制就是Currying.
这里再讲以下,网上的那一道和柯里化相关的面试题,以及一个关于函数内置的toString函数的知识点。
写一个函数实现
add(1)(2)(3) // 6
// 实现方法
var add = function(a) {
return function(b) {
return funciton(c) {
return a+b+c;
}
}
}
add(1)(2)(3);
上面这种写法是最简单的实现方法,但是缺点显而易见,只能实现单一一种情况,如果传入的参数不定,那么就无法使用这种方法,这里附上一种写法,可以适应多个参数的情况。
var add = function(a) {
var sum = a;
var addMore = function(b) {
sum += b;
return addMore;
};
addMore.toString = function() {
return sum;
};
return addMore;
};
var a = add(1)(2)(3)(4).toString(); //10
这里需要提一下,如果是单纯的,
var a = add(1)(2)(3)(4);
这样输出的是一个函数式
{ [Function: addMore] toString: [Function] }
必须得转化以下,才能够输出数字。另外,还有一种情况:
var add = function(a) {
var sum = a;
var addMore = function(b) {
sum += b;
return addMore;
};
addMore.toString = function() {
return sum;
};
return addMore;
};
var a = add(1)(2)(3)(4)+10; //20
也就是说,当没有定义toString时,函数的隐式转换会调用默认的toString方法,它会将函数的定义内容作为字符串返回。而当我们自定义了toString方法时,那么隐式转换的返回结果则由我们自己控制了。另外,vauleOf 的方法的作用和 toString 方法一样,但是valueOf会比toString后执行。
add (2,3) arguments.length == 2
add (2)(3) arguments.length == 1
柯里化面试题还有另一种形式
var a = add(1)(2)(3)(4); //10
var b = add(1, 2, 3, 4); //10
var c = add(1, 2)(3, 4); //10
var d = add(1, 2, 3)(4); //10
// 实现方法
function add() {
var _args = [].slice.call(arguments);
var adder = function () {
var _adder = function() {
_args.push(...arguments); //将接收到的参数放进来,一个参数放一个,两个参数放两个
return _adder;
};
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
});
}
return _adder;
}
return adder(..._args);
}
var a = add(1)(2)(3)(4,5).toString();
console.log(a); // 10
19、async 和await
async返回的是一个Promise对象,可以跟then((resolve,reject)=>{})与catch等方法。注意下面这两种写法的区别,会先执行async中的代码,因为他只是返回一个promise对象,不代表他本身是异步。只能说明他里面的异步代码不会阻塞后面代码的执行。
async function timeout() {
return 'hello world'
}
console.log(timeout());
console.log('虽然在后面,但是我先执行')
async function timeout() {
return 'hello world'
}
timeout().then(result => {
console.log(result);
})
console.log('虽然在后面,但是我先执行');
现在写一个函数,让它返回promise 对象,该函数的作用是2s 之后让数值乘以2
// 2s 之后返回双倍的值
function doubleAfter2seconds(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2 * num)
}, 2000);
} )
}
现在再写一个async 函数,从而可以使用await 关键字, await 后面放置的就是返回promise对象的一个表达式,所以它后面可以写上 doubleAfter2seconds 函数的调用
async function testResult() {
let result = await doubleAfter2seconds(30);
console.log(result);
}
testResult();
// 打开控制台,2s 之后,输出了60.
现在我们看看代码的执行过程,调用testResult 函数,它里面遇到了await, await 表示等一下,代码就暂停到这里,不再向下执行了,它等什么呢?等后面的promise对象执行完毕,然后拿到promise resolve 的值并进行返回,返回值拿到之后,它继续向下执行。具体到 我们的代码, 遇到await 之后,代码就暂停执行了, 等待doubleAfter2seconds(30) 执行完毕,doubleAfter2seconds(30) 返回的promise 开始执行,2秒 之后,promise resolve 了, 并返回了值为60, 这时await 才拿到返回值60, 然后赋值给result, 暂停结束,代码才开始继续执行,执行 console.log语句。
总结:遇到await就等待,等待Promise执行完毕,await会拿到其返回值。之后继续执行代码
就这一个函数,我们可能看不出async/await 的作用,如果我们要计算3个数的值,然后把得到的值进行输出呢?
async function testResult() {
let first = await doubleAfter2seconds(30);
let second = await doubleAfter2seconds(50);
let third = await doubleAfter2seconds(30);
console.log(first + second + third);
}
6秒后,控制台输出220, 我们可以看到,写异步代码就像写同步代码一样了,再也没有回调地域了。