数据类型
USONB 你很牛逼 U:undefined S:string symbol O:object N:number null B:boolean
基本数据类型
:Number(数值)、String(字符串)、Boolean(布尔值)、Null(空值)、Undefined(未定义)、Symbol(独一无二的值)、
引用数据类型统称为Object类型
,细分的话有object, array, function, Boolean, Number, String
数据储存
:
1.基本数据类型的数据直接存储在栈中;而引用数据类型的数据存储在堆中,在栈中保存数据的引用地址,这个引用地址指向的是对应的数据,以便快速查找到堆内存中的对象。
2.栈内存由操作系统分配释放。 堆内存一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收。
在于在JS中采用的IEEE 754的双精度标准
,计算机内部存储数据的编码的时候,0.1在计算机内部根本就不是精确的0.1,而是取了近似值0.1。出现了精度缺失
// 转成十进制正好是 0.30000000000000004
答:0.2
和0.3
分别转换为二进制进行计算:在JS中实际取值只能存储52位尾数位,它们相加又恰巧前52位尾数都是0
,就是0.5
在console.log
的时候会二进制转换为十进制,十进制再会转为字符串的形式,在转换的过程中发生了取近似值,所以打印出来的是一个近似值的字符串
typeof
typeof null
的值为Object
,无法分辨是null
还是Object
instanceof
constructor
Object.prototype.toString.call()
一种最好的基本类型检测方式 Object.prototype.toString.call()
;它可以区分 null 、 string 、boolean 、 number 、 undefined 、 array 、 function 、 object 、 date 、 math 数据类型。
缺点:不能细分为谁谁的实例
// -----------------------------------------typeof
typeof undefined // 'undefined'
typeof '10' // 'String'
typeof 10 // 'Number'
typeof false // 'Boolean'
typeof Symbol() // 'Symbol'
typeof Function // ‘function'
`typeof null // ‘Object’ `
typeof [] // 'Object'
typeof {
} // 'Object'
// -----------------------------------------instanceof
function Foo() {
}
var f1 = new Foo();
var d = new Number(1)
console.log(f1 instanceof Foo);// true
console.log(d instanceof Number); //true
console.log(123 instanceof Number); //false -->不能判断字面量的基本数据类型
// -----------------------------------------constructor
var d = new Number(1)
var e = 1
function fn() {
console.log("ming");
}
var date = new Date();
var arr = [1, 2, 3];
var reg = /[hbc]at/gi;
console.log(e.constructor);//ƒ Number() { [native code] }
console.log(e.constructor.name);//Number
console.log(fn.constructor.name) // Function
console.log(date.constructor.name)// Date
console.log(arr.constructor.name) // Array
console.log(reg.constructor.name) // RegExp
//-----------------------------------------Object.prototype.toString.call()
console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
console.log(Object.prototype.toString.call(123)); // "[object Number]"
console.log(Object.prototype.toString.call("abc")); // "[object String]"
console.log(Object.prototype.toString.call(true)); // "[object Boolean]"
function fn() {
console.log("ming");
}
var date = new Date();
var arr = [1, 2, 3];
var reg = /[hbc]at/gi;
console.log(Object.prototype.toString.call(fn));// "[object Function]"
console.log(Object.prototype.toString.call(date));// "[object Date]"
console.log(Object.prototype.toString.call(arr)); // "[object Array]"
console.log(Object.prototype.toString.call(reg));// "[object RegExp]"
instanceof原理
:判断对象是否存在于目标对象的原型链上,一层一层向上查找,一直找到Object为止
function Foo() {
}
var f1 = new Foo();
console.log(f1 instanceof Foo);// true
function myInstance(L, R) {
//L代表instanceof左边,R代表右边
var RP = R.prototype
var LP = L.__proto__
while (true) {
if(LP == null) {
return false
}
if(LP == RP) {
return true
}
LP = LP.__proto__
}
}
console.log(myInstance({
},Object));
JavaScript
中,不同的对象都是使用二进制存储的,如果二进制前三位都是0的话,系统会判断为是Object
类型,而null的二进制全是0,自然也就判断为Object
null
:
undefined
:
==
和===
有什么区别?(5星)===
是严格意义上的相等,会比较两边的数据类型和值大小
==
是非严格意义上的相等,
1=='1'//ture
1==[1]//true
1==false//false
1==true//true
1==='1'//false
1===[1]//false
//特例
null==undefined //true
null===undefined //false
NaN==NaN //false
NaN===NaN //false
//NaN(not a number)是一个特殊的数值,这个数值用于表示一个本来要返回数值的操作数未返回数值的情况,避免报错。NaN与任何值都不相等,包括NaN自身。
fn
设置为需要调用的函数fn
__proto__
指向_this
的prototype
Array.prototype.slice.call()
call
Function.prototype.myCall = function (context) {
// 先判断调用myCall是不是一个函数
// 这里的this就是调用myCall的
if (typeof this !== 'function') {
throw new TypeError("Not a Function")
}
// 不传参数默认为window
context = context || window
// 保存this
context.fn = this
// 保存参数
let args = Array.from(arguments).slice(1) //Array.from 把伪数组对象转为数组
// 调用函数
let result = context.fn(...args)
delete context.fn
return result
}
apply
Function.prototype.myApply = function (context) {
// 判断this是不是函数
if (typeof this !== "function") {
throw new TypeError("Not a Function")
}
let result
// 默认是window
context = context || window
// 保存this
context.fn = this
// 是否传参
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
retun result
}
bind
Function.prototype.myBind = function(context){
// 判断是否是一个函数
if(typeof this !== "function") {
throw new TypeError("Not a Function")
}
// 保存调用bind的函数
const _this = this
// 保存参数
const args = Array.prototype.slice.call(arguments,1)
// 返回一个函数
return function F () {
// 判断是不是new出来的
if(this instanceof F) {
// 如果是new出来的
// 返回一个空对象,且使创建出来的实例的__proto__指向_this的prototype,且完成函数柯里化
return new _this(...args,...arguments)
}else{
// 如果不是new出来的改变this指向,且完成函数柯里化
return _this.apply(context,args.concat(...arguments))
}
}
}
区别
:
字面量:
new:
__proto__
指向原函数的原型对象手写new
// 手写一个new
function myNew(fn, ...args) {
// 创建一个空对象
let obj = {
}
// 使空对象的隐式原型指向原函数的显式原型
obj.__proto__ = fn.prototype
// this指向obj
let result = fn.apply(obj, args)
// 返回
return result instanceof Object ? result : obj
}
new内部
:
__proto__
指向原函数的原型对象Object.create(null)
创建出来的对象有什么区别 ?(3星)Object.create(null)
创建出来的对象原型为null,作为原型链的顶端,自然也没有继承Object的方法和属性构造函数都有一个prototype
属性,指向原型对象。原型对象的所有属性和方法,都会被构造函数所拥有。我们可以把那些不变的方法,直接定义在原型对象上,这样所有对象的实例就可以共享这些方法
。
对象都会有一个属性__proto__
,实例对象在创建时自动生成__proto__
属性,指向构造函数原型对象,之所以对象可以使用构造函数原型对象的属性和方法,就是因为对象有__proto__原型的存在。
构造函数.prototype => 对象原型,实例对象.proto => 对象原型。
所以构造函数.prototype === 实例对象.__proto__
原型对象中有一个constructor
属性,指向原来的构造函数
示意图:
1.当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
2.如果没有就查找它的原型(也就是__proto__指向的构造函数的原型对象)
3.如果还没有就查找原型对象的原型(即Object的原型对象)
4.依此类推一直找到Object为止(Object的原型对象__proto__=>查找机制的终点为null
).
5.__proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。
原型
: 原型分为隐式原型和显式原型,每个对象都有一个隐式原型,它指向自己的构造函数的显式原型。
原型链
: 多个__proto__组成的集合成为原型链
实例对象的__proto__指向构造函数的原型对象
构造函数的原型对象的__proto__指向Object的原型对象
Object的原型对象的__proto__指向null
作用域
作用域链
。执行上下文(执行环境)
:
全局执行上下文
创建一个全局的window对象,并规定this指向window,执行js的时候就压入栈底,关闭浏览器的时候才弹出
函数执行上下文
每次函数调用时,都会新创建一个函数执行上下文
eval执行上下文
eval的功能是把对应的字符串解析成JS代码并运行
执行栈
:
内部函数引用外部函数的参数和局部变量,称为闭包
,可以理解为“定义在一个函数内部的函数”
作用
:
缺点
:
会造成内存泄漏(有一块内存空间被长期占用,而不被释放)
应用
:
防抖和节流
封装私有变量
设计模式中的单例模式
单例模式:用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象
for循环中的保留i的操作
函数柯里化
理解内容
函数内部可以直接读取全局变量,但是在函数外部无法读取函数内部的局部变量。这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!
var n=999;
function f1(){
alert(n);
}
f1(); // 999
function f1(){
var n=999;
}
alert(n); // error
function f1(){
n=999;
}
f1();
alert(n); // 999
function f1(){
var n=999;//局部变量,生存期在f1()调用完以后被删除,但使用闭包后就不会被删除了
nAdd=function(){
n+=1
}
function f2(){
alert(n);
}
return f2;//只要函数遇到return就把后面的结果返回给函数的调用者
}
var result=f1();
result(); // 999
nAdd();//闭包使得变量始终保持在内存中 var n =999得以保存 (函数不调用自己不执行)
result(); // 1000 //n并没有被删除
全局变量在内存会一直存在,除非手动释放。局部变量出了这个作用域就会释放,显然对内存提升有好处。
在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们就可以在f1外部读取它的内部变量了.
result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制回收。
这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在**nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。**其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。
在理解
JS中的全局变量都有不可兼得的优缺点。
1.全局变量:可重用性好,任意地方都可使用,但极易被污染。
2.局部变量:仅函数内可用,不易被污染,但可重用性不好。
var a = 100; //全局变量
function fun() {
var a = 200; //局部变量
a++; //全局与局部重名,局部优先原则
console.log(a); //输出局部变量的值
}
fun(); //201
console.log(a); //100
var a = 300, //原先的全局变量被覆盖(这叫污染)
有没有一种方式既可以保证变量的可重用性又不被污染呢?
闭包概述
JS中的闭包设计,是一种设计思想,其主要目的是保证一个函数内部的变量既可以得到重用,又不被污染(不会被随意篡改)
基本步骤分析
:
定义外部函数,用于封装要保护的变量以及可外部访问的内部函数。
定义外部函数中受保护的变量以及用于操作保护变量的的内部函数并将内部函数返回。
调用外部函数获取内部函数,然后基于内部函数操作受保护变量。
function outer() {
//外部函数
var count = 0; //受保护变量(这个变量外部不可直接使用)
return function () {
//内部函数
count++; //通过内部函数操作受保护变量
console.log(count);
};
}//上面整个函数就是闭包设计
var inner = outer(); //调用外部函数获取内部函数
inner(); //调用内部函数操作受保护变量
案例1
var a = 0,
b = 0;
function A(a) {
A = function (b) {
alert(a + b++);
};
alert(a++);
}
A(1);//'1'
A(2);//'4'
函数是一个堆,function A(a)和function(b)两个堆
函数A中的A没有用var声明,所以属于全局变量,重写赋值函数A,又形成了闭包
闭包机制:闭包创建后,可以保存创建时的活动对象。
自加操作符:i++先执行再相加,++i先加在执行。如var a = 0; var b = 1; var c = a+b++,console.log(c,b) // 1 2
第一次调用函数A时,函数不调用自己不执行,所以 A = function (b) {alert(a + b++);}没有执行,执行到alert(a++)时,a++先执行后自加,所以a++为1,a为2,故A(1)=‘1’
A(1)不销毁的原因就在于函数A是A的父函数,而A被赋给了一个全局变量,这导致A始终在内存中,而A的存在依赖于函数A,故不销毁。
第二次调用函数A时,函数A已经它里面的A重新赋值,指向了一个A函数引用;
由于标识符A是在全局作用域定义的,所以在函数内部被重新赋值,在全局作用域也可以访问到重新赋值后的函数。 此时,也创建了一个闭包,该闭包保存了创建环境的活动对象。
执行到A(2),即b=2,alert(a + b++)=‘4’,A(2)输出’4’
答:
原型继承、组合继承、寄生组合继承、ES6的extend
原型继承
//方法一:原型继承
// 原型继承
// 把父类的实例作为子类的原型
// 缺点:子类的实例共享了父类构造函数的引用属性 不能传参
var person = {
friends: ["a", "b", "c", "d"]
}
var p1 = Object.create(person)
p1.friends.push("aaa")
console.log(p1);
console.log(person);
组合继承
// 方法二:组合继承
// 在子函数中运行父函数,但是要利用call把this改变一下,
// 再在子函数的prototype里面new Father() ,使Father的原型中的方法也得到继承,最后改变Son的原型中的constructor
// 缺点:调用了两次父类的构造函数,造成了不必要的消耗,父类方法可以复用
// 优点可传参,不共享父类引用属性
function Father(name) {
this.name = name
this.hobby = ["篮球", "足球", "乒乓球"]
}
Father.prototype.getName = function () {
console.log(this.name);
}
function Son(name, age) {
Father.call(this, name)
this.age = age
}
Son.prototype = new Father()
Son.prototype.constructor = Son
var s = new Son("ming", 20)
console.log(s);
寄生组合继承
//方法三:寄生组合继承
function Father(name) {
this.name = name
this.hobby = ["篮球", "足球", "乒乓球"]
}
Father.prototype.getName = function () {
console.log(this.name);
}
function Son(name, age) {
Father.call(this, name)
this.age = age
}
Son.prototype = Object.create(Father.prototype)
Son.prototype.constructor = Son
var s2 = new Son("ming", 18)
console.log(s2);
extend
// 方法四:ES6的extend(寄生组合继承的语法糖)
// 子类只要继承父类,可以不写constructor,一旦写了,则在constructor 中的第一句话必须是 super
class Son3 extends Father {
// Son.prototype.__proto__ = Father.prototype
constructor(y) {
super(200) // super(200) => Father.call(this,200)
this.y = y
}
}
答:
内存泄露是指不再用的内存没有被及时释放出来,导致该段内存无法被使用就是内存泄漏
答:
内存泄漏指我们无法在通过js访问某个对象,而垃圾回收机制却认为该对象还在被引用,因此垃圾回收机制不会释放该对象,导致该块内存永远无法释放,积少成多,系统会越来越卡以至于崩溃
未使用 var 声明的全局变量
作用域未释放(闭包)
定时器未清除
事件监听为空白
如何优化?
垃圾回收机制
:当程序创建对象,数组等引用类型实体时,系统会在堆内存中为之分配一块内存区,对象就保存在内存区中,当内存不再被任何引用变量引用时,这块内存就变成了垃圾,等待垃圾回收机制去进行回收。
(了解)新生代的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象.V8采用了一种分代回收的策略,将内存分为两个生代:新生代和老生代
新生代算法:GC复制算法
将内存分为两个大小相同的空间 From 和 To, 利用 From 空间进行分配,当 From 空间满的时候,GC将其中的存活的对象复制到 To 空间,然后把From空间一次清理掉,之后将两个空间互换即完成GC(垃圾回收)
老生代算法:标记清除算法
垃圾回收器会在运行的时候给内存中的所有变量加上标记,然后去掉执行环境中的变量以及被执行环境中变量所引用的变量(闭包),在这些完成之后仍存在标记的就是要删除的变量了
答:
标记清除法
引用计数法
原理:跟踪记录每个值被引用的次数。
浅拷贝
只克隆了对象的第一层,常见对象一般有3-4层,如果该对象为多维对象,即二维及以上的对象。这时就需要使用深拷贝
的方法。浅拷贝把对象的属性和属性值拷贝到另一个对象中
let obj = {
a: 100,
b:[10,20,30],
c:{
x:10
},
d: /^\d+$/
}
浅拷贝
之所以被称为浅拷贝,是因为对象只会被拷贝最外部的一层,至于更深层的对象,则依然是通过引用指向同一块堆内存.
// 浅拷贝函数
function shallowClone(o) {
const obj = {
};
for ( let i in o) {
obj[i] = o[i];
}
return obj;
}
// 被拷贝对象
const oldObj = {
a: 1,
b: [ 'e', 'f', 'g' ],
c: {
h: {
i: 2 } }
};
const newObj = shallowClone(oldObj);
console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 }
console.log(oldObj.c.h === newObj.c.h); // true
我们可以看到,很明显虽然oldObj.c.h
被克隆了,但是它还与oldObj.c.h
相等,这表明他们依然指向同一段堆内存,这就造成了如果对newObj.c.h
进行修改,也会影响oldObj.c.h
,这就不是一版好的拷贝.
newObj.c.h.i = 'change';
console.log(newObj.c.h, oldObj.c.h); // { i: 'change' } { i: 'change' }
我们改变了newObj.c.h.i
的值,oldObj.c.h.i
也被改变了,这就是浅拷贝的问题所在.
当然有一个新的apiObject.assign()
也可以实现浅复制,但是效果跟上面没有差别,所以我们不再细说了.
方法1:使用对象的解构
对象的扩展运算符(…)用于取出对象的所有可遍历属性,拷贝到当前对象中。
let obj2 = {
...obj}
方法2:使用循环
对象循环我们使用 for in 循环,但for in 循环会遍历到对象的继承属性,我们只需要它的私有属性,所以可以加一个判断方法:hasOwnProperty
保留对象私有属性hasOwnProperty() 方法只检测检测当前对象本身,只有当前对象本身存在该属性时才返回 true,若不加这个属性for in可能会遍历使用第三方工具库 Object.prototype 添加了新的方法或属性,所以要加hasOwnProperty 。
let obj2 = {
}
for (let i in obj){
if(!obj.hasOwnProperty(i)) break; // 这里使用continue也可以
obj2[i] = obj[i]
}
方法1:_.cloneDeep
lodash 提供了_.cloneDeep
方法用于深拷贝
let deep = _.cloneDeep(obj)
console.log(deep === obj) //false
方法2:JSON.stringify,JSON.parse
let obj2 = JSON.parse(JSON.stringify(obj))
存在的问题: 遇到正则会变为空对象,函数为空,日期会变为字符串
方法3:万能方法
function deepClone(obj){
//过滤特殊情况
if(obj === null) return null;
if(typeOf obj !== "Object") return obj;
if(obj instanceof RegExp){
return new RegExp(obj)
}
//不直接创建空对象的目的是拷贝的结果和之前保持相同的所属类
let newObj = new Obj.constructor
//也可以这么写:
//let newObj = obj instanceof Array?[]:{}
for (let i in obj){
if(obj.hasOwnProperty(i)){
newObj[i] = deepClone(obj[i])
}
}
return newObj;
}
let obj2 = deepClone(obj)
console.log(obj.c === obj2.c) //fasle 表示指向的不同一内存
因为JS里面有可视的Dom,如果是多线程的话,这个线程正在删除DOM节点,另一个线程正在编辑Dom节点,导致浏览器不知道该听谁的
事件循环
:主线程从"任务队列"中读取事件,这个过程是循环不断的,这种执行机制成称为事件循环.
(1)所有同步任务都在主线程上执行,形成一个执行栈
(execution context stack)。
(2)主线程之外,还存在一个任务队列
(task queue)。只要异步任务有了运行结果,就在’‘任务队列’'之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
具体执行顺序
:
宏任务
:setTimeout,setInterval,setImmediate,I/O,UI交互事件,事件绑定
微任务
:Promise.then,process.nextTick(node)
同一次事件循环中,微任务永远在宏任务之前执行。
1.将同步任务排队到执行栈中,异步任务排队到事件队列中。
2.浏览器环境中执行方法时,先将执行栈中的同步任务清空,再将微任务推到执行栈中并清空,之后检查是否存在任务,若存在则取出一个宏任务,执行完成检查是否有微任务,以此循环…
3.Event Loop在浏览器与node环境中的区别:
Promise 和 async 中的立即执行
:
Promise 中的异步体现在then
和catch
中,所以写在 Promise 中的代码是被当做同步任务立即执行的。
而在 async/await 中,在出现 await 出现之前,其中的代码也是立即执行的。
而await 后面的表达式会先执行一遍,将 await 后面的代码加入到 微任务中,然后就会跳出整个 async 函数来执行后面的代码。因为 async await 本身就是 promise+generator 的语法糖。所以 await 后面的代码是微任务.
案例一
console.log(1);
setTimeout(() => {
console.log("setTimeout");
let promise1 = new Promise((resolve) => {
console.log(2);
resolve();
})
.then((data) => {
console.log(100);
})
}, 10);
setTimeout(() => {
console.log("setTimeout1");
let promise1 = new Promise((resolve) => {
console.log(3);
resolve();
})
.then((data) => {
console.log(200);
})
}, 0);//注意时间先后
let promise = new Promise((resolve) => {
console.log(4);
resolve();
})
.then((data) => {
console.log(500);
})
.then((data) => {
console.log(600);
});
console.log(7);
//1 4 7 500 600 setTimeout1 3 200 setTimeout 2 100
上面的 ,按照js由上到下的执行顺序,遇到同步任务先输出1。setTimeout是宏任务,会先放到宏任务队列中。而new Promise是立即执行的,所以会先输出4。而Promise.then是微任务,会依次排列到微任务队列中,继续向下执行输出7。现在执行栈中的任务已经清空,再将微任务队列清空,依次输出500和700。之后取出第一个宏任务,输出setTimeout1,每次执行一个宏任务,再去检查微任务,输出setTimeout1、3 、200。然后取出第二个宏任务输出setTimeout,再去检查微任务,输出setTimeout 、2 、100。
案例二
//node环境下
setImmediate(() => {
console.log(1);
process.nextTick(() => {
console.log(4);
});
});
process.nextTick(() => {
console.log(2);
setImmediate(() => {
console.log(3);
});
});
js按照从上到下的执行顺序,将setImmediate排列到node事件环的I/O callbacks队列中,process.nextTick排列到微任务队列。首先检查执行栈,发现没有任务,然后检查微任务队列并执行清空,输出2,并将setImmediate排列到I/O callbacks队列,检查timers队列,发现并没有任务,继续检查I/O callbacks队列,发现有任务,执行清空,输出1,将process.nextTick排列到微任务队列,继续执行,输出3,最后将微任务清空,输出4。
案例三
async function async1(){
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
},
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log("script end");
//script start/async1 start/async2/promise1/script end/async1 end/promise2/setTimeout
我们看到首先定义了两个 async 函数,接着往下看,然后遇到了 console
语句,直接输出 script start
。输出之后,script 任务继续往下执行,遇到 setTimeout
,其作为一个宏任务,则会先将其任务分发到对应的队列中
script 任务继续往下执行,执行了 async1()函数, async 函数中在 await 之前的代码是立即执行的,所以会立即输出async1 start
。 遇到了 await 时,会将 await 后面的表达式执行一遍,所以就紧接着输出async2
,然后将 await 后面的代码也就是console.log('async1 end')
加入到 微任务中的 Promise 队列中,接着跳出 async1 函数来执行后面的代码。
script 任务继续往下执行,遇到 Promise 实例。由于 Promise 中的函数是立即执行的,而后续的 .then
则会被分发到 微任务的 Promise
队列中去。所以会先输出 promise1
,然后执行 resolve
,将 promise2
分配到对应队列。
script 任务继续往下执行,最后只有一句输出了 script end
,至此,全局任务就执行完毕了。
在 script 任务执行完毕之后,开始查找清空微任务队列。此时,微任务中, Promise
队列有的两个任务async1 end
和promise2
,因此按先后顺序输出 async1 end,promise2
。
最后执行宏任务,只有一个 setTimeout
,取出直接输出即可,至此整个流程结束。
在第一次 macrotask 执行完之后,也就是输出script end
之后,会去清理所有 microtask。所以会相继输出promise2
, async1 end
,promise4
,其余不再多说。
案例四
async function a1 () {
console.log('a1 start')//2
await a2()
console.log('a1 end')//微2 7
}
async function a2 () {
console.log('a2')//3
}
console.log('script start')//1
setTimeout(() ={
console.log('setTimeout')
}, 0)//宏1 10
Promise.resolve().then(() ={
console.log('promise1')
})//微1 6
a1()
let promise2 = new Promise((resolve) ={
resolve('promise2.then')//微3 8
console.log('promise2')//4
})
promise2.then((res) ={
console.log(res)
Promise.resolve().then(() ={
console.log('promise3')//微4 9
})
})
console.log('script end')//5
//script start/a1 start/a2/promise2/script end/promise1/a1 end/promise2.then/promise3/setTimeout
案例五
setTimeout(() => {
console.log('1');//5
new Promise(function (resolve, reject) {
console.log('2');//6
setTimeout(() => {
console.log('3'); //9
}, 0);
resolve();
}).then(function () {
console.log('4')//7
})
}, 0);//宏1
console.log('5'); //1
setTimeout(() => {
console.log('6');//8
}, 0); //宏2
new Promise(function (resolve, reject) {
console.log('7');//2
// reject();
resolve();
}).then(function () {
console.log('8')//微1 4
}).catch(function () {
//catch在Promise状态为rejected时执行,then方法捕捉到Promise的状态为rejected,就执行catch方法里面的操作
console.log('9')
})
console.log('10');//3
//5 7 10 8 1 2 4 6 3
案例六
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i++);//5 6 7 8 9
}, 1000);
}
console.log(i);//5
先执行主线程上的任务,for循环和 console.log(i),其中每一次for循环添加一个宏任务进入任务队列中,执行结果先后顺序为 console.log(i);//5 i++=5,i++=6,i++=7,i++=8,i++=9 i++先运运算在相加
总结:
任务执行,先整体过一遍,同步任务在主线程按顺序执行,异步任务中微任务永远在宏任务之前执行,理清它们进入任务队列的顺序,主线程中的微任务宏任务排在前面。
ES6新特性
undefined
//变量提升:变量的声明会被提升到当前作用域的最上面,变量的赋值不会提升
var fn = function () {
console.log(1);
}
fn();
//=> var fn;
// fn();
// fn=function(){
// console.log(1);
// }
//函数提升:函数的声明会被提升到当前作用域的最上面,但不会调用函数
fn();
function fn() {
console.log(1);
}
//=> function fn() {
// console.log(1);
// }
// fn();
//案例1
function Foo() {
getName = function () {
console.log(1);
}
return this;
}
Foo.getName = function () {
console.log(2);
}
Foo.prototype.getName = function () {
console.log(3);
}
var getName = function () {
console.log(4);
}
function getName() {
console.log(5)
}
//变量提升
//[1]function Foo() {
// getName = function () {
// console.log(1);
// }
// return this;//window
// }
//[2]var getName;
//[3]function getName() {
// console.log(5)
// }
//[4]Foo.getName = function () {
// console.log(2);
// }
//[5]Foo.prototype.getName = function () {
// console.log(3);
// }
//[6]getName= function () {
// console.log(4);
// }
//运算类型优先级 new(带参数列表)>new(无参数列表))
Foo.getName(); //2 [4]Foo.getName()就是单纯的表示 输出函数 Foo的静态方法,所以直接输出2
getName(); //4 [6] getName()因为提前声明的原因, 声明后被 var getNmae() = xxx 所覆盖 所以这里的输出 变成 4
Foo().getName(); //1 先执行Foo(),里面的return this 是window,即Foo().getName()变成window.getName 在Foo里面的全局变量getName覆盖,所以输出为 1
getName(); //1 同上 1
new Foo.getName();//2 [4]new 18 Foo.getName() 19 ,先Foo.getName()在new,为2
new Foo().getName(); //3 [5]new Foo() 19 Foo.getName() 19 优先级相同从左到右,先new Foo()在getName(),new Foo()创建实例,xxx.getName()原型上的方法 为3
new new Foo().getName(); //3 [5]先new Foo().getName() new(new Foo().getName)new Foo()创建实例,xxx.getName()原型上的方法 为3
箭头函数()=>{}
:
与普通函数的区别
:
this
,arguments
,箭头函数的this指向为父级
:首先从它的父级作用域中找,如果父级作用域还是箭头函数,再往上找,直到找到this的指向。new
命令(没有this),否则会抛出一个错误。yield
属性,不能用作生成器 Generator 函数箭头函数不能new
_proto_
指向函数的prototype箭头函数的作用
:
1.语法简洁
2.可以隐式返回
//显示返回
const double3 = numbers.map(number => {
return number * 2;
})
//隐式返回
//去掉return和花括号括号,把返回内容移到一行,较为简洁;
const double3 = numbers.map(number => number * 2);
3.不绑定this
this实际上是继承自其父级作用域中的this,箭头函数本身的this是不存在的,这样就相当于箭头函数的this是在声明的时候就确定了,this的指向并不会随方法的调用而改变
要会写Promise/all函数
Promise
:ES6引入的异步编程的新解决方案,语法是一个构造函数
,用来封装异步操作并可以获取其成功或失败的结果. Promise对象有三种状态:初始化pending 成功fulfilled 失败rejected
当我们在构造 Promise 的时候,构造函数内部的代码是立即执行
的。Promise构造函数是主线程,而Promise.then是微任务.
new Promise((resolve, reject) => {
console.log('new Promise');
resolve('success');
});
console.log('finifsh');
// 先打印new Promise, 再打印 finifsh
调用Promise对象的then方法
,两个参数为函数,返回的是promise对象,对象状态由回调函数的执行结果决定 .
//创建Promis对象
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("成功");
}, 1000);
});
//1.如果回调函数中的返回结果是 非promise对象,状态为成功,返回值为对象成功值fulfilled
const result = p.then(
(value) => {
console.log(value); //成功
return 1;
},
(reason) => {
}
);
console.log(result); //Promise {: 1}
-------------------------------------------------------------------------------------------
//2.如果回调函数中的返回结果是promise对象,此promise对象决定上面promise对象p的状态
const result = p.then(
(value) => {
console.log(value); //成功
return new Promise((resolve, reject) => {
resolve("ok");
reject("err");
});
},
(reason) => {
}
);
console.log(result);// //Promise {: 'ok} Promise {: 'err'}
-------------------------------------------------------------------------------------------
//3.抛出错误
const result = p.then(
(value) => {
console.log(value); //成功
throw "出错啦";
},
(reason) => {
}
);
console.log(result); // Promise {: '出错啦'}
Promise 实现了链式调用
,也就是说每次调用 then 之后返回的都是一个 Promise,并且是一个全新的 Promise,原因也是因为状态不可变。如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装。
Promise.resolve(1)
.then((res) => {
console.log(res); // => 1
return 2; // 包装成 Promise.resolve(2)
})
.then((res) => {
console.log(res); // => 2
});
Promise 也很好地解决了回调地狱
的问题.
回调地狱
:一个异步请求套着一个异步请求,一个异步请求依赖于另一个的执行结果,使用回调的方式相互嵌套,代码可读性低、编写费劲、不方便后期维护
ajax(url)
.then((res) => {
console.log(res);
return ajax(url1);
})
.then((res) => {
console.log(res);
return ajax(url2);
})
.then((res) => console.log(res));
//手写一个Promise
var promise = new Promise((resolve,reject) => {
if (操作成功) {
resolve(value)
} else {
reject(error)
}
})
promise.then(function (value) {
// success
},function (value) {
// failurejs
})
async
和await
两种语法结合可以让异步代码看起来像同步代码一样,简化异步函数的写法
async函数
:
1.async函数的返回值为promise对象;
2.promise对象的结果由async函数执行的返回值决定
3.async 是Generator函数的语法糖,并对Generator函数进行了改进,是对 yield 的简单封装
async function fn() {
//1.如果返回结果是 非promise对象,状态为成功,返回值为对象成功值fulfilled
return 123;
//2.抛出错误
throw "出错啦";
}
const result = fn();
console.log(result); // Promise {: 123} Promise {: '出错啦'}
-----------------------------------------------------------------------------------------
async function fn() {
//3.如果返回的promise对象,那么返回的最终结果就是Promise对象
return new Promise((reslove, reject) => {
//resolve('成功');
reject('失败');
})
}
const result = fn();
console.log(result); //Promise {: '失败'}
// then方法来获得最终结果或出错原因
result.then(
(value) => {
console.log(value);
},
(reason) => {
console.log(reason); //失败
}
)
await表达式
:
1.await必须写在aysnc函数中;
2.await后面的表达式一般为Promise对象;
3.await返回的是Promise成功的值;
4.await的Promise失败了,就会抛出异常,无法处理promise返回的reject对象,需要通过try…catch捕获处理.
//async函数 + await表达式:异步函数
async function fn() {
//await 返回的promise成功值
let result = await new Promise((resolve, reject) => {
resolve('成功')
});
console.log(result); //成功
}
fn();
一个await 的例子:
let a = 0;
let b = async () => {
a = a + (await 10);
console.log('2', a);
};
b();
a++;
console.log('1', a);
//先输出 ‘1’, 1
//在输出 ‘2’, 10
上述解释中提到了 await 内部实现了 generator,其实 await 就是 generator 加上 Promise 的语法糖,且内部实现了自动执行 generator。
答:
done
,是个布尔类型,done为true说明生成器函数执行完毕,没有可返回的值了,done
为true
后继续调用迭代器的next方法,返回值的value
为undefined
状态变化:
yield
属性的时候,都会返回一个对象yield
依次循环yield
了,就会返回一个结果对象done
为true
,value
为undefined
方式一:回调函数(callback)
:将一个函数当做参数传到另一个函数里,当那个函数执行完后,再执行传进去的这个函数,这个过程就叫做回调
回调地狱
:回调函数层层嵌套,代码可读性低、不方便后期维护
优点
:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。)
缺点
:回调地狱,每个任务只能指定一个回调函数,不能 return.
方式二: Promise
Promise就是为了解决callback的问题而产生的。
Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装
优点
:解决了回调地狱的问题
缺点
:无法取消 Promise ,内部抛出的错误需要通过回调函数来捕获
方式三:生成器Gnenrator
Generator是解决异步编程的一种方案,解决回调地狱
方式四:async/await
优点
:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题
缺点
:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。
Promise、generator、asynac/await
forEach
遍历数组,但不能使用break、continue和return语句
for...in
是用来循环带有字符串key的对象的方法。实际是为循环”enumerable“(可枚举)对象而设计的
for...of
数组对象都可以遍历,遍历对象需要通过和Object.keys()
for in循环出的是key,for of循环出的是value
可枚举属性
是指那些内部 “可枚举” 标志设置为 true 的属性。
对于通过直接的赋值和属性初始化的属性,该标识值默认为即为 true。
Js基本数据类型自带的原型属性不可枚举,通过Object.defineProperty0方法指定enumeralbe为false的属性不可枚举。
Set集合
:类似于数组,但成员的值都是唯一.数组去重
Map集合
:类似于对象,也是键值对的集合,但’键’不限于字符串,各种类型的值(包括对象)也可以当做键.
应用场景Set用于数据重组,Map用于数据储存
Set
:
Map
:
WeakSet
结构与 Set 类似,也是不重复的值的集合。但与 Set 有几个区别:
WeakMap
适用于你要往对象上添加数据,又不想干扰垃圾回收机制
一个典型应用场景是,在网页的 DOM 元素上添加数据,就可以使用 WeakMap 结构。当该 DOM 元素被清除,其所对应的 WeakMap 记录就会自动被移除。
数组解构
let [a, b, c] = [1, 2, 3] //a=1, b=2, c=3
let [d, [e], f] = [1, [2], 3] //嵌套数组解构 d=1, e=2, f=3
let [g, ...h] = [1, 2, 3] //数组拆分 g=1, h=[2, 3]
let [i,,j] = [1, 2, 3] //不连续解构 i=1, j=3
let [k,l] = [1, 2, 3] //不完全解构 k=1, l=2
对象解构
let {
a, b} = {
a: 'aaaa', b: 'bbbb'} //a='aaaa' b='bbbb'
let obj = {
d: 'aaaa', e: {
f: 'bbbb'}}
let {
d, e:{
f}} = obj //嵌套解构 d='aaaa' f='bbbb'
let g;
(g = {
g: 'aaaa'}) //以声明变量解构 g='aaaa'
let [h, i, j, k] = 'nice' //字符串解构 h='n' i='i' j='c' k='e'
指不在事件的触发目标上设置监听,而是在其父元素设置,通过冒泡机制,父元素可以监听到子元素上事件的触发。
模块化
是指将一个大的程序文件,拆分成许多小的文件,然后将小文件组合起来.
一个模块是实现一个特定功能的一组方法
优点
:1. 防止命名冲突;2. 代码复用;3. 高维护性;
实现模块化方式
:
exports
和module.exports
有什么区别?(3星)exports.xxx='xxx'
module.export = {}
exports
是module.exports
的引用,两个指向的是用一个地址,而require能看到的只有module.exports
CommonJS模块
,它通过 require 来引入模块,通过 module.exports 定义模块的输出接口。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。
ES6模块
使用 import 和 export 的形式来导入导出模块。
区别
:
**CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。**CommonJS 模块输出的是值的拷贝,模块内部的变化就影响不到这个值。ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
**CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。**CommonJS 模块就是对象,即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
跨域
是指一个域下的文档或脚本试图去请求另一个域下的资源
当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。
JSONP
JSONP原理:利用script元素的这个开放策略,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。
JSONP
只能get请求
源码:
function addScriptTag(src) {
var script = document.createElement("script")
script.setAttribute('type','text/javascript')
script.src = src
document.appendChild(script)
}
// 回调函数
function endFn(res) {
console.log(res.message);
}
// 前后端商量好,后端如果传数据的话,返回`endFn({messag:'hello'})`
CORS
CORS原理:整个CORS通信过程,都是浏览器自动完成,不需要用户参与。实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
通过自定义请求头来让服务器和浏览器进行沟通
有简单请求和非简单请求
满足以下条件,就是简单请求
Accept
、AcceptLanguage
、ContentType
、ContentLanguage
、Last-Event-Id
简单请求,浏览器自动添加一个Origin字段
XMLHttpRequest
只能拿到六个字段,要想拿到其他的需要在这里指定xhr.withCredentials = true
,后端设置Access-Control-Allow-Credentials非简单请求,浏览器判断是否为简单请求,如果是非简单请求,则 浏览器先发送一个header头为option的请求进行预检
nginx代理跨域
DOM 标准采用捕获+冒泡。两种事件流都会触发 DOM 的所有对象,从 window 对象开始,也在 window 对象结束。
DOM 标准规定事件流包括三个阶段:
this 代表函数调用相关联的对象,通常页称之为执行上下文。
箭头函数的this指向为父级
:首先从它的父级作用域中找,如果父级作用域还是箭头函数,再往上找,直到找到this的指向。 //防抖函数
function debounce(func, delay) {
let timeout
return function () {
let arg = arguments
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
func(arg)
}, delay);
}
}
// ---------------------------------------------------------立即执行防抖函数
function debounce2(fn, delay) {
let timer
return function () {
let args = arguments
if (timer) clearTimeout(timer)
let callNow = !timer
timer = setTimeout(() => {
timer = null
}, delay);
if (callNow) {
fn(args) }
}
}
// ---------------------------------------------------------立即执行防抖函数+普通防抖
function debounce3(fn, delay, immediate) {
let timer
return function () {
let args = arguments
let _this = this
if (timer) clearTimeout(timer)
if (immediate) {
let callNow = !timer
timer = setTimeout(() => {
timer = null
}, delay);
if (callNow) {
fn.apply(_this, args) }
} else {
timeout = setTimeout(() => {
func.apply(_this, arguments)
}, delay);
}
}
}
// ---------------------------------------------------------节流 ,时间戳版
function throttle(fn, wait) {
let previous = 0
return function () {
let now = Date.now()
let _this = this
let args = arguments
if (now - previous > wait) {
fn.apply(_this, arguments)
previous = now
}
}
}
// ---------------------------------------------------------节流 ,定时器版
function throttle2(fn, wait) {
let timer
return function () {
let _this = this
let args = arguments
if (!timer) {
timer = setTimeout(() => {
timer = null
fn.apply(_this, arguments)
}, wait);
}
}
}
function add() {
var args = Array.prototype.slice.call(arguments)
var adder = function () {
args.push(...arguments)
return adder
}
adder.toString = function () {
return args.reduce((prev, curr) => {
return prev + curr
}, 0)
}
return adder
}
let a = add(1, 2, 3)
let b = add(1)(2)(3)
console.log(a)
console.log(b)
console.log(add(1, 2)(3));
console.log(Function.toString)
单例模式
不管创建多少个对象都只有一个实例
var Single = (function () {
var instance = null
function Single(name) {
this.name = name
}
return function (name) {
if (!instance) {
instance = new Single()
}
return instance
}
})()
var oA = new Single('hi')
var oB = new Single('hello')
console.log(oA);
console.log(oB);
console.log(oB === oA);
工厂模式
代替new创建一个对象,且这个对象想工厂制作一样,批量制作属性相同的实例对象(指向不同)
function Animal(o) {
var instance = new Object()
instance.name = o.name
instance.age = o.age
instance.getAnimal = function () {
return "name:" + instance.name + " age:" + instance.age
}
return instance
}
var cat = Animal({
name:"cat", age:3})
console.log(cat);
构造函数模式
发布订阅者模式
class Watcher {
// name模拟使用属性的地方
constructor(name, cb) {
this.name = name
this.cb = cb
}
update() {
//更新
console.log(this.name + "更新了");
this.cb() //做出更新回调
}
}
class Dep {
//依赖收集器
constructor() {
this.subs = []
}
addSubs(watcher) {
this.subs.push(watcher)
}
notify() {
//通知每一个观察者做出更新
this.subs.forEach(w => {
w.update()
});
}
}
// 假如现在用到age的有三个地方
var w1 = new Watcher("我{
{age}}了", () => {
console.log("更新age"); })
var w2 = new Watcher("v-model:age", () => {
console.log("更新age"); })
var w3 = new Watcher("I am {
{age}} years old", () => {
console.log("更新age"); })
var dep = new Dep()
dep.addSubs(w1)
dep.addSubs(w2)
dep.addSubs(w3)
// 在Object.defineProperty 中的 set中运行
dep.notify()
代理模式
迭代器模式
两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。预加载则会增加服务器前端压力,懒加载对服务器有一定的缓解压力作用。