本知识体系是为了查漏补缺,提升自己(不一定适合你们)而整理的。
本文结构尽量做到精简,详细内容点击每个模块下方链接。
文章将按模块进行分类,以简单的文字和外链相对深入的文章来整理各个模块。
1js基础
1-1 声明
「js函数声明三种方式:」
//(1) Function()构造器
var f =new Function()
//(2) 函数声明
function f (){
console.log(2);
}
//(3) 函数表达式
var f = function() {
console.log(1);
}
复制代码
「js变量声明:」
- var声明的变量会挂载在window上,而let和const声明的变量不会
- var声明变量存在变量提升,let和const不存在变量提升(严格来说,let也存在)
- let和const声明形成块作用域
- let存在暂存死区
- const声明必须赋值
(1) var声明的变量会挂载在window上,而let和const声明的变量不会:
var a = 100;
console.log(a,window.a); // 100 100
let b = 10;
console.log(b,window.b); // 10 undefined
const c = 1;
console.log(c,window.c); // 1 undefined
复制代码
(2) var声明变量存在变量提升,let和const不存在变量提升
console.log(a); // undefined ===> a已声明还没赋值,默认得到undefined值
var a = 100;
console.log(b); // 报错:b is not defined ===> 找不到b这个变量
let b = 10;
console.log(c); // 报错:c is not defined ===> 找不到c这个变量
const c = 10;
复制代码
(3) let和const声明形成块作用域
if(1){
var a = 100;
let b = 10;
}
console.log(a); // 100
console.log(b) // 报错:b is not defined ===> 找不到b这个变量
//
if(1){
var a = 100;
const c = 1;
}
console.log(a); // 100
console.log(c) // 报错:c is not defined ===> 找不到c这个变量
复制代码
(4) 同一作用域下let和const不能重复声明,而var可以
var a = 100;
console.log(a); // 100
var a = 10;
console.log(a); // 10
let a = 100;
let a = 10;
// 控制台报错:Identifier 'a' has already been declared ===> 标识符a已经被声明了。
复制代码
(5) 暂存死区
var a = 100;
if(1){
a = 10;
let a = 1;
//在当前块作用域中存在a使用let/const声明的情况下,给a赋值10时,只会在当前作用域查找变量a,
// 而这时,还未到声明时候,所以控制台Error:a is not defined
// 即let 和 const 不会声明提前
}
复制代码
(6) const
- 一旦声明必须赋值,不能使用null占位。
- 声明后不能再修改
- 如果声明的是引用类型数据,可以修改其属性
const a = 100;
const list = [];
list[0] = 10;
console.log(list); // [10]
const obj = {a:100};
obj.name = 'apple';
obj.a = 10000;
console.log(obj); // {a:10000,name:'apple'}
复制代码
1-2 数据类型的分类:
基本类型:
- string(字符串)--原始类型
- boolean(布尔值)--原始类型
- number(数字)--原始类型
- symbol(符号)--原始类型
- null(空值)
- undefined(未定义)
- BigInt(BigInt数据类型的目的是比Number数据类型支持的范围更大的整数值,精度在(2^53-1)范围内,BigInt(10) 值为:10n)
对象类型(引用类型),有以下3种:
A.内置对象/原生对象
- String、Number、Boolean、Array、Date、RegExp、Math、 Error、 Object、Function、 Global
B.宿主对象
- (1)BOM对象: Window、Navigator、Screen、History、Location
- (2)DOM对象:Document、Body、Button、Canvas等
C.自定义对象--(指由用户创建的对象,兼容性问题需要由编写者注意)
创建自定义对象几种方式:
- (1)对象直接量:
var obj1 = {};
var obj2 = {x:0,y:0};
var obj3 = {name:‘Mary’,age:18}
复制代码
- (2)工厂模式--用函数来封装以特定接口创建对象的细节:
function createPerson(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
return o;
}
var person1 = createPerson('zhang',30,'java');
复制代码
- (3)构造函数模式:
function Person(name,age,job){
this.name= name;
this.age = age;
this.job = job;
}
var person1 = new Person('zhang',30,'java');
复制代码
- (4)原型模式:
function Person(){}
Person.prototype.name = 'zhang';
Person.prototype.age = '22';
Person.prototype.job = 'html5';
var person1 = new Person();
复制代码
1-3 数据类型的判断:
typeof:
- 一般通过 typeof 操作符来判断一个值属于哪种基本类型。
- 缺点:无法分辨对象类型
typeof 'seymoe' // 'string'
typeof true // 'boolean'
typeof 10 // 'number'
typeof Symbol() // 'symbol'
typeof null // 'object' 无法判定是否为 null
typeof undefined // 'undefined'
复制代码
typeof {} // 'object'
typeof [] // 'object'
typeof(() => {}) // 'function'
复制代码
「为什么typeof null为object:」
js 在底层存储变量的时候,会在变量的机器码的低位1-3位存储其类型信息
- 000:对象
- 010:浮点数
- 100:字符串
- 110: 布尔值
- 1:整数
但是, 对于 undefined 和 null 来说,这两个值的信息存储是有点特殊的。
- null:所有机器码均为0
- undefined:用 −2^30 整数来表示
所以,typeof 在判断 null 的时候就出现问题了,由于null 的所有机器码均为0,因此直接被当做了对象来看待。
instanceof:
- 判断对象类型:测试构造函数的 prototype 是否出现在被检测对象的原型链上。
- 缺点:无法判断一个值到底属于数组还是普通对象
[] instanceof Array // true
({}) instanceof Object // true
(()=>{}) instanceof Function // true
复制代码
let arr = []
let obj = {}
arr instanceof Array // true
arr instanceof Object // true
obj instanceof Object // true
在这个例子中,arr 数组相当于 new Array() 出的一个实例,
所以 arr.__proto__ === Array.prototype,
又因为 Array 属于 Object 子类型,
即 Array.prototype.__proto__ === Object.prototype,
所以 Object 构造函数在 arr 的原型链上
//
//判断不了原始类型
console.log(true instanceof Boolean);// false
console.log(undefined instanceof Object); // false
console.log(arr instanceof Array); // true
console.log(null instanceof Object); // false
console.log({} instanceof Object); // true
console.log(function(){} instanceof Function);// true
复制代码
Object.prototype.toString.call():
Object.prototype.toString.call({})// '[object Object]'
Object.prototype.toString.call([])// '[object Array]'
Object.prototype.toString.call(() => {})// '[object Function]'
Object.prototype.toString.call('abc')// '[object String]'
复制代码
-
传入原始类型却能够判定出结果是因为对值进行了包装。
「那么,什么是包装对象:」
所谓“包装对象”,指的是与数值、字符串、布尔值分别相对应的Number、String、Boolean三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。
1-4 js运算符:
2 内存
2-1 执行上下文
「概念:」 当代码运行时,会产生一个对应的执行环境,在这个环境中,所有变量会被事先提出来(变量提升),有的直接赋值,有的为默认值 undefined,代码从上往下开始执行,就叫做执行上下文。
「执行环境有三种」:
- 1.全局环境:代码首先进入的环境
- 2.函数环境:函数被调用时执行的环境
- 3.eval函数
「执行上下文特点:」
- 1.单线程,在主进程上运行
- 2.同步执行,从上往下按顺序执行
- 3.全局上下文只有一个,浏览器关闭时会被弹出栈
- 4.函数的执行上下文没有数目限制
- 5.函数每被调用一次,都会产生一个新的执行上下文环境
「执行3个阶段:」
-
1.创建阶段
(1).生成变量对象
(2).建立作用域链
(3).确定 this 指向
-
2.执行阶段
(1).变量赋值
(2).函数引用
(3).执行其他代码
-
3.销毁阶段
(1).执行完毕出栈,等待回收被销毁
2-2 堆栈
「概念:」
栈: 栈会自动分配内存空间,它由系统自动释放;存放基本类型,简单的数据段,占据固定大小的空间
堆: 动态分配的内存,大小不定也不会自动释放。存放引用类型,那些可能由多个值构成的对象,保存在堆内存中
2-3 垃圾回收机制
「提示:」 MDN上有说:从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对于js垃圾回收算法的改进都是基于标记-清除算法的改进
「什么是垃圾:」 一般来说,没有被引用的对象就是垃圾,就是要才清除的。但有个例外,如果几个对象相互引用形成一个环,但根访问不到他们,他们也是垃圾(引用计数法,无法清除他们)
「垃圾回收的几种算法:」
(1).引用计数法
概念: 记录有多少“程序”在引用自己,当引用的数值为0时,就开始清除它。
优势:
- 可马上回收垃圾,当被引用数值为0时,对象马上会把自己作为空闲空间连到空闲链表上,也就是说。在变成垃圾的时候就立刻被回收。
- 因为是即时回收,那么‘程序’不会暂停去单独使用很长一段时间的GC,那么最大暂停时间很短。
- 不用去遍历堆里面的所有活动对象和非活动对象
劣势:
- 计数器需要占很大的位置,因为不能预估被引用的上限,打个比方,可能出现32位即2的32次方个对象同时引用一个对象,那么计数器就需要32位。
- 最大的劣势是无法解决循环引用无法回收的问题 这就是前文中IE9之前出现的问题
(2).标记清除法
主要将GC的垃圾回收过程分为两个阶段
- 标记阶段:把所有活动对象做上标记。
- 清除阶段:把没有标记(也就是非活动对象)销毁。
优势:
- 实现简单,打标记也就是打或者不打两种可能,所以就一位二进制位就可以表示
- 解决了循环引用问题
缺点
- 造成碎片化(有点类似磁盘的碎片化)
- 再分配时遍次数多,如果一直没有找到合适的内存块大小,那么会遍历空闲链表(保存堆中所有空闲地址空间的地址形成的链表)一直遍历到尾端
3.复制算法
将一个内存空间分为两部分,一部分是From空间,另一部分是To空间,将From空间里面的活动对象复制到To空间,然后释放掉整个From空间,然后再将From空间和To空间的身份互换,那么就完成了一次GC。
2-4 内存泄漏
「概念:」 申请的内存没有及时回收掉,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果
「内存泄漏发生的场景:」
- (1) 意外的全局变量
function leaks(){
leak = 'xxxxxx';//leak 成为一个全局变量,不会被回收
}
复制代码
- (2) 遗忘的定时器
setTimeout 和 setInterval 是由浏览器专门线程
来维护它的生命周期,如果在某个页面使用了定时器,当销毁页面时,没有手动去释放清理这些定时器的话,那么这些定时器还是存活着的
- (3) 使用不当的闭包
var leaks = (function(){
var leak = 'xxxxxx';// 被闭包所引用,不会被回收
return function(){
console.log(leak);
}
})()
复制代码
- (4) 遗漏的 DOM 元素
<div id="container">
</div>
$('#container').bind('click', function(){
console.log('click');
}).remove();//dom移除了,但是js还持有对它的引用
复制代码
解决:
$('#container').bind('click', function(){
console.log('click');
}).off('click').remove();
//把事件清除了,即可从内存中移除
复制代码
- (5) 网络回调
「如何监控内存泄漏」
- 使用控制台
3 作用域
3-1 概念
扩展:
JavaScript是门动态语言
,跟Java不一样,JavaScript可以随意定义全局变量和局部变量,每一个函数都是一个作用域,当函数执行时会优先查找当前作用域,然后逐级向上。
JavaScript是静态作用域,在对变量进行查询时,变量值由函数定义时的位置
决定,和执行时的所处的作用域
无关。
ES6已经有块级作用域了,而且用 let 和 const 定义的变量不会提升。
概念:
作用域:变量或者函数的有效作用范围
作用域链:我们需要查找某个变量值,会先在当前作用域查找,如果找不到会往上一级查,如果找到的话,就返回停止查找,返回查找的值,这种向上查找的链条关系,叫作用域
3-2 相关案例
- (1)变量提升/变量由函数定义时的位置决定
var a = 1;
function fn() {
console.log('1:' + a);
var a = 2;
bar()
console.log('2:' + a)
}
function bar() {
console.log('3:' + a)
}
fn()
复制代码
分别打印:1:undefined 3:1 2:2 「解析:」
第一个 a 打印的值是 1:undefined 而不是 1。因为我们在 fn() 中定义了变量 a,用 var 定义的变量会提升到当前作用域的顶部
(即当前函数作用域顶部),但是赋值还在原地,所以是undefined。
第二个a 打印的值是 3:1 而不是 2。因为函数 bar 是定义在全局作用域中的,所以作用域链是 bar -> global,bar 里面没有定义a,所以就会顺着作用域链向上找,然后在 global 中找到了 a。注意:查找是在其定义的执行上下文环境中
查找。
第三个 a 打印的值是 2:2。这句话所在的作用域链是 fn -> global,执行 console.log('2:' + a) 会首先在 fn 作用域里查找 a,找到有 a,并且已经赋值为2,所以结果就是2。
- (2)变量赋值
var a = 1;
function fn() {
console.log('1:' + a);
a = 2
}
a = 3;
function bar() {
console.log('2:' + a);
}
复制代码
fn(); bar();
分别打印:1:3 2:2
「解析:」
第一个 打印的值是 1:3。首先, fn 中的 a = 2 是给变量 a 赋值,并不是声明变量。然后,执行函数 fn,此时 a 已经赋值为3了,注意,fn()是在a=3后面执行。
第二个 打印的值是 2:2。函数 bar 所能访问的作用域链为 bar->global,在执行函数 bar 时,由于在bar前执行了fn()将a修改为2了,所以这个时候拿到的a为2。
- (3)全局变量声明提前
if(!(a in window)){
var a = 10;
}
console.log(a);
复制代码
打印:undefined
「解析:」
相当于:
var a;
if(!(a in window)){
a = 10;
}
console.log(a);
复制代码
用 var 定义的变量会提升到当前作用域的顶部
(即当前全局作用域), 所以a会声明提前
到window中,但值还是在原地,即为undefined。 所以if得到是a in window是ture 故不走里面赋值 console.log(a) == undefined
上一个例子的变种:
(function(){
var x = c = b = {a:1}
})()
console.log(c,b) // {a: 1} {a: 1}
console.log(x.a); // error , x is not defined
复制代码
注意:x是在函数中声明的,是局部变量,c和b未声明,直接赋值,所以是全局变量。 赋值过程是从右往左的,即b={a:1},c=b,x=c
- (4)变量提升/运算符顺序
(function(){
var a = b = 3;
})()
console.log(typeof a === "undefined"); // true
console.log(typeof b === "undefined"); // false
console.log(typeof b === "number" && b ===3); // true
// 这里涉及的就是立即执行和闭包的问题,还有变量提升,运算符执行方向(=号自右向左)
// 那个函数可以拆成这样
(function()
var a; /* 局部变量,外部没法访问*/
b = 3; /* 全局变量,so . window.b === 3 , 外部可以访问到*/
a = b;
})()
复制代码
- (5)变量提升/运算符顺序
var x = 1;
if (function f(){console.log(2)}) {
x += typeof f;
}
console.log(x); // 1undefined
//因为函数体在()中会以表达式去运行,fn函数不起作用,函数不会执行。
//最后表达式转换为true,f未声明(上面的函数没起作用),值为undefined
复制代码
「知识点:」
(1). 在JavaScript中,通过 let 和 const 定义的变量具有块级作用域的特性。
(2). 通过 var 定义的变量会在它自身的作用域中进行提升,而 let 和 const 定义的变量不会。
(3).每个JavaScript程序都具有一个全局作用域,每创建一个函数都会创建一个作用域。
(4).在创建函数时,将这些函数进行嵌套,它们的作用域也会嵌套,形成作用域链,子作用域可以访问父作用域,但是父作用域不能访问子作用域。
(5).在执行一个函数时,如果我们需要查找某个变量值,那么会去这个函数被 定义 时所在的作用域链中查找,一旦找到需要的变量,就会停止向上查找。
(6).“变量的值由函数定义时的位置决定”这句话有歧义,准确说是查找变量时,是去定义这个函数时所在的作用域链查找。
4 闭包
「概念:」
闭包指的是能够访问另一个函数作用域的变量的函数。
闭包就是一个函数,这个函数能够访问其他函数的作用域中的变量
「应用场景:」
函数防抖
封装私有变量
5 this
5-1 this的指向:
- ES5中:
this 永远指向最后调用它
的那个对象 - ES6箭头函数:
箭头函数的 this 始终指向函数定义时
的 this,而非执行时。
5-2 怎么改变this的指向:
- 使用 ES6 的箭头函数
- 在函数内部使用 _this = this
- 使用 apply、call、bind
- new 实例化一个对象
案例1:
var name = "windowsName";
var a = {
name : "Cherry",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1()
},100);
}
};
a.func2() // this.func1 is not a function
在不使用箭头函数的情况下,是会报错的,因为最后调用 setTimeout 的对象是
window,但是在 window 中并没有 func1 函数。可以看做window.setTimeout
复制代码
案例2:
var webName="long";
let func=()=>{
console.log(this.webName);
}
func();//long
//箭头函数在全局作用域声明,所以它捕获全局作用域中的this,this指向window对象
复制代码
案例3:
var webName = "long";
function wrap(){
let func=() => {
console.log(this.webName);
}
func();
}
wrap();//long
//wrap函数执行时,箭头函数func定义在wrap中,func会找到它最近一层非箭头函数的this
//也就是wrap的this,而wrap函数作用域中的this指向window对象。
复制代码
5-3 箭头函数:
「Tips:」 众所周知,ES6 的箭头函数是可以避免 ES5 中使用 this 的坑的。箭头函数的 this 始终指向函数定义时
的 this,而非执行时
。箭头函数需要记着这句话:“箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值(箭头函数本身没有this,但是在它声明时可以捕获别人的this供自己使用。),如果箭头函数被非箭头函数包含
,则 this 绑定的是最近一层非箭头函数的 this
,否则,this 为 undefined”。
特点:
- 没有this
- 没有arguments
- 不能通过new关键字调用
- 没有new.target
- 没有原型
- 没有super
6 原型和原型链
6-1 背景:
一个函数可以看成一个类,原型是所有类都有的一个属性,原型的作用就是给这个类的每一个对象都添加一个统一的方法
6-2基本概念
「prototype :」 每个函数
都会有这个属性,这里强调,是函数,普通对象
是没有这个属性的(这里为什么说普通对象呢,因为JS里面,一切皆为对象,所以这里的普通对象不包括函数对象)。它是构造函数的原型对象;
「「proto」 :」 每个对象
都有这个属性,这里强调,是对象,同样,因为函数也是对象,所以函数也有这个属性。它指向构造函数的原型对象;
「constructor :」 这是原型对象上的一个指向构造函数的属性。
var webName = "long";
// Pig的构造函数
function Pig(name, age) {
this.name = name;
this.age = age;
}
// 创建一个Pig的实例,小猪佩奇
var Peppa = new Pig('Peppa', 5);
Peppa.__proto__ === Pig.prototype。 //true
Pig.__proto__ === Function.prototype //true
Pig.prototype.constructor === Pig //true
复制代码
7深浅拷贝
8数组
8-1数组去重
1. es6的new Set
let arr=[1,2,2,3]
let arrNew=[...new Set(arr)]
console.log(arrNew)//[1,2,3]
复制代码
2. 遍历旧数组往新数组中添加唯一的元素
function unique(arr) {
var newArr = []
for (var i = 0; i < arr.length; i++) {
f (newArr.indexOf(arr[i])===-1) {//方式1
newArr.push(arr[i])
}
// if (!newArr.includes(arr[i])) {//方式2
// newArr.push(arr[i])
//}
}
return newArr
}
console.log(unique(arr))//[1,2,3]
复制代码
3. 利用Map数据结构去重
function unique(){
let map =new Map()
let arr=new Array()
for(let i=0;i<arr.length;i++){
if(map.has(arr[i])){//如果有key值,它是把元素值作为key
map.set(arr[i],true)
}else{
map.set(arr[i],false)//如果没有该key值
array.push(arr[i])
}
}
return array
}
console.log(unique([1,2,2,3]))
解析:
把每一个元素作为key值存到Map中,由于Map不会出现相同的key,所以最终得到去重后的结果。
复制代码
8-2数组展开
1. flat方法
let arr = [1,2,[3,4],[5[6,7]]]
let arr1 = arr.flat(Infinity)//[1,2,3,4,5,6,7]
复制代码
- join,split
let arr1 = arr.join().split(",")
复制代码
- toString,split
let arr1 = arr.toString().split(",")
复制代码
8-3数组合并
1. es6展开合并
let arr1 = [1,2]
let arr2 = [3,4]
let arr = [...arr1,...arr2]//[1,2,3,4]
复制代码
2. concat
let arr = arr1.concat(arr2)
复制代码
8-4判断数组
- instanceof
console.log(arr instanceof Array)
复制代码
- constructor
console.log(arr.constructor === Array)
复制代码
- Array.isArray
console.log(Array.isArray(arr))
复制代码
- toString
console.log(Object.prototype.toString.call(arr) === "[object Array]")
复制代码
- 判断是否有数组的push等方法
console.log(!!arr.push && !!arr.concat)
复制代码
9 v8引擎
9-1 v8垃圾回收
「背景:」
V8的垃圾回收策略基于分代回收机制
,该机制又基于 世代假说
。该假说有两个特点:
- 大部分新生对象倾向于早死;
- 不死的对象,会活得更久。
基于这个理论,现代垃圾回收算法根据对象的存活时间
将内存进行了分代,并对不同分代的内存采用不同的高效算法进行垃圾回收。
「V8的内存分代」
在V8中,将内存分为了新生代(
new space)和老生代
(old space)。它们特点如下:
- 新生代:对象的存活时间较短。新生对象或只经过一次垃圾回收的对象。
- 老生代:对象存活时间较长。经历过一次或多次垃圾回收的对象。
「V8堆的空间」
V8堆的空间等于
新生代空间加上老生代空间。我们可以通过 --max-old-space-size
命令设置老生代空间的最大值,--max-new-space-size
命令设置新生代空间的最大值。老生代与新生代的空间大小在程序初始化
时设置,一旦生效则不能动态改变。
- node --max-old-space-size=1700 test.js // 单位为 MB
- node --max-new-space-size=1024 test.js // 单位为KB
默认设置下,64位
系统的老生代大小为1400M
,32位
系统为700M
。
对于新生代,它由两个 reserved_semispace_size
组成。每个reserved_semispace_size
的大小在不同位数的机器上大小不同。默认设置下,在64位与32位的系统下分别为16MB
和8MB
。我们将新生代、老生代、reserved_semispace_size 空间大小总结如下表。
类型\系统位数 | 64位 | 32位 |
---|---|---|
老生代 | 1400MB | 700MB |
reserved_semispace_size | 16MB | 8MB |
新生代 | 32MB | 16MB |
10 event loop
10-1 什么是事件循环
11 严格模式的优缺点
11-1 概念
ECMAScript 5 中引入的一种将更好的错误检查引入代码中的方法, 现在已经被大多浏览器实现. 这种模式使得Javascript在更严格的条件下运行
11-2 优点
- 无法再意外创建全局变量。
- 会使引起静默失败(silently fail,即:不报错也没有任何效果)的赋值操抛出异常。
- 试图删除不可删除的属性时会抛出异常(之前这种操作不会产生任何效果)。
- 要求函数的参数名唯一。
- 全局作用域下,this的值为undefined。
- 捕获了一些常见的编码错误,并抛出异常。
- 禁用令人困惑或欠佳的功能。
11-3 缺点
- 缺失许多开发人员已经习惯的功能。
- 无法访问function.caller和function.arguments。
- 以不同严格模式编写的脚本合并后可能导致问题。
12 函数式编程
12-1 概念
13 ES6
13-1 背景
EC版本 | 发布时间 | 新增特性 |
---|---|---|
EC 2009(ES5) | 2009年11月 | 扩展了Object、Array、Function的功能等 |
EC 2015(ES6) | 2015年6月 | 类,模块化,箭头函数,函数参数默认值等 |
EC 2016(ES7) | 2016年3月 | includes,指数操作符 |
EC 2017(ES8) | 2017年6月 | sync/await,Object.values(),Object.entries(),String padding等 |
13-2 常用特性
- 类
- 模块化
- 箭头函数
- 函数参数默认值
- 模板字符串
- 解构赋值
- 延展操作符
- 对象属性简写
- Promise
- Let与Const
「(1) 类(class)」
传统的javascript中只有对象,没有类的概念。
它是基于原型的面向对象语言。原型对象特点就是将自身的属性共享给新对象。
这样的写法相对于其它传统面向对象语言来讲,很有一种独树一帜的感脚!非常容易让人困惑!
如果要生成一个对象实例,需要先定义一个构造函数,然后通过new操作符来完成。
下面用一个例子演示构造函数
到class
的演变:
构造函数--
function Person(name,age) {
this.name = name;
this.age=age;
}
Person.prototype.say = function(){
return "我的名字叫" + this.name+"今年"+this.age+"岁了";
}
var obj=new Person("laotie",88);
//通过构造函数创建对象,必须使用new 运算符
console.log(obj.say());//我的名字叫laotie今年88岁了
复制代码
ES6引入了Class(类)这个概念,通过class关键字可以定义类。
该关键字的出现使得其在对象写法上更加清晰,更像是一种面向对象的语言。
如果将之前的代码改为ES6的写法就会是这个样子:
class--
class Person{//定义了一个名字为Person的类
constructor(name,age){//constructor是一个构造方法,用来接收参数
this.name = name;//this代表的是实例对象
this.age=age;
}
say(){//这是一个类的方法,注意千万不要加上function
return "我的名字叫" + this.name+"今年"+this.age+"岁了";
}
}
var obj=new Person("laotie",88);
console.log(obj.say());//我的名字叫laotie今年88岁了
console.log(typeof Person);//function--类实质上就是一个函数
console.log(Person===Person.prototype.constructor);//true
//类自身指向的就是构造函数。所以可以认为ES6中的类其实就是构造函数的另外一种写法!
复制代码
注意项:
1. 在类中声明方法的时候,千万不要给该方法加上function关键字
2. 方法之间不要用逗号分隔,否则会报错
3. class不存在变量提升
,所以需要先定义
再使用。因为ES6不会把类的声明提升到代码头部,但是ES5就不一样,ES5存在变量提升,可以先使用,然后再定义。
//ES5可以先使用再定义,存在变量提升
new A();
function A(){
}
//ES6不能先使用再定义,不存在变量提升 会报错
new B();//B is not defined
class B{
}
复制代码
「(2) 模块化(Module)」
背景
在之前的javascript中是没有模块化概念的。如果要进行模块化操作,需要引入第三方的类库。随着技术的发展,前后端分离,前端的业务变的越来越复杂化。直至ES6带来了模块化,才让javascript第一次支持了module。ES6的模块化分为导出(export)
与导入(import)
两个模块。
export的用法
在ES6中每一个模块
即是一个文件
,在文件中定义的变量,函数,对象在外部是无法获取的。如果你希望外部可以读取模块当中的内容,就必须使用export
来对其进行暴露(输出)
。
先来看个例子,来对一个变量进行模块化。
//我们先来创建一个test.js文件,来对这一个变量进行输出:
export let myName="laowang";
复制代码
//然后可以创建一个index.js文件,以import的形式将这个变量进行引入:
import {myName} from "./test.js";
console.log(myName);//laowang
复制代码
如果要输出多个变量
可以将这些变量包装成对象
进行模块化输出:
let myName="laowang";
let myAge=90;
let myfn=function(){
return "我是"+myName+"!今年"+myAge+"岁了"
}
export {
myName,
myAge,
myfn
}
/******************************接收的代码调整为**********************/
import {myfn,myAge,myName} from "./test.js";
console.log(myfn());//我是laowang!今年90岁了
console.log(myAge);//90
console.log(myName);//laowang
复制代码
如果你不想暴露模块当中的变量名字,可以通过as来进行操作:
let myName="laowang";
let myAge=90;
let myfn=function(){
return "我是"+myName+"!今年"+myAge+"岁了"
}
export {
myName as name,
myAge as age,
myfn as fn
}
/******************************接收的代码调整为**********************/
import {fn,age,name} from "./test.js";
console.log(fn());//我是laowang!今年90岁了
console.log(age);//90
console.log(name);//laowang
复制代码
也可以直接导入整个模块,将上面的接收代码修改为:
import * as info from "./test.js";//通过*来批量接收,as 来指定接收的名字
console.log(info.fn());//我是laowang!今年90岁了
console.log(info.age);//90
console.log(info.name);//laowang
复制代码
默认导出(default export) 一个模块只能有一个默认导出,对于默认导出,导入的名称可以和导出的名称不一致。
/******************************导出**********************/
export default function(){
return "默认导出一个方法"
}
/******************************引入**********************/
import myFn from "./test.js";//注意这里默认导出不需要用{}。
console.log(myFn());//默认导出一个方法
复制代码
可以将所有需要导出的变量放入一个对象中,然后通过default export进行导出
/******************************导出**********************/
export default {
myFn(){
return "默认导出一个方法"
},
myName:"laowang"
}
/******************************引入**********************/
import myObj from "./test.js";
console.log(myObj.myFn(),myObj.myName);//默认导出一个方法 laowang
复制代码
同样也支持混合导出
/******************************导出**********************/
export default function(){
return "默认导出一个方法"
}
export var myName="laowang";
/******************************引入**********************/
import myFn,{myName} from "./test.js";
console.log(myFn(),myName);//默认导出一个方法 laowang
复制代码
重命名export和import 如果导入的多个文件中,变量名字相同,即会产生命名冲突的问题,为了解决该问题,ES6为提供了重命名的方法,当你在导入名称时可以这样做:
/******************************test1.js**********************/
export let myName="我来自test1.js";
/******************************test2.js**********************/
export let myName="我来自test2.js";
/******************************index.js**********************/
import {myName as name1} from "./test1.js";
import {myName as name2} from "./test2.js";
console.log(name1);//我来自test1.js
console.log(name2);//我来自test1.js
复制代码
「(3) 箭头(Arrow)函数」
查看1-5-3 箭头函数小节:
箭头函数的
this 始终指向函数定义时
的 this,而非执行时
。箭头函数需要记着这句话:“箭头函数中没有 this 绑定
,必须通过查找作用域链
来决定其值,如果箭头函数被非箭头函数包含
,则 this 绑定的是最近一层非箭头函数
的 this,否则,this 为 undefined
”。
「(4) 函数参数默认值」
ES6支持在定义函数
的时候为其设置默认值:
function foo(height = 50, color = 'red')
{
// ...
}
复制代码
不使用默认值:
function foo(height, color)
{
var height = height || 50;
var color = color || 'red';
//...
}
复制代码
这样写一般没问题,但当参数的布尔值为false时,就会有问题了。
比如,我们这样调用foo函数: foo(0, "")
因为0的布尔值为false,这样height的取值将是50。同理color的取值为‘red’。 所以说,函数参数默认值不仅能是代码变得更加简洁而且能规避一些问题。
「(5) 模板字符串」
ES6支持在定义函数
ES6支持模板字符串,使得字符串的拼接更加的简洁、直观。
不使用模板字符串:
var name = 'Your name is ' + first + ' ' + last + '.'
复制代码
使用模板字符串:
var name = `Your name is ${first} ${last}.`
复制代码
在ES6中通过${}就可以完成字符串的拼接,只需要将变量放在大括号之中。
「(6) 解构赋值」
解构赋值语法是JavaScript的一种表达式,可以方便的从数组或者对象中快速提取值赋给定义的变量。
获取数组中的值
//从数组中获取值并赋值到变量中,变量的顺序与数组中对象顺序对应。
var foo = ["one", "two", "three", "four"];
var [one, two, three] = foo;
console.log(one); // "one"
console.log(two); // "two"
console.log(three); // "three"
复制代码
//如果你要忽略某些值,你可以按照下面的写法获取你想要的值
var [first, , , last] = foo;
console.log(first); // "one"
console.log(last); // "four"
复制代码
//你也可以这样写
var a, b; //先声明变量
[a, b] = [1, 2];
console.log(a); // 1
console.log(b); // 2
复制代码
//如果没有从数组中的获取到值,你可以为变量设置一个默认值。
var a, b;
[a=5, b=7] = [1];
console.log(a); // 1
console.log(b); // 7
复制代码
//通过解构赋值可以方便的交换两个变量的值。
var a = 1;
var b = 3;
[a, b] = [b, a];
console.log(a); // 3
console.log(b); // 1
复制代码
获取对象中的值
const student = {
name:'Ming',
age:'18',
city:'Shanghai'
};
const {name,age,city} = student;
console.log(name); // "Ming"
console.log(age); // "18"
console.log(city); // "Shanghai"
复制代码
「(7) 延展操作符」
在ECMAScript 2018中延展操作符增加了对对象的支持
var obj1 = { foo: 'bar', x: 42 };
var obj2 = { foo: 'baz', y: 13 };
var clonedObj = { ...obj1 };
// 克隆后的对象: { foo: "bar", x: 42 }
var mergedObj = { ...obj1, ...obj2 };
// 合并后的对象: { foo: "baz", x: 42, y: 13 }
复制代码
//我们可以这样合并数组:
var arr1=['a','b','c'];
var arr2=[...arr1,'d','e']; //['a','b','c','d','e']
复制代码
//展开运算符也可以用在push函数中,可以不用再用apply()函数来合并两个数组:
var arr1=['a','b','c'];
var arr2=['d','e'];
arr1.push(...arr2); //['a','b','c','d','e']
复制代码
//用于解构赋值
let [arg1,arg2,...arg3] = [1, 2, 3, 4];
arg1 //1
arg2 //2
arg3 //['3','4']
复制代码
//展开运算符既然能合并数组,自然也能解构数组,不过要注意,解构赋值中展开运算符只能用在最后:
let [arg1,...arg2,arg3] = [1, 2, 3, 4]; //报错
复制代码
「(8) 对象属性简写」
在ES6中允许我们在设置一个对象的属性的时候不指定属性名。
//不使用ES6
const name='Ming',age='18',city='Shanghai';
const student = {
name:name,
age:age,
city:city
};
console.log(student);//{name: "Ming", age: "18", city: "Shanghai"}
//对象中必须包含属性和值,显得非常冗余。
复制代码
//使用ES6
const name='Ming',age='18',city='Shanghai';
const student = {
name,
age,
city
};
console.log(student);//{name: "Ming", age: "18", city: "Shanghai"}
//对象中直接写变量,非常简洁。
复制代码
「(9) Promise」 什么是 Promise
Promise 是异步编程的一种解决方案,比传统的异步解决方案【回调函数】和【事件】更合理、更强大。现已被 ES6 纳入进规范中。
下面通过几个案例来加深promise的了解:
(1) Promise 构造函数是同步执行的.then是异步的
const promise = new Promise((resolve, reject) => {
console.log(1)
resolve()
console.log(2)
})
promise.then(() => {
console.log(3)
})
console.log(4)
//运行结果:1 2 4 3
复制代码
解析:Promise 构造函数是同步执行的,promise.then 中的函数是异步执行的。
(2) promise 状态一旦改变则不能再变
const promise = new Promise((resolve, reject) => {
resolve('success1')
reject('error')
resolve('success2')
})
promise
.then((res) => {
console.log('then: ', res)
})
.catch((err) => {
console.log('catch: ', err)
})
//运行结果:then: success1
复制代码
解析:构造函数中的 resolve 或 reject 只有第一次执行
有效,多次调用
没有任何作用,promise 状态一旦改变则不能再变
。
(3) .then 或者 .catch 都会返回一个新的 promise
Promise.resolve(1)
.then((res) => {
console.log(res)
return 2
})
.catch((err) => {
return 3
})
.then((res) => {
console.log(res)
})
//运行结果:1 2
复制代码
解析:promise 可以链式调用。提起链式调用我们通常会想到通过 return this 实现,不过 Promise 并不是这样实现的。promise 每次调用 .then 或者 .catch 都会返回一个新的 promise,从而实现了链式调用。
(4) .then 或者 .catch 都会返回一个新的 promise
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('once')
resolve('success')
}, 1000)
})
const start = Date.now()
promise.then((res) => {
console.log(res, Date.now() - start)
})
promise.then((res) => {
console.log(res, Date.now() - start)
})
//运行结果:once
success 1005
success 1007
复制代码
解析:promise 的 .then 或者 .catch 可以被调用多次,但这里 Promise 构造函数只执行一次
。或者说 promise 内部状态一经改变(第一次调用.then就改变了),并且有了一个值,那么后续每次调用 .then 或者 .catch 都会直接拿到该值。
(5) .then 或者 .catch 都会返回一个新的 promise
Promise.resolve()
.then(() => {
return new Error('error!!!')
})
.then((res) => {
console.log('then: ', res)
})
.catch((err) => {
console.log('catch: ', err)
})
//运行结果:then: Error: error!!!
at Promise.resolve.then (...)
at ...
复制代码
解析:.then 或者 .catch 中 return 一个 error 对象并不会抛出错误,所以不会被后续的 .catch 捕获,需要改成其中一种:
1. return Promise.reject(new Error('error!!!'))
2. throw new Error('error!!!')
复制代码
因为返回任意一个非 promise 的值都会被包裹成 promise 对象,即 return new Error('error!!!')
等价于 return Promise.resolve(new Error('error!!!'))
。
(6) .then 或 .catch 返回的值不能是 promise 本身
const promise = Promise.resolve()
.then(() => {
return promise
})
promise.catch(console.error)
//运行结果:TypeError: Chaining cycle detected for promise #<Promise>
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)
at Function.Module.runMain (module.js:667:11)
at startup (bootstrap_node.js:187:16)
at bootstrap_node.js:607:3
复制代码
解析:.then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环。
(7) .then函数返回值类型与参数传递
Promise.resolve(2) // resolve(2) 函数返回一个Promise<number>对象
.then(x=>{
console.log( x ); // 输出2, 表示x类型为number且值为2,也就是上面resolve参数值
return "hello world"; // 回调函数返回字符串类型,then函数返回Promise<string>
}) // then函数返回Promise<string>类型
.then(x=>{
console.log( x ); // 输出hello world,也就是上一个then回调函数返回值,表明上一个then的返回值就是下一个then的参数
}) // then函数回调函数中没有返回值,则为Promise<void>
.then(x=>{ // 前面的then的回调函数没有返回值所以这个x是undefined
console.log( x ); // undefined
}) // Promise<void>
.then(()=>{ // 前面没有返回值,这里回调函数可以不加返回值
return Promise.resolve("hello world"); // 返回一个Promise<string>类型
}) // 这里then的返回值是Promise<string>
.then(x=>{ // 虽然上面的then中回调函数返回的是Promise<string>类型但是这里x并不是Promise<string>类型而是string类型
console.log(x); // hello world
return Promise.resolve(2); // 返回一个Promise<number>类型对象
}) // 返回Promise<number>类型
复制代码
(8) .then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透。
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log) //1
复制代码
解析:.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透。
(9) .catch 相当于.then的简写(省略了.then的第二个参数)
Promise.resolve()
.then(function success (res) {
throw new Error('error')
}, function fail1 (e) {
console.error('fail1: ', e)
})
.catch(function fail2 (e) {
console.error('fail2: ', e)
})
//运行结果:fail2: Error: error
at success (...)
at ...
复制代码
解析:.then 可以接收两个参数
,第一个
是处理成功
的函数,第二个
是处理错误
的函数。.catch也相当于是一个.then,只不过把.then的第二个参数省略了,但是它们用法上有一点需要注意:.then 的第二个处理错误的函数
捕获不了第一个处理成功
的函数抛出的错误
,而后续的 .catch 可以捕获之前的错误。
(10) 微任务宏任务执行顺序
process.nextTick(() => {
console.log('nextTick')
})
Promise.resolve()
.then(() => {
console.log('then')
})
setImmediate(() => {
console.log('setImmediate')
})
console.log('end')
//运行结果:end
nextTick
then
setImmediate
复制代码
解析:process.nextTick
和 promise.then
都属于 microtask
,而 setImmediate 属于 macrotask
,在事件循环的 check 阶段执行。事件循环的每个阶段(macrotask)之间都会执行 microtask,事件循环的开始
会先执行一次 microtask。