ES6
前言:ES6新推出的语法具有自己的特性,我们在实际开发时,一个项目中可能不会指定使用ES几,而是根据实际情况,所有的配合使用。
例如:
if(true){
var c=88;
let d=99;
}
console.log(c);//88
console.log(d);//报错
这里为什么var就可以,而let定义的变量就不可以呢?
这是因为:var的作用域就是全局作用域和函数作用域,执行环境也就是只有全局执行环境和函数作用域。
let定义的变量的作用域是:全局作用域、函数作用域、块级作用域({}包围的作用域如:if、for包围的)。对应的执行环境也应该包括(全局执行环境、函数执行环境、块级执行环境。)。
【1】块级执行环境,代码执行到{}包围代码的时候,创建一个块级的执行环境,这个执行环境在创建的同时也被执行(边创建边执行):
1.作用域链。当前的活动变量为作用域的最前端。
2.this和外部的函数的执行环境或者全局执行的this一致。
3.活动变量,存储着let或者const定义的变量,以及for中定义的i等(不存在变量提升)
当块级执行环境中的代码执行完成之后,该执行环境被销毁,同时变量对象以及属性均被销毁,外部访问不到其变量了。
【2】函数、全局执行环境
对于函数和全局环境来说,let、const定义的变量不能成为函数或全局执行环境变量对象的属性。可以说let和const定义的变量不在执行环境创建时被添加到变量对象(VO)中,而是在执行阶段将其以另一种形式添加到活动对象(AO)中,而且不是AO的属性,以另外一种形式存在。
总结:当执行全局代码或者调用函数的时候都会分别创建自己的一个执行环境,执行代码期间,遇到{}比如if或者for的时候,(1)如果里面有let或者const,则会给创建一个块级的执行环境(这个块级执行环境会有一个变量对象,但是这个变量对象是活动对象即一行行执行,不存在变量提升等。还有一个作用域链。);(2)如果里面没有let或者const则不会创建块级的执行环境(即执行环境只有全局和函数)。在执行全局或者函数代码期间,如果遇到let或者const,将其添加到活动对象(AO)中,只不过不是和var定义的一样是AO的属性,而是以另一种形式存在
(一)块作用域构造let和const
let声明(声明变量)
1.块级作用域(即let声明的变量除了全局作用域、函数作用域还有块级作用域):
let和var的用法一致,都是用来声明变量,只不过let声明的变量不会被提升,可以把变量的作用域限制在当前代码块中。也就是{}中。
例如:
2. 使用let声明的变量名不可以和同一作用域下的任何变量重名
例如:var index=0;
var index=10;
let index=100//报错:因为在这个作用域中,已经有了变量名为index的变量。
注意:但是不同的作用域中可以是重复。例如:
var index=0
{let index=10}
const声明常量
const用于声明常量,其特点如下:
1. 每个通过const关键字声明的常量必须在声明的同时进行初始化;
例如:const x=10; //正确
const x;
x=10 //报错
2.与let类似,在同一作用域下用const声明已经存在的标识符也会导致语法错误,无论该标识符是使用var 还是let声明的。
3.如果const声明的是对象,对象本身的绑定不能修改,但是对象的属性和属性值是可以修改的。例如:
let和const的全局块作用域绑定:
我们知道,在全局作用域中使用var声明的变量或对象,将作为浏览器环境中的window对象的属性,这意味着使用var很可能会无意中覆盖一个已经存在的全局属性。例如:
getting被定义为一个全局变量,并立即称为window对象的属性。定义的全局变量Screen,则覆盖了window对象中原有的Screen属性。
如果在全局作用域下使用let或者const,则会在全局作用域下创建一个新的绑定,但该绑定不会成为window对象的属性。【这也充分证明let和const定义的变量和常量不是变量对象中一部分】代码如下:
上述的Screen是内建的属性,而不是通过var定义的。如果通过var 定义 ,因为在同一作用域下let或者const不能使用重名即不能使用已有的变量名。
注意:无论是var 、let、还是const:当其所在的作用域中有一个标识符的定义,那么就会屏蔽掉所有外部的同名标识符。对于var来说好理解,因为其一开始就会在所在作用域进行变量提升,故而自然就会屏蔽掉所有外部的同名变量。对于let和const不存在变量提升,故而容易犯错。记住一点:在本作用域中有定义标识符的会屏蔽掉外部所有的同名的标识符,因此一定要注意'变量必须先声明后使用'。也就是说对于let和const来说,他们所在的作用域的代码编译阶段(没开始执行本作用域中的代码,但是即将执行),会为它们所在这个块区创建一个和执行环境差不多的词法环境,这个词法环境包括词法对象(存储着let或者const定义的变量或者常量)、词法作用域链。这个词法对象中的变量名或者常量名属性是灰区(暂时性死区)即不能访问这些属性,只有等代码执行到定义该变量名或者常量名的时候该属性才能被访问。如果在这之前访问该变量或者常量属性,就会报错,例如:
var x=10;
var name = 'ConardLi';
function a(){
console.log(x); //undefined
console.log(name) // Uncaught ReferenceError: name is notdefined(报错)
var x=10
let name = 'code秘密花园'
}
词法对象的作用域链和变量对象的作用域链类似。当let或者const所在的词法环境中代码执行完毕,该词法环境会销毁即作用域链和词法变量都会销毁。
词法环境和执行环境的关系:词法环境不是一个执行环境!只有执行环境才有对应的变量对象,定义在该执行环境中的变量和函数将成为这个对象的属性和方法。因此,词法环境中的定义的变量或者常量并不能成为执行环境中变量对象的属性。这两个环境没什么关系。但是有一点要注意:在同一作用域下:1.如果变量对象中某个变量,那么词法环境中的词法对象中就不能再存在一个同名的变量; 2.同样,如果词法对象中已经有一个变量,在变量对象中不能再有同名的变量 3.词法环境中不能有同名的变量存在。
关于词法环境中的词法对象和执行环境的变量对象没什么关系,各自创建各自的。例如:
(二)模板字面量(反引号包起来的字符串就称为模板字面量)
ES 引入了模板字面量,对于字符串的操作进行了增强方式。
- 多行字符串(真正的多行字符串)
- 字符串占位符 (可以将变量或JavaScript表达式嵌入占位符中并将其作为字符串的一部分输出到结果中)
2.1多行字符串
模板字面量的基础语法就是用反引号(`)来替换字符串的单、双引号。例如:
let message=`Hello World`;
这句代码使用模板字面量创建了一个字符串,并赋值给message变量,这时变量的值与一个普通的字符串并无差异
如果想要在字符串中使用反引号,那么同样使用反斜杠(\)将它转义即可。如下所示:
var message=`Hello \`world`;
注:在模板字面量中,不需要转义单、双引号。
在ES5中,如果一个字符串量要分为多行书写,那么可以采用两种方式来实现:即\和+来拼接字符串(/表示承接下一行的代码),例如:
var message="Hello \
World!"
var g="welcom"+
"you"
但是将二者打印在控制台,仍然显示在一行,没有换行。
如果要真正换行,则需要手动加入换行符:
var message=“Hello\n\
Wordl”
var g="Welcom"
+ "\n"
+"you"
在ES6中,使用模板字面量语法,可以很方便地实现多行字符串的创建。如果需要在字符串中添加新的一行,只需要在代码中直接换行即可。代码如下所示:
var message=`hello
wolrd
!`;
console.log(message)
注意:在反引号中的所有空白字符(包括但不限于空格、换行符、制表符)都属于字符串的一部分。
2.2 字符串占位符
在一个模板字面量中,可以将JavaScript变量或者JavaScript表达式嵌入占位符中并将其值作为字符串的一部分输出到结果中。(变量,取的是变量值;表达式,同样取的是表达式的值)
占位符由一个左侧的"${" 和右侧的"}"符号组成,中间可以包含变量或者JavaScript表达式。例如:
var name="zhangsan"; var mess=`hello ,${name}`; console.log(mess); // hello,zhangsan
再例如:
var a=5;
var b=10;
var total=`The total is ${a*b}`
console.log(total) //The total is 50
模板字面量本身也是表达式,因此也可以在一个模板字面量中嵌入另一个模板字面量。代码如下:
let k="lisi"; let mss=`hello,${
`my name is ${k}`
}`; console.log(mss) //hello,my name is lisi
(三)rest参数(用...参数名表示,例如,...data)
由于JavaScript函数有一个特别的地方就是:无论在函数定义中声明多少形参(paragram)【形参在实际调用的时候,相当于函数内定义的变量,存储在执行环境的变量对象中】
在调用函数事可以传入任意多数量的参数(arguments对象中存储实参,0:第一个形参, 1:第二个实参
......)。
例如:
function calculate(op){ if(op==="+"){ let result=0; for(let i=1;i<arguments.length;i++){ result+=arguments[i]; } return result; } else if(op==="*"){ let result=1; for(let i=1;i<arguments.length;i++){ result*=arguments[i]; } return result; } }
console.log(calculate("*",2,3,4,5)); //120
当形参的数量多,调用时传入的参数少,形参多出的参数的值是undefined。
当形参的数量少,调用时传入的参数多,无论传入多少都是向数组一样按次序存储在arguments对象中。【rest就是存储这些多出来的实参,让形参和实参的数量相应】
rest参数就是除了函数定义时列的形参,其余没列出来的形参都存储在rest中。rest参数是一个数组。【注释:如果列出来形参的数量比调用时传入的实参数量多,rest则是一个空数组。如果实参的树龄比形参的数量多,则rest数组中存储的元素就是除了列出来的参数对应的实参外的其余所剩的实参】
例如上例中的2,3,4,5就可以存放在rest中
再例如:
function calculate(op,...data){ if(op==="+"){ let result=0; for(let i=0;i<data.length;i++){ result+=data[i]; } return result; } else if(op==="*"){ let result=1; for(let i=0;i<data.length;i++){ result*=data[i]; } return result; } } console.log(calculate("*",2,3,4,5)); //120
注意:每个函数最多只能声明一个rest参数,并且它只能是最后一个参数。
calculate函数执行环境变量对象(VO){
arguments:{
0:“*”,
1:2,
2:3,
3,4,
4,5
...
}
op:"*",
data:[2,3,4,5],
//没有函数定义也没有var定义的变量。let,const定义的变量和常量在VO中没有,在变量对象AO中以特殊的形式存在,不是变量环境的属性。
}
(四)展开运算符
展开运算符在语法上与rest参数是类似的,也是三个点(...)。它可以将一个数组转化为各个独立的参数,也可用于取出对象的所有可遍历属性。(相当于去掉函数的[],去掉对象的{})
reset用在函数定义时的形参中,将剩余的独立的各个实参整合成一个数组。(如上例)
展开运算符(...)主要用于展开数组和对象,相当于将数组和对象的外壳去掉,只剩赤裸裸的果实。
例1:
var r=[1,2,3,4,5];
console.log(...r);
执行结果 这里特殊,...r将数组的[]去掉,本来每个元素之间有逗号,这里没有显示。,唯一的特殊之处
例二:
var r=[1,2,3,4,5];
function f2(a,b,c,d,e) {
console.log(a,b,c,d,e)
}
f2(...r); 将...r作为实参传di
这里...r就是相当于将数组[1,2,3,4,5]的[]去掉,剩余1,2,3,4,5,即f2(1,2,3,4,5)
例三:
var r=[1,2,3,4,5];
var br=[...r];
console.log(br);[1,2,3,4,5] //这里...r就是相当于将数组的[]去掉。
console.log(br==r);//false //这里因为...不改变数组r,因此相当于创建一个新的数组赋给br变量。
相当于复制一个新的数组对象
例四:
var r=[1,2,3,4,5]; function f2(a,...data) { console.log(a); //1 console.log(data);// } f2(...r);
例五:合并数组
var a1=[1,2,3];
var a2=[4,5,6];
var a3=[7,8,9];
var a4=[...a1,...a2,...a3];
console.log(a4); //[1, 2, 3, 4, 5, 6, 7, 8, 9]
总结:对于...数组即展开数组,就是相当于将数组的[]去掉,然后将去掉[]的结果放在...数组的位置使用。当然打印...数组特殊,各个数之间没有逗号分隔。
例六:展开对象「展开对象和展开数组一样,去掉外壳{}.但是不能打印 ...对象,会报错」
var bc={
name:"zhaosi",
grade:1
};
// console.log(...bc);报错
var dd={
...bc,
age:1991991
}
console.log(dd)
(五)解构【对象和数组】
ES6为对象和数组提供了解构功能,允许按照一定模式从对象和数组中提取值,对变量进行赋值。
5.1对象解构
对象解构的语法形式是在一个赋值操作符的左边防治一个对象字面量。代码如下:
var bc={
names:"zhaosi",
grade:1
};
//console.log(names,grade) 结果为undefined
var {names,grade}=bc;
console.log(names);//zhaosi
console.log(grade);//grade
注意,我们打印window对象,在window对象中能够找到names,grade属性=>充分证明:对象解构就是将对象的一些属性释放为其作用域中的普通变量。
解析:var {names,grade}=bc;这句代码分两个步骤:第一步:定义变量names,grade; 第二步.释放对象bc的属性names,grade到其作用域中。即:
var nams,grade;
nams="zhaosi";
grade=1;
注:对于之前的展开运算符(...bc),二者是本质是一样,都是将{}去掉,成为赤裸的内容,但不能打印展开的结果,只能在别处使用。解构刚好可以解决这一问题。可以将属性打印出来.另一方面,解构是有选择的,展开是全部展开。
如果变量之前已经声明,之后想要用解构语法给变量赋值,那么需要用圆括号包裹整个解构赋值语句。代码如下:
var bc={ names:"zhaosi", grade:1 }; var names,grade; ({names,grade}=bc); console.log(names,grade)//zhaosi 1 // 初始化 names,grade var names=10,grade=20; ({names,grade}=bc); console.log(names,grade)//zhaosi 1
整个解析赋值表达式的值与表达式右侧的值相等
例如:
var bc={
names:"zhaosi",
grade:1
};
var names,grade;
console.log({names,grade}=bc);//{names:"zhaosi",grade:1}
使用解构赋值表达式时,如果指定的局部变量名称在对象中不存在,那么这个局部变量会被赋值为undefined,在这种情况下,可以考虑为该变量定义一个默认值,在变量名称后添加一个等号(=)和相应的默认值即可。例如:
var bc={
names:"zhaosi",
grade:1
};
var {names,grade,age=0}=bc;
console.log(names,grade,age);//zhaosi 1 0
//
var bc={
names:"zhaosi",
grade:1
};
var {names,grade,age=0}=bc;
console.log(names,grade,age);//zhaosi 1 0
如果希望在使用解构赋值时,使用与对象属性名不同的局部变量名字,那么可以采用:"属性名:局部变量名"的语法形式,例如:
var bc={
names:"zhaosi",
grade:1
};
var {names:n,grade:gg}=bc;
console.log(n,gg);
names:n语法的含义是:读取名为nams的属性并将其值存储到变量n中。
嵌套
5.2数组解构
与对象 解构语法不同,数组解构使用方括号。此外,由于数据结构本质是哪个的不同,数组解构没有对象属性名的问题,因而语法上更为简单。代码如下:
let arr=[1,2,3] let [a,b,c]=arr; console.log(a) console.log(b) console.log(c)
解析:let [a,b,c]=arr;第一步先定义变量a,b,c;第二步:释放数组中的一些元素,将这些元素按照顺序分别赋值给对应的变量。
如果要获取指定位置的数组元素值,可以只为该位置的元素提供变量名。例如,需要获取数组中第三个位置的元素,可以采用以下的代码来实现:
let arr=[1,2,3]
let[,,c]=arr;
console.log(c)//3
(六)对象字面量语法扩展
如果属性名和属性值同名,则可以简写。例如:
let name="zhangsan"
let age=99
var brr={
name:name,
age:age
}
//简写后
brr={
name,
age
}
(七)箭头函数
(八)promise的用法(参考:https://www.cnblogs.com/lvdabao/p/es6-promise-1.html)【Promise主要的功能就是:当一个异步执行完之后,我们能够知道这个异步是什么时候执行完的,以至于我们在这个异步执行完之后可以进行一些其他的操作】
前言:ES2015正式发布(也就是ES6,ES6是它的乳名),其中Promise被列为正式规范。作为ES6中最重要的特性之一,我们有必要掌握并理解透彻。本文将由浅到深,讲解Promise的基本概念与使用方法。
序言:内部了解一下promise:Promise是内建的一个构造函数,和Object等一样。那就是说实例化的promise是一个Promise类型的对象。Promise构造函数的原型对象中有cath、finally、then方法,可以被实例promise继承。
这么一看就明白了,Promise是一个构造函数,自己身上有all、reject、resolve这几个眼熟的方法,原型上有then、catch等同样很眼熟的方法。这么说用Promise new出来的对象肯定就有then、catch方法喽,没错。
那就new一个玩玩吧。
正文:
一个promise可以通过Promise构造函数来创建,这个构造函数只接受一个参数:包含初始化promise代码的执行器函数,在该函数内包含需要异步执行的代码。执行器函数接受两个参数,分别是resolve函数和reject函数。异步操作结束成功时调用resolve函数,失败时调用reject函数。注:无论是resolve还是reject函数主要有三个任务:
1.修改promise对象的[[PromiseState]]属性值从pending变为fulfilled(或者rejected);state:状态
2.修改promise对象的[[Promiseresult]]属性值从undefined变为resolve中的参数或者reject中的参数;
3.告诉浏览器可以将promise.then中的回调函数从Web API中移进Task Queue中等待被执行。
例如:
1 function fun() {
2 var promise=new Promise(function (resolve, reject) {
3 setTimeout(function () { //setTimeout中的回调函数先被丢进WEB API中,1秒钟后被扔进任务队列中的宏队列中等待被执行
4 console.log("执行完成");
5 resolve("随便什么数据") //当执行到resolve这里,表示异步部分已经处理完毕,我们开始执行resolve函数,在resolve函数中我们改变promise对象的状态属性和结果属性的属性值,当promise对象的状态改变之后,浏览器立马将这个promise.then方法中的回调函数相关的异步从web API中扔进任务队列的微任务中等待被执行
6 console.log(promise) //这时候,promise的promiseState属性的值为:fullfied,promiseResult属性的值为:"随便什么数据"
},1000);
console.log("promise中的一个打印");
7 });
8 }
9 promise.then(function(data){
10 console.log(data) //随便什么数据
11 })
cosnole.log("这是一个打印")
}
12 fun()
分析:这里Promise是一个构造函数,在用new调用Promise的时候传递了一个回调函数。(回调函数就是我们只关注我们自己的定义,至于什么时候调用、调用的时候传递什么实参,那是别人的事!我们只关注我们的这个回调函数需要怎么写,实现写什么?当然在这个过程中,需要别人在调用时给我们传递的实参。一般情况下,我们和别人都是有约定的,一般实参会按照形参的数量给传递数量相等的实参。)。这个回调函数是同步的 回调函数,在该函数内会包含需要异步执行的代码。ps:在构造函数被调用,同时会将Promise的方法resolve和reject作为实参传过来。这里我们可以大概还原一下Promise构造函数:
function Promise(fun){
this.[[promiseState]]='pedding';//一旦promise的这两个属性被重新赋值,这两个属性的特性就被修改了false.即不能被修改了
this.[[promiseResult]]=undefined;
fun(Promise.resolve,Promise.reject)
}
Promise.resolve=function(){...}
Promise.reject=function(){...}
Promise.name="Promise"
Promise.all=function(){...}
Promise.prototype={ constructor:Promise
then:function(f){ ...f(resolve实参的值)...}
...
}
调用fun之后,在我们定义fun的地方开始执行。
I.new 调用Promise构造函数,【这时候执行栈把执行权交给Promise构造函数的执行环境】这时候Promise执行环境创建并入执行栈执行代码:
(1)Promise构造函数中的其他代码我们看不见,我们只关注其调用我们传的回调函数的部分就可以了。即当Promise执行环境中的代码执行到我们传的回调函数f调用的地方的时候,执行该回调函数f及创建执行环境、执行环境入栈:
【1】当执行到setTimeout的时候,这是个异步函数,扔给浏览器Web API[在1秒钟后,该setTImeout中的回调函数被扔进任务队列的宏队里中等待被执行], 继续往下执行代码,
【2】 执行第6-7行之间的代码,打印出:'promise中的一个打印'。
【3】继续往下执行,发现后面没代码了
回调函数f执行完成,f执行环境退出执行栈将执行权交给外面的Promise函数的执行环境,然后销毁f执行环境[注意,因为f函数中有定义匿名函数作为setTimeout函数的参数,即将来调用该参数函数时,该参数函数的作用域链中有f函数的变量对象,因此,f执行环境在销毁的时候,只销毁this对象的绑定、作用域链,而f函数执行环境的变量对象(AO)将保存在堆内存中。]
(2)继续执行Promise构造函数中的代码...(Promise构造函数中应该只有这一个回调函数,如上面的推断)
(3)Promise构造函数中的代码执行完成。这时候promise对象被创建完成Promise执行环境退出并销毁(变量对象、this、作用域链)。外围执行环境继续执行。这时候的promise对象应该是:
即promise对象有三个属性:1.[[Prototype]]:Promise 是Promise构造函数的原型对象。这个原型对象中有then、catch、filly方法;
2.[[PromiseState]]:pending 表示promise对象当前的状态的属性 //这个状态属性在执行resolve函数的时候,在该函数中改变;
3.[[PromiseResult]]:undefined 是resolve或者reject函数的参数。即resolve或reject函数传回来数据。//在执行resolve/reject的时候,在该函数中改变
var promise=new Promise()构造函数的一个实例
II.fun函数的执行环境中的代码继续执行:当执行到promise.then的时候,调用promise.then()方法:
前言:promise.then()方法是promise对象从原型对象上继承来的,我们同样看不到其代码,总之,then方法:1.其回调函数是一个异步执行,扔进WEB API中:回调函数在promise对象的状态改变之后被从WEB API中扔进Task Queue中等待执行及异步代码执行之后再执行的一部分代码; 2.创建一个promise对象作为then方法的返回值。 附:等将来当then方法中的回调函数执行完成之后,这个新创建的promise对象的状态属性和结果属性被修改。继而,可以通知下一个then方法的回调从WEB API 中进入任务队列的微队列中等待被执行。 var then=promise.then方法的返回值即一个Promise类型的实例。
III.继续执行代码:打印:这是一个打印。到此为止,fun函数执行完成,其执行环境退出执行栈并销毁[fun函数的执行环境销毁只销毁this、作用域链,而变量对象被保存在堆内存中。这是因为将来执行其子孙里面定义的一些函数的时候,这些函数的作用域链中有fun函数的变量对象]。
这时候执行栈为空(我们这里姑且认为就有这些js代码),事件循环(event loop)检测到执行栈为空的时候,根据先进先出以及一些规则,将任务队列中的下一个等待执行代码压入执行栈执行。
在任务队列中只有setTimeout的回调函数,创建该函数的执行环境并压入执行栈执行:
依次执行该回调函数中的代码:先执行console.log('执行完成') //页面打印出;
再执行resolve(''随便什么数据")语句:当执行到这里的时候,表示该异步函数执行完成,并且成功执行完成,执行resolve函数:修改promise中的2、3属性即:[[PromiseState]]:pending——>[[PromiseState]]:fulfilled【这时候,浏览器会安排then中的回调函数移出WEB API进入任务队列中等待被执行】;[[PromiseResult]]:undefined——>[[PromiseResult]]:'随便什么数据'。当resolve函数中的代码执行完成,resolve函数的执行环境退出栈继而销毁。
最后执行console.log(promise):打印结果为:执行完resolve后,我们再次打印promise,这时候promise对象的状态等就改变了,如下图所示。
这时候,setTimeout延迟函数中的回调函数也执行完成,故而退出执行栈,这时候栈为空。
这时候event loop(事件循环)会安排执行等在在任务队列中的所有微任务,即then中的回调函数被压入执行栈执行。打印里面的数据。
注意:调用promise的then方法,then接收两个参数,是异步回调函数,回调函数又有参数,这个参数就是fun的setTImeout中的回调函数调用resolve时传的的参数。 【注意:then里面的回调函数是异步回调函数,当执行到promise.then的时候,会将该方法的回调函数会被直接扔进Web API中。当其对应的setTimeout中的回调函数执行到resolve的时候,被扔进微任务队里中等待被执行。切记:Promise构造函数中的参数回调函数是同步的,只是如果其里面可能会包含异步。但是then方法中的参数回调函数一定是异步函数,在代码执行到.then这里的时候,then中的回调函数会被直接丢进Web API中。当其对应的setTimeout中的回调函数执行了resolve即promise的状态被修改,该函数才会被加入Task Queue中等待别执行。例如,如果我们没有在Promise构造函数的回调函数中添加resolve,promise.then中的回调会一直在Web API中,根本不会被执行,如下例:
1 var promise=new Promise(function (resolve,reject) {
2 console.log("我是promise内的setTimeout") ;
3 }).then(function (data) {
4 console.log("我是promise.then,我里面的回调是异步???")
5 console.log(data);
6 console.log("我是promise.then,我里面的回调是异步???")
7 });
这里,打印结果为
并没有then中回调函数的任何打印。这是因为Promise的参数回调函数中没有执行resolve,因此then中的回调函数一直没有进入任务队列,永远不会被执行。即promise.then方法中的回调函数会不会被执行,完全取决于resolve有没有被执行。因为resolve方法被执行,promise对象的状态才能从pending->fulfilled/rejected.
】
总结:promise状态改变之后,就不会再改变了,任何时候都可以得到这个结果。在promise状态改变之后,我们怎么根据不同的状态来做相应的处理呢?promise对象的then方法有两个参数:第一个是promise的状态为fulfilled时要调用的函数;第二个是promise的状态为rejected的时,会调用的函数。这两个函数只会执行一个。如前面分析的,当执行resolve函数或者reject函数时,改变promise的状态以及给第三个属性赋值,这时候当promise的状态改变,浏览器会根据promise的状态将promise.then中和状态对应的的回调函数移进Task Queue中等待被执行。至于then回调函数的参数就是promise对象的第三个属性的值。
ps:上面主要就是实现执行完异步函数之后,我们再执行一些代码。如果将setTimeout和剩余需要执行的部分写在一起(如上例,settimeout和console.log(data) 写在一起,如下所示:)
function f5() {
// var promise=new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("Promise的回调函数,我在这里定义,在Promise构造函数中调用,调用我时,会传两个实参。不然我的形参就是undefined哦!");
},1000);
console.log("随便什么数据");
//}); //return promise; } f5()
接上例:在平时开发中我们会遇到一个问题,就是想让在执行完异步函数之后,执行一些代码,这里会有一个问题:会先执行
console.log("随便什么数据");
然后再执行setTimeout中的函数。
那么,Promise就是解决这个问题的。那么除了用promise还有其他的办法让console.log在执行完settimeout之后再执行吗?可以,例如,上例可以改写为:但这里会将要执行的部分写在setTimeout回调中了,没有分离出来。即:
function f5() {
//var promise=new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("Promise的回调函数,我在这里定义,在Promise构造函数中调用,调用我时,会传两个实参。不然我的形参就是undefined哦!");
console.log("随便什么数据");
},1000); // }); //return promise; }
f5()
再例如:如果是执行完setimeout之后再执行某个函数,就需要嵌套回调:
function f5(fun) {
//var promise=new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("Promise的回调函数,我在这里定义,在Promise构造函数中调用,调用我时,会传两个实参。不然我的形参就是undefined哦!");
fun("随便什么数据");
},1000);
// });
//return promise;
}
f5(function(x){
console.log(x);//随便什么数据
})
再例如:如果执行完回调之后,以及fun函数之后,还需要再执行一个函数:
function f5(fun) {
//var promise=new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("Promise的回调函数,我在这里定义,在Promise构造函数中调用,调用我时,会传两个实参。不然我的形参就是undefined哦!");
fun(function(data){console.log(data);}); //随便什么数据
},1000);
// });
//return promise;
}
f5(function(callback){
callback("随便什么数据")
})
如果还需要再按次序再执行一个函数,就需要在上一个函数中调用下一个函数,即给上一个函数传入下一个执行函数回调函数。 比较麻烦!!!而Promise的优势在于,可以在then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作。
(二)promise(Promise的实例):
2.1 promise的方法then
then方法用来执行异步函数之后要执行的代码。 then方法有两个参数,这两个参数是回调函数, 一个是异步执行成功之后,需要执行的剩余代码,与异步操作相关的附加数据通过调用resolve函数传递给这个完成函数。另一个是执行失败之后需要执行的代码,所有与失败相关的附加数据通过调用reject函数传递给这个拒绝函数。
具体用法如下:(接上例:)
promise.then(function(value){
console.log(value) //异步函数执行成功后,需要执行的代码
},function(err){
console.log(err.message);//异步函数执行失败后需要执行的代码
})
then()方法的两个参数都是可选的。例如:只在执行失败后进行处理,可以给then方法的第一个参数传递null。代码如下:
promise.then(null,function(err){
console.log(err.message);//异步函数执行失败后需要执行的代码
})
以上(-)(二)为一个完整的promise异步处理。注意:如果promise的[[PromiseState]]是pending,表示promise中的异步还没有被处理,不能执行之后的所有代码。只有当promise的[[PromiseState]]是fulfilled或者rejected的时候,浏览器才会将该promise.then中的对应状态回调函数丢进任务队列。而promise的状态的改变只有执行resolve函数或者reject函数时才能被改变,因此一个promise对应在创建的时候必须有resolve或者reject,不然其方法then中的回调函数会一直在浏览器的WEB API中,根本不会进入Task Qeueu,故而then中的回调永远不会被执行。
(三)promise的链的调用形式。
例如:
promise链其实就是then方法的级联,前一个then方法中函数返回的结果会传递到下一个then方法。
- 首先promise函数在一秒后执行resolve(1)
(*)
- 第一个then中函数被调用,打印1,并且返回2
(**)
- 下一个then中函数被调用,打印上面then传递的2,并且返回4
(***)
- 依次类推,最后的结果就是1->2->4
之所以能这样使用,是因为then方法返回的结果还是一个promise对象,而其回调函数的return值就是这个then方法中创建的promise对应的resolve的内容。
代码解析:即执行第3行:发现这是一个异步,将setTimeout中的回调函数丢进WEB API中,等1秒钟之后,将其移入任务队列的宏队列中等待执行
在执行第5行的时候,执行promise.then方法,为该方法创建一个执行环境,入栈执行(then方法是promise原型对象的一个方法):
1.执行该方法中的一些代码;
2.遇到该方法的回调函数,这个回调是异步函数,被丢进Web API中(等setTimeout中resolve执行之后即第3行的resolve,promise对象的状态被改变,然后其会进入任务队列等待被执行。)
3.继续执行一些代码
4.返回一个新创建的promise1
5.代码执行完毕,执行环境退栈并销毁,执行权交给外面的执行环境
注:这时候返回来的promise1对象的状态为pending,参数是undefined;我们给这个promise起名为promise1
执行第10行,执promise1.then方法,为该方法创建一个执行环境,入栈执行
1.执行该方法中的一些代码;
2.遇到该方法的回调函数,这个回调是异步函数,被丢进Web API中(上一个then方法中的代码执行完成之后,promise1对象的状态被改变,然后其会进入任务队列等待被执行。)
3.继续执行一些代码
4.返回一个新创建的promise2
5.代码执行完毕,执行环境退栈并销毁,执行权交给外面的执行环境
注:这时候返回来的promise2对象的状态为pending,参数是undefined;我们给这个promise起名为promise2
执行第15行,执行promise2.then方法....返回promise3
到这里全局环境代码执行完毕,退栈;这时候event loop检测到栈空,让等在任务队列中的setTimeout中的回调函数入执行栈执行:
a.这里只有一句代码:resolve(1)即调用resolve函数,给resolve函数创建执行环境、压入执行栈执行该函数:
i. 改变promise对象的状态和第三个属性值,并通知浏览器将promise.then中的回调函数移除Web API,进入任务队列等待被执行。 ii.resolve代码执行完毕,退栈并销毁,将执行权交由外面的执行环境
b.setTimeout中的方法执行完成,执行环境退栈并销毁。
这时候,event loop检测到执行栈为空,将任务队列中的promise.then中的回调函数压入执行栈执行该回调函数:
i.执行我们自己写的那个回调函数;
ii.改变promise1的状态以及第三个属性(注:第三个属性的属性值为我们写的回调函数中的return值)【浏览器将promise1.then中的回调函数从web API中移除,移入Task Queue中。】【这里一步应该是执行promise1对应的resolve函数或者reject函数了,resolve函数或者reject函数的参数就是我们i中函数的返回值】。promise.then中的回调函数执行完毕,退栈并销毁。
这时候,event loop检测到执行栈为空,将任务队列中的promise1.then中的回调函数压入执行栈执行该回调函数:.....
总结:对于链式调用:只有第一个promise我们自己通过new创建,链式中的其他promise都是在调用then方法的时候同步创建的。同时将其所有then中的回调函数扔进WEB API中。等全局环境中代码都执行完成,执行栈为空,我们开始执行Task Queue(任务队列中的代码)。
然后执行我们给构造函数传的回调函数中的异步代码,紧接着在异步代码中执行resolve:改变promise的状态属性和结果属性,因为promise状态的改变,WEB API 中的promise.then中的回调函数进入Task Queque中等待被执行。
第一个promise.then函数中的异步回调函数执行完成之后,紧接着在异步回调函数之后改变这个then中创建的promise即第二个promise[then方法应给我们传的这个回调函数进行了封装,让其和resolve绑在一起,即执行完回调函数之后,执行resolve函数或者reject函数]的状态属性和结果属性【这个异步回调函数的返回值会成为第二个promise对象的结果属性的值】,因为第二个promise的状态改变,WEB API中的这个promise.then中的回调函数进入任务队列(微队列)等待被执行。
依次类推。我们这里可以大胆地推测一下原型对象的then方法:
then:function (callback) {
var self=this;
const promise=new Promise(function (resolve,reject) {
//执行到这里的时候丢进入web API中 ,这里不是setTimeout,因为setTImeout在指定的时间内会从WEB API进入任务队列,但是我们这里的异步执行必须等前面创建的promise的状态改变之后才会被扔进任务队列中,不然一直都在WEB API中。
setTimeout(function () {
var result=callback(self.[[PromiseResult]]);//等前面的promise对象的状态变为fulfilled或者rejected的时候,这些异步才会被放进任务队列的微队列中
if(instanceof result=='Promise'){ //if里面的代码主要是为了当callback回调函数返回的是一个promise对象,则将这个对象执行赋值给我们这里的promise,也就是说,我们这里的promise的状态不再依赖于下面的resolve了而是依赖于返回的这个promise对象对应的那个resolve/reject函数
promise=result;
return
}
resolve(result)
},100) })
//同步代码
return promise;
}
再例如:
1 var promise=new Promise(function (resolve,reject) {
2 console.log("我是promise内的setTimeout") ;
3 setTimeout(function () {
4 console.log("2分钟之后再将setTimeout中的回调函数从Web API丢进Task Queue中")
5 resolve("dsd");
6 },20000)
7 });
8 console.log(promise);
9 var then=promise.then(function (data) {
10 console.log("我是promise.then,我里面的回调是异步???")
11 console.log(data);
12 console.log("我是promise.then,我里面的回调是异步???")
13 console.log("我是promise.then,我里面的回调是异步???")
14 console.log("我是promise.then,我里面的回调是异步???")
15 console.log("我是promise.then,我里面的回调是异步???")
16 console.log(then);
17 });
代码解析如下:全局执行环境中的第1行:var promise=new Promise(),先调用Promise()构造函数,然后将创建的实例赋值给promise变量。
先给Promise函数创建一个执行环境,然后将该执行环境压入栈,执行其里面的代码:
Promise构造函数中的代码应该只有执行回调函数这一个语句:
即 function Promise(callback){
1 callback(Promise.resolve,Promise.rejcet);
}
I.代码执行到1的时候,调用callback回调函数,这时候调试指针指向这里【浏览器给该函数创建执行环境,将该执行环境压入执行栈】,指针指向我们定义这个回调函数的地方执行,即调试指针指向前面代码的1处,开始执行代码:
1.执行console.log("我是promise内的setTimeout")//打印
2.执行到setTimeout的时候,这是一个处理异步的函数,将其回调函数扔进WEB API中; 3. 继续执行代码,发现后面没代码了,该函数的执行环境退出执行栈并销毁。将执行权交给外层的执行环境即Promise函数的执行环境。
II. 执行权交给本执行环境,继续执行callback(Promise.resolve,Promise.reject) 之后的代码,发现Promise构造函数就这个语句,执行环境退出执行栈并销毁,将执行权交给全局执行环境,这时候promise实例被创建完成
执行第8行:console.log(promise);//打印
执行第9行:var then=promise.then(...),先调用promise对象的then方法,然后将该方法的返回值赋值给then变量。
先给then方法创建一个执行环境,然后将该执行环境压入栈,执行其里面的代码:
即then:function(callback){
var self=this;
var promise=new Promise(function(resolve,reject){
//这里应该是不是setTimeout,因为这个异步会被加入微任务队列中
setTimeout(function(){
var result=callback(self.[[PromiseResult]])
resolve(result)
},0);
});
return promise;
}
I. 执行代码var self=this;
II. 执行代码var promise=new Promise(...)
这里先执行右边的代码:即调用Promise函数。给该函数创建一个执行环境,入栈执: 1. 调用callback(Promise.resolve,Promise.reject);
这里确定callback的this值为window;开始创建执行环境:创建变量对象(VO)、this绑定、作用域链;执行环境创建完成之后,压执行环境入栈执行代码:
i 执行 setTimeout(...),执行setTimeout函数,发现这是一个处理异步的函数,将该函数的回调函数扔给WEB API[等到了指定的时间,再将其丢进任务队列中等待被执行];callback函数同步代码执行完毕,退栈(这时候肯定没有被销毁,因为其里面还有延时函数没有被执行。应该是保存在另一个临时栈吧。等将来调用其里面的延迟函数的时候再入栈。)执行权交给外面的执行环境
Promise函数的代码就一句,而且是同步的代码,因此Promise函数执行环境退栈并销毁。
变量promise的指针指向这个新创建的实例。
III 执行
总结:then:新创建一个promise对象返回;2.其回调函数被扔进WEB API中【只有当promise.then中的promise对象的状态变为成功或者失败时,该回调函数才会被执行】,而且该回调函数如果有返回值,则被作为在该then中新创建的promise对象的结果属性值即下一个then方法的回调函数的参数。3.在上一个then方法中的回调函数执行之后,马上紧接着执行下一个then中的回调函数。
更正:前面关于执行环境销毁和执行栈为空:
1.执行环境退出执行栈:当一个函数的所有代码执行完成,该执行环境退出执行栈并销毁。注意:所有代码执行完成:
(1)当一个函数中包含异步(即setTimeout、setIntravel、事件、ajax、promise等所有处理异步的函数),这个函数执行环境需要等这些异步全部执行完成、而且不会再执行才会退出执行栈。)setIntralve是每个多长时间执行一次,这个如果不被清除,一直会执行,那么包含其的执行环境会一直存在在执行栈中。
(2)包含闭包的函数的执行环境等闭包不会被调用才会被销毁。个人觉得应该是一直都存在吧,因为你不确定什么时候会被调用。
2.执行栈为空:其实执行栈不会为空,只要程序启动或者页面打开着,全局执行环境会一直在执行栈中,而且包含异步的函数的执行环境也会一直在执行环境中等待异步执行完毕,如果异步不会再执行(如setTimeout),等执行完这个异步之后,包含这个setTimeout的函数的执行环境退出执行栈并被销毁【当然,这个函数其余的代码都执行完毕,只为等这个异步而存在执行环境中】。
上面的更正是❌的。切记:每个函数都有一个作用域链、这个作用域链根据其定义的地方就确定【物理空间位置确定】,因此跟在哪里调用、什么时候调用都没有关系。只要一个函数被定义了、其就有一个固定的作用域链。在一个函数的执行环境创建的时候,会创建该函数的作用域链。
如下例,我们调用add()函数即匿名函数的时候,我们去定义的地方即第3行知道匿名函数的作用域链应该是:匿名函数的变量对象->a函数的变量对象->window对象。
但是我们在执行匿名函数的时候,a函数的执行环境已经被销毁了即a函数的执行环境不在执行栈中,也就是说a函数的变量对象也被销毁了,我怎么链接呀?我们在执行完a执行环境并准备销毁的时候,如果a函数中包含:闭包、【延迟执行函数、事件处理程序、promise、ajax等】异步执行函数的时候(注意,这些延迟执行函数是在这里定义的哦,即将来调用的时候调用指针会指向这里进行执行),我们a执行环境的变量对象AO会被保存在堆内存中。
那么这时候,我们给add函数创建执行环境中的作用链的时候:先在执行栈中找其物理外层函数a的执行环境,发现没找到(因为a函数的执行环境早就别被销毁了),则在堆内存中找a函数的变量对象——>接下来找全局执行环境,全局执行环境在栈底,找到了,将其变量环境(AO)链上来。
注:一般情况下变量对象保存对应的执行环境中,环境销毁该变量对象也销毁。但是如果有定义在该函数中的一些函数将来可能需要被执行,那么该函数的变量对象在该环境被销毁之前保存在堆内存中即该函数的变量对象在销毁之后还可能存在别的函数的作用域链上,那么该函数的变量对象被保存在堆内存中。
ps:Javascript高级程序设计(第3版有解释),意思差不多:
在匿名函数从createComparisonFunction()中被返回后,它的作用域链初始化为包含createComparisonFunction()函数的活动对象和全局变量对象。这样,匿名函数就可以访问在createComparisonFunction()中定义的所有变量。更为重要的是,createComparisonFunction()函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域仍然在引用这个活动对象。换句话说,当createComparisonFunction()中的代码执行完成之后,其执行环境退栈并销毁(作用域链、this对象),但是它的变量对象(AO)被保留在堆内存中,直到匿名函数被销毁后,createComparisonFunction()的活动变量对象才会被销毁。一般的函数,当其执行环境被销毁,变量对象、作用域链、this对象的绑定都会被销毁。。
同理,除了匿名函数,异步执行的函数也是一样的道理。
比如:闭包:
1 function a(){ 2 var count=0; 3 return function(){ 4 count++ 5 console.log(count); 6 } 7 } 8 var add=a(); 9 add();//1 10 add();//2
打印结果为:
1
2
分析:这里我们先进入全局执行环境,入栈执行:1.当执行到var add=a() 的时候:
创建a函数的执行环境(变量对象VO的创建、this值的绑定、作用域链根据函数定义的地方确定),压入执行栈执行:
变量对象变为AO,代码一句一句执行,遇到函数调用就执行调用、遇到赋值就给AO中的变量分别赋值;
AO:{argument:{},
count:undefined,
匿名函数:function
}
1.执行 var count=0 改变AO中count属性的值为0;
2.执行return function(){count++;console.log(count)}语句;将这个匿名函数作为a函数的返回值返回
a函数的执行环境中的代码执行完毕,执行环境退栈并销毁。【这里a函数因为其里面有闭包,因此,该执行环境在销毁的时候,只销毁this对象的绑定和a函数的作用域链,而其活动对象AO保存在堆内存中】。执行栈执行权交给其外层的执行环境——全局执行环境
给var add变量赋值,即add=function(){count++;console.log(count)}
2.执行add()语句:调用add()函数:
创建add函数的执行环境:即确定add函数的this对象,作用域链早就在其指向的函数定义的地方已经确定好了。在调用指针指向第3行的时候,创建执行环境(创建变量环境VO,绑定this值,作用域链:add指向的匿名函数的变量对象——>a函数的变量对象【这时候a函数执行环境不在执行栈中,a函数的变量对象被保存在堆内存中,故而去堆内存中找到将其链起来——>window对象】),执行环境创建完成进入执行栈执行代码:这时候VO变为AO:
AO:{argument:{}
}
1.执行count++ count在这个变量对象(AO)中没有这个属性,那么就在add函数的作用域中的上一级去找找【a函数的变量对象】,找到了,这时候count的值变为1;
2.执行console.log(count)//打印1
add函数执行完毕,执行环境退栈并销毁
3.执行第10行add()语句:同理跟第2不一样的,这里要注意的就是a函数的变量对象被保存在堆内存中。
再比如:setTimeout异步执行
1 function setTime(){
2 var x=0;
3 setTimeout(function () {
4 console.log("我是x的值:"+x,y);
5 },10);
6 var y=100
7 }
8 setTime();
分析:这里我们先进入全局执行环境,入栈执行:1.执行第8行setTime():
创建setTime()函数的执行环境(确定this对象的值、作用域链的创建、变量对象(VO)的创建),压入栈执行 【1】执行var x=0,即AO.x=0,即改变AO变量对象的x属性的值。
【2】执行setTimeout函数,其里面的参数是一个匿名函数,这个函数是一个延迟执行函数,将其丢进WEB API中,等10毫秒之后,在将其丢进任务对列的宏队列中等待被执行。
【3】执行var y=100;其实是执行 y=100,因为在VO中,已经执行了var y即变量提升。AO.y=100
直到现在setTime函数中的代码全部执行完毕,这时候的AO应该会是:AO:{ argument:[],
x:0,
y:100
匿名函数:function() }
setTime函数的执行环境退出执行栈并销毁(本来其this对象的绑定、作用域链、变量对象都应该销毁,但是,因为这个函数定义着一个延迟执行函数,这个延迟执行函数将来在执行的时候会创建一个作用域链,这个作用域链会包含setTime()的活动变量对象,因此,setTImeout变量对象不会被销毁,保存在堆内存中)。
因为全局执行环境就这一句代码,因此到这里全局执行环境中代码执行完成,退栈(不销毁,全局执行环境只有页面关闭等才被销毁)。这时候event Loop检测到执行栈为空,将等待在Task Queue任务对列中的函数压入执行栈执行:这里只有setTimeout的回调函数在等待被执行,故而,该回调函数创建执行环境(变量对象、this对象值的确定、建立作用域链),压入栈执行:
【1】只有一句代码console.log(“我是x的值:"+x,y);
这里的x,ysetTIMEout的回调函数的变量对象中没有,因此在其作用域链上找,其下一个作用域setTime()的变量对象中有这两个属性,因此,可以使用。
面试题:Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?
答:promise构造函数是同步执行的,then方法是异步执行的。Promise new的时候会立即执行里面的代码, then是微任务 会在本次任务执行完的时候执行, setTimeout是宏任务 会在下次任务执行的时候执行
(九)模块
什么是模块?模块就是符合一些规则定义的代码块,这些代码块一般都是在匿名函数中包含即:
(function(){
//......代码
})()
这里是立即执行的匿名函数,为什么这么写呢?主要是作用域的问题,让代码块中的所有变量,函数均为局部的。例如:jQuery模块的代码大体如下:
(function(globle,factory) {
if ( typeof module === "object" && typeof module.exports === "object" ) {
module.exports = global.document ?
factory( global, true ) :
function( w ) {
if ( !w.document ) {
throw new Error( "jQuery requires a window with a document" );
}
return factory( w );
};
} else {
factory( global );
}
})(typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
//...
return jQuery;
});
那么这些模块怎么能够被JavaScript中的其他代码使用呢?首先将这些模块的js文件引入HTML页面,相当于页面中的script标签中的代码,当页面加载到这些代码模块的时候,执行这些代码模块(其实一个页面的所有script标签中的所有代码都相当于可以合并为一个JavaScript文件,执行顺序从上到下执行即引入时的先后顺序,只是因为这些模块都是具有自己的作用域即用匿名函数包裹,因模块外的代码不能直接访问),从而一般情况下都会将这些模块的模块名称为window对象的属性或者方法,其他的JavaScript中可以通过这个属性或者方法对这个模块进行调用(模块对外暴露的方法和属性在原型链中都能够找到)例如:
那么引入模块到HTML的方式有:1.传统的<script>标签引入。
依赖性最强的最下面,因为在其加载的时候,必须保证依赖的模块已加载完成。
缺点:最大的缺点就是加载时间过长,因为JavaScript是单线程,必须等上一个script标签中的代码加载完成,再加载下一个;
其次,管理依赖比较麻烦,必须保证依赖的次序。
2.利用requery.js:
优点:由于其可以让模块异步执行,所以,节省一些时间。
其次,可以很好的管理模块,保证所有的依赖模块都加载完成之后,再执行回调中的主方法。