JS数据类型
js的数据类型有哪些
基本数据类型
Number:数值
String:字符串
Boolean:判断
Null:空
Undefine:未定义
Symbol: ES6新增类型为解决原对象中属性名都为字符串,当添加的属性名与对象中的属性名相同时就会有冲突的风险。Symbol正是解决了这一个问题,属于Symbol类型的属性名,都是独一无二的,不会与其他的属性名产生冲突
BigInt:大整型
引用数据类型
统称为Object
细分有Object,Array, Function,Date,RegExp
基本数据类型的数据直接存放在栈中,而引用数据类型的数据存放在堆中,在栈中存放数据的引用地址,栈内存是自动分配的,会自动释放,而堆内存是动态分配的,不会自动释放,需将对象设置为null后才得以释放
为什么0.1+0.2>0.3
在js底层中每一个变量都以2进制进行表达,0.1和0.2转为二进制是无限循环小数,js会进行截取操作,从而不是0.1和0.2本身了
解决方法
//让其不为小数即可
(0.1*1000+0.2*1000)/1000
数据类型的有哪些判断方式
1. typeof
右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示。
typeof ''; // 'string' 有效
typeof 1; // 'number' 有效
typeof Symbol(); // 'symbol' 有效
typeof true; //'boolean' 有效
typeof undefined; //'undefined' 有效
typeof null; //'object' 无效
typeof [] ; //'object' 无效
typeof new Function(); // 'function' 有效
typeof new Date(); //'object' 无效
typeof new RegExp(); //'object' 无效
基本类型中只有null没有返回想要的类型,因为在js中不同的对象都是使用二进制进行存储的,typeof通过二进制前三位为0则为Object类型,而null为空,全为0自然也就判断为Object对象
引用类型中只有Function返回了想要的类型,其余都只返回了处于原型链顶端的Object对象
2. instanceof
instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。缺点只能用来判断对象是否存在目标对象上的原型链上,不能得出对象实体具体属于那种类型,基础类型的字面量不能进行判断。null和undefined不能判断
在这里需要特别注意的是:instanceof 检测的是原型,instanceof实现原理
function myInstanceof (a,b){
let typeA = typeof a
//使用字面量定义的基础类型变量时要返回false
if(typeA !== 'object'&& typeA !== 'function' )
return false
let left = a.__proto__
let right = b.prototype
//根据隐式原型向上查找,是否直接指向或间接指向右侧构造函数的显式原型
while(true){
if(left === right) return true
else if (left ===null) return false
left = left.__proto__
}
}
console.log(myInstanceof([],Array))
例子
[] instanceof Array; //true
({}) instanceof Object; //true 左边直接写{}会报错是因为{}可以表示空的代码块也可以表示空的对象js不知道为哪一个
new Date() instanceof Date; //true
function fn(){
}
fn instanceof Function; //true 左边为方法,右边为Funtion构造函数
new fn() instanceof fn; //true 左边为实例右边为构造函数
/()/ instanceof RegExp; //true
fn instanceof Object; //true Object是fn的间接隐式原型
//使用基础类型使用字面量都会返回false
1 instanceof Number; //false
'str1' instanceof String; //false
true instanceof Boolean; //false
let symbol =Symbol('123123')
symbol instanceof Symbol //false
//使用构造函数创建基础类型对象,即都返回true
let number = new Number(1)
number instanceof Number
3.constructor
当一个函数 F被定义时,JS引擎会为F添加 prototype 原型,然后再在 prototype上添加一个 constructor 属性,并让其指向 F 的引用
当执行 var f = new F() 时,F 被当成了构造函数,f 是F的实例对象,此时 F 原型上的 constructor 传递到了 f 上,因此 f.constructor == F
可以看出,F 利用原型对象上的 constructor 引用了自身,当 F 作为构造函数来创建对象时,原型上的 constructor 就被遗传到了新创建的对象上, 从原型链角度讲,构造函数 F 就是新对象的类型。这样做的意义是,让新对象在诞生以后,就具有可追溯的数据类型。 constructor.name 以字符串的方式返回对象类型
例子
''.constructor == String //true
(1).constructor == Number //true
new Number(1).constructor == Number //true
let symbol =Symbol('123')
symbol.constructor == Symbol //true
let big = BigInt(123213)
big.constructor == BigInt //true
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'
缺点:
1.null和undefined是无效的对象,因此不会有constructor对象
4.Object.prototype.toString.call()
一种最好的基本类型检测方式 Object.prototype.toString.call() ;它可以区分 null 、 string 、boolean 、 number 、 undefined 、 array 、 function 、 object 、 date 、 math 数据类型。
toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。
对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。
为什么不直接使用toString()?
因为很多类型中的toString()都被重写过,并不能返回类型信息
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]"
参考
https://www.cnblogs.com/onepixel/p/5126046.html
字面量创建对象和new创建对象有什么区别,new内部都实现了什么,手写一个new
字面量:
- 创建对象更简单方便阅读
- 不需要作用域解析,速度更快
new内部:
- 创建一个新的对象
- 新对象的
__proto__
指向构造函数的prototype
- 改变this指向(指向新的obj)并执行该函数,执行结果保存起来作为result
- 判断执行函数的结果是不是null或Undefined,如果是则返回之前的新对象,如果不是则返回result
function myNew (fn ,...args){
let obj = {}
obj.__proto__ = fn.prototype
//result 返回null或undefined表示其为构造函数
let result = fn.apply(obj,args)
//判断执行函数的结果是不是null或Undefined,如果是则返回之前的新对象,如果不是则返回result
return result instanceof Object ? result : obj
}
function Foo(name,age){
this.name=age,
this.age = age
}
function fn(name,age){
return {name:name,age:age}
}
let foo = myNew(Foo,'zlb',18)
let fnResult = myNew(fn,'zlb',18)
console.log(foo) //Foo {name:18,age:18}
console.log(fnResult)//{name:18,age:18}
call()、apply()、bind()
call()、apply()、bind() 都是用来重定义 this 这个对象的!
例子
let name1:string = '小万'
let age:number=12
let obj ={
name1:'小张',
age:18,
myFun:function(){
console.log(this.name1,"年龄:",this.age)
}
}
let db={
name1:'小王',
age:22
}
//call()、apply()、bind() 都是用来重定义 this 这个对象的
obj.myFun() //小张 年龄:18
obj.myFun.apply(db) //小王 年龄:22
obj.myFun.call(db) //小王 年龄:22
obj.myFun.bind(db)() //小王 年龄:22
对比call()、bind() 、 apply() 传参情况下
let name1:string = '小万'
let age:number=12
let obj ={
name1:'小张',
age:18,
myFun:function(from,to){
console.log(`${this.name1} 年龄:${this.age}从${from}到${to}`)
}
}
let db={
name1:'小王',
age:22
}
obj.myFun('嘉兴','杭州') //小张 年龄:18从嘉兴到杭州
obj.myFun.call(db,'嘉兴','杭州') //小王 年龄:22从嘉兴到杭州
obj.myFun.apply(db,['嘉兴','杭州']) //小王 年龄:22从嘉兴到杭州
obj.myFun.bind(db,'嘉兴','杭州')() //小王 年龄:22从嘉兴到杭州
obj.myFun.bind(db,['嘉兴','杭州'])() //小王 年龄:22从嘉兴,杭州到undefined
call 、bind 、 apply 这三个函数的第一个参数都是 this 的指向对象,第二个参数差别就来了:
call 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔,直接放到后面 obj.myFun.call(db,‘成都’, … ,‘string’ )。
apply 的所有参数都必须放在一个数组里面传进去 obj.myFun.apply(db,[‘成都’, …, ‘string’ ])。
bind 除了返回是函数以外,它 的参数和 call 一样。
参考
https://www.runoob.com/w3cnote/js-call-apply-bind.html
手写call、apply、bind
call实现思路主要是:
1.判断是否是函数调用,若非函数调用抛异常
2.通过新对象(context)来调用函数
3.给context创建一个fn设置为需要调用的函数
4.把arguments转换为真实数组,并解构来获取参数,同时传入用context创建好的函数中
5.结束调用完之后删除fn
Function.prototype.myCall=function (context){
//this对象不为函数类型时抛出错误
if(typeof this !== 'function')
{
throw new TypeError('Not a Function')
}
//不传参时默认为window
context = context || window
//保存函数方法
context.fn=this
//把伪数组转换成数组,同时忽略第一个参数,因为第一个参数为this指向的对象,并不是函数所需的参数
let args = Array.from(arguments).slice(1)
let result = context.fn(...args)
//删除
delete context.fn
return result
}
aplly实现思路主要是:
1.判断是否是函数调用,若非函数调用抛异常
2.通过新对象(context)来调用函数
3.给context创建一个fn设置为需要调用的函数
4.直接对arguments的第二个元素解构来获取参数
5.结束调用完之后删除fn
Function.prototype.myApply=function(context){
//判断是否是函数
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
return result
}
bind 主要实现思路
- 判断是否是函数调用,若非函数调用抛异常
- 返回函数
- 判断函数的调用方式,是否是被new出来的
- new出来的话返回空对象,但是实例的__proto__指向_this的prototype
- 完成函数柯里化
- Array.prototype.slice.call()
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))
}
}
}
测试用例
let name1= '小万'
let age=12
let obj ={
name1:'小张',
age:18,
myFun:function(from,to){
console.log(`${this.name1} 年龄:${this.age}从${from}到${to}`)
},
myFun1:function(){
console.log(`${this.name1} 年龄:${this.age}`)
}
}
let db={
name1:'小王',
age:22
}
obj.myFun('嘉兴','杭州') //小张 年龄:18从嘉兴到杭州
obj.myFun.myCall(db,'嘉兴','杭州') //小王 年龄:22从嘉兴到杭州
obj.myFun1.myCall(db) //小王 年龄:22
obj.myFun.myApply(db,['嘉兴','杭州']) //小王 年龄:22从嘉兴到杭州
obj.myFun1.myApply(db,['嘉兴','杭州']) //小王 年龄:22从嘉兴到杭州
闭包
为什么要有闭包?
我们都知道,js的作用域分两种,全局和局部,基于我们所熟悉的作用域链相关知识,我们知道在js作用域环境中访问变量的权利是由内向外的,内部作用域可以获得当前作用域下的变量并且可以获得当前包含当前作用域的外层作用域下的变量,反之则不能,也就是说在外层作用域下无法获取内层作用域下的变量,同样在不同的函数作用域中也是不能相互访问彼此变量的,那么我们想在一个函数内部也有限权访问另一个函数内部的变量该怎么办呢?闭包就是用来解决这一需求的,闭包的本质就是在一个函数内部创建另一个函数。
闭包特点:
- 函数嵌套函数
- 函数内部可以引用函数外部的参数和变量
- 参数和变量不会被垃圾回收机制回收
作用:
- 函数执行,形成私有的执行上下文,使内部私有变量不受外界干扰,起到保护和保存的作用
- 可使函数内引用用外部的变量不被垃圾回收机制回收
应用:
- 设计模式中的单例模式
- for循环中保留i的操作
- 防抖和节流
- 函数柯里化
缺点: - 使用不当会出现内存泄漏问题
①函数作为返回值
function fn(){
let num = 3
return function (){
let n =0
console.log(++n)
console.log(++num)
}
}
let fn1= fn()
fn1() //1 4
fn1() //1 5
一般情况下,在函数fn执行完后,就应该连同它里面的变量一同被销毁,但是在这个例子中,匿名函数作为fn的返回值被赋值给了fn1,这时候相当于fn1=function(){var n = 0 … },并且匿名函数内部引用着fn里的变量num,所以变量num无法被销毁,而变量n是每次被调用时新创建的,所以每次fn1执行完后它就把属于自己的变量连同自己一起销毁,于是乎最后就剩下孤零零的num,于是这里就产生了内存消耗的问题
②闭包作为参数传递
let num = 15
function fn1(x){
if(x>num){
console.log(x)
}
else
console.log(num)
}
void function (fn2){
var num =100
fn2(30)
}(fn1)
//作用域在创建时就已经产生了
在这段代码中,函数fn1作为参数传入立即执行函数中,在执行到fn2(30)的时候,30作为参数传入fn1中,这时候if(x>num)中的num取的并不是立即执行函数中的num,而是取创建函数的作用域中的num这里函数创建的作用域是全局作用域下,所以num取的是全局作用域中的值15,即30>15,打印30
reduce
参考
https://www.jianshu.com/p/e375ba1cfc47
执行栈和执行上下文
什么是作用域,什么是作用域链?
- 规定变量和函数的可使用范围称作作用域
- 每个函数都有一个作用域链,查找变量或者函数时,需要从局部作用域到全局作用域依次查找,这些作用域的集合称作作用域链。
什么是执行栈,什么是执行上下文?
执行上下文分为:
- 全局上下文
- 创建一个全局的window对象,并规定this指向window,执行js的时候就压入栈底,关闭浏览器的时候才弹出。
- 函数执行上下文
- 每次函数调用时,都会创建一个函数执行上下文
- 执行上下文分为创建阶段和执行阶段
- 创建阶段:
- 生成变量对象
- 创建arguments
- 扫描函数声明
- 扫描变量声明
- 建立作用域链
- 确定this的指向
- 生成变量对象
- 执行阶段
- 变量赋值
- 函数引用
- 执行其他代码
- 创建阶段:
- eval执行上下文
执行栈:
- 首先栈特点:先进后出
- 当进入一个执行环境,就会创建出它的执行上下文,然后进行压栈,当程序执行完成时,它的执行上下文就会被销毁,进行弹栈
- 栈底永远是全局环境的执行上下文,栈顶永远是正在执行函数的执行上下文
- 只有浏览器关闭的时候全局执行上下文才会弹出
例子
function getName() {
const year = getYear();
const name = 'Lynn';
console.log(`${name} ${year} years old this year`);
}
function getYear() {
return 18;
}
getName()
1.首先创建全局上下文,当前全局上下文处于活跃状态,把其放入执行栈的栈顶
2.当调用getName函数时,停止执行全局上下文,创建了新的函数上下文,且将其放入执行栈的栈顶,当前执行栈变的活跃。
3.当调用getYear()函数时,停止getName的函数上下文,将getYear()的函数上下文放入执行栈栈顶,getYear()处于活跃状态。
4.getYear函数执行完毕,将其上下文在执行栈顶出栈,回到getName()执行上下文中执行
5.getName函数执行完毕,将其上下文在执行栈顶出栈,回到全局全局上下文中。
参考:
https://zhuanlan.zhihu.com/p/72959191
原型和原型链定义
原型:原型链分为显式原型(prototype
)和隐式原型(__proto__
),每个对象都有一个隐式原型,它指向它的构造函数的显式原型。每个构造函数都有一个显式原型。
__proto__
是隐式原型;prototype是显式原型- 所有实例的
__proto__
都指向它们构造函数的prototype - 所有的prototype都是对象,自然它的
__proto__
指向的是Object()的prototype - 所有的构造函数的隐式原型指向的都是
Function()
的显式原型 - Object的隐式原型为null
为什么要使用原型?
分别在对象的构造函数显式原型中添加方法和直接在构造函数中添加对象函数,这两种方法作比较
function Foo1 (name:string,age:number){
this.name=name
this.age=age
}
//使用在Foo1构造函数的显式原型中添加showName方法
Foo1.prototype={
showName:function(){
console.log('foo1',this.name)
}
}
//直接在Foo2构造函数中添加一个showName函数对象
function Foo2 (name:string,age:number){
this.name=name
this.age=age
this.showName=function(){
console.log('foo2',this.name)
}
}
//当创建多个对象时,存放在构造函数prototype中的showName只创建一次
let foo1 =new Foo1('zlb',22)
foo1.showName()
//当创建多个对象时,创建出来的对象中都有showName方法,这样会非常占用资源
let foo2 =new Foo2('zlb',22)
foo2.showName()
原型链:多个隐式原型(__proto__
)组成的集合称为原型链,当一个对象寻找一个方法时,会根据隐式原型由下而上寻找
// 构造函数
function Foo(name,age){
this.name=name;
this.age=age;
}
Object.prototype.toString=function(){
let str ="I'm "+this.name+" And I'm "+this.age
return str
}
var fn=new Foo('zll',22);
console.log(fn.toString()) //I'm zll And I'm 19
console.log(fn.toString===Foo.prototype.__proto__.toString); //true
console.log(fn.__proto__ ===Foo.prototype)//true
console.log(Foo.prototype.__proto__===Object.prototype)//true
console.log(Object.prototype.__proto__===null)//true
//通过隐式原型从下而上寻找toString方法
console.log(fn.__proto__.__proto__.toString===fn.toString)//true
参考
https://blog.csdn.net/qq_36996271/article/details/82527256
继承
//父类
function Father(name){
this.name=name
this.age = 10
this.showName=function(){
console.log(name)
}
}
原型链继承
重点:让新实例的原型等于父类的实例。
特点:1、实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性。(新实例不会继承父类实例的属性!)
缺点:1、新实例无法向父类构造函数传参。
2、继承单一。(只能继承一个父类)
3、所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改!)
function Child1(){
this.name='child1'
}
Child1.prototype = new Father()
let child1 =new Child1()
console.log(child1.age,child1.name) //10 undefined
//child1 的隐式原型指向了父亲的显式原型,在同一条原型链上
console.log(child1 instanceof Father) //true
构造函数继承
重点:用.call()和.apply()将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))
特点:1、只继承了父类构造函数的属性,没有继承父类原型的属性。
2、解决了原型链继承缺点1、2、3。
3、可以继承多个构造函数属性(call多个)。
4、在子实例中可向父实例传参。
缺点:1、只能继承父类构造函数的属性。(父类原型上的属性不继承)
2、无法实现构造函数的复用。(每次用每次都要重新调用)
3、每个新实例都有父类构造函数的副本,臃肿。
//构造函数继承
function Child2(){
Father.call(this,'child2')
this.age =12
}
let child2 =new Child2()
console.log(child2.age,child2.name) //12 child2
//不在一条原型链上,从而
console.log(child2 instanceof Father) //false
组合继承
重点:结合了两种模式的优点,传参和复用
特点:1、可以继承父类原型上的属性,可以传参,可复用。
2、每个新实例引入的构造函数属性是私有的。
缺点:调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。
//组合继承
function Child3(name){
Father.call(this,name)
}
//为什么右边不用 Father.prototype ,因为这样就和父类同一个引用地址了
Child3.prototype = new Father();
let child3 = new Child3("child3")
let child31 = new Child3("child31")
child31.age=50
console.log(child3.age,child3.name) //10 child3
console.log(child3 instanceof Father) //true
console.log(child31.age,child31.name) //50 child3
原型式继承
与原型继承相同
重点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。object.create()就是这个原理。
特点:类似于复制一个对象,用函数来包装。
缺点:
1、所有实例都会继承原型上的属性。
2、无法实现复用。(新实例属性都是后面添加的)
3,不可传参
//原型式
function Child4(obj){
function F (){}
F.prototype= obj
return new F()
}
let father =new Father()
//用一个函数包装一个对象,然后返回这个对象,就是object.create()的原理
let child4 = new Child4(father)
//也可以直接使用Object中的create方法
let child41 = Object.create(father)
console.log(child4.age,child4.name) //10 undefined
//child1 的隐式原型指向了父亲的显式原型,在同一条原型链上
console.log(child4 instanceof Father) //true
console.log(child41.age,child41.name) //10 undefined
//child1 的隐式原型指向了父亲的显式原型,在同一条原型链上
console.log(child41 instanceof Father) //true
寄生式继承
重点:就是给原型式继承外面套了个壳子。
优点:
- 没有创建自定义类型,因为只是套了个壳子返回对象(这个),这个函数顺理成章就成了创建的新对象,。
- 每个实例都有自己的私有部分
- 可以添加参数
缺点:没用到原型,无法复用
//寄生式
let father5 = new Father()
function Child5(obj){
function F(){}
F.prototype=obj
return new F()
}
function subChild5(obj,name){
let subChild5 = Child5(obj)
subChild5.name = name
return subChild5
}
let child5 =subChild5(father,'child5')
console.log(child5.age,child5.name) //10 child5
console.log( child5 instanceof Father ) //true
寄生式组合继承
寄生:在函数内返回对象然后调用
组合:1、函数的原型等于另一个实例。2、在函数中用apply或者call引入另一个构造函数,可传参
重点:修复了组合继承的问题
//寄生式组合继承
function Child6(obj){
function F(){}
F.prototype = obj
return new F()
}
// childF 就是F实例的另一种表达方式
//为什么这里可以使用Father.prototype 因为在Child6函数中还要进行一次new的操作
let childF=Child6(Father.prototype)
//上述更像是原型链继承
function SubChild6(name){
Father.call(this,name)//继承了构造函数中的属性
}
SubChild6.prototype = childF //继承了父类的原型
childF.constructor =SubChild6 //给 childF构造函数赋值 ????
let child6 =new SubChild6('child6')
console.log(child6.age,child6.name) //10 child6
console.log( child6 instanceof Father ) //true
extend
// 子类只要继承父类,可以不写 constructor ,一旦写了,则在 constructor 中的第一句话
// 必须是 super 。
class Child7 extends Father { // Child7.prototype.__proto__ = Father.prototype
constructor(name) {
super(name) // super(200) => Father.call(this,name)
}
}
let child7 = new Child7('child7')
console.log(child7.age,child7.name) //10 child7
console.log( child7 instanceof Father ) //true
参考
https://www.cnblogs.com/ranyonsue/p/11201730.html
内存泄露、垃圾回收机制
什么是内存泄漏 ?
内存泄露是指不再用的内存没有被及时释放出来,导致该段内存无法被使用就是内存泄漏
为什么会导致的内存泄漏?
内存泄漏指我们无法在通过js访问某个对象,而垃圾回收机制却认为该对象还在被引用,因此垃圾回收机制不会释放该对象,导致该块内存永远无法释放,积少成多,系统会越来越卡以至于崩溃
什么情况会导致的内存泄漏?
1.意外的全局变量引起的内存泄漏。
原因:使用严格模式避免
2.闭包引起的内存泄漏
原因:闭包可以维持函数内局部变量,使其得不到释放。
解决:将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对dom的引用。
3. 没有清理的DOM元素引用
原因:虽然别的地方删除了,但是对象中还存在对dom的引用
解决:手动删除。
4.被遗忘的定时器或者回调
原因:定时器中有dom的引用,即使dom删除了,但是定时器还在,所以内存中还是有这个dom。
解决:手动删除定时器和dom。
5.子元素存在引用引起的内存泄漏
原因:div中的ul li 得到这个div,会间接引用某个得到的li,那么此时因为div间接引用li,即使li被清空,也还是在内存中,并且只要li不被删除,他的父元素都不会被删除。
解决:手动删除清空。
垃圾回收机制都有哪些策略?
标记清除法
工作原理: 是当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。
工作流程:
- 垃圾回收器,在运行的时候给存储在内存中的所有变量都添加商标去
- 去掉环境变量中的变量以及被环境变量中的变量引用的变量的标记
- 再被加上标记的变量会被视作准备删除的变量,原因是环境中的变量已经无法访问到这些变量了
- 销毁哪些标记的值并回收他们所占用的空间
引用计数法
工作原理:跟踪记录每个值被引用的次数。
工作流程:
- 声明了一个变量并将一个引用类型的值赋值给这个变量,这个引用类型值的引用次数就是1
- 如果同一个值又被赋值给了另一个变量,那这个引用类型的值的引用次数加1
- 如果引用类型的值的变量又被赋值成另一个值,那么这个引用类型值的引用次数就减一
- 当引用次数变成0时就表示没有办法再访问了
- 当垃圾收集器下一次运行,它就会释放引用次数为0的值的内存
缺点
但是循环引用的时候就会释放不掉内存。循环引用就是对象A中包含另一个指向对象B的指针,B中也包含一个指向A的引用。
因为IE中的BOM、DOM的实现使用了COM,而COM对象使用的垃圾收集机制是引用计数策略。所以会存在循环引用的问题。
缺点解决:手工断开js对象和DOM之间的链接。赋值为null。IE9把DOM和BOM转换成真正的JS对象了,所以避免了这个问题。
浅拷贝深拷贝
如何区分浅拷贝还是深拷贝?
假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着发生了变化,说明这是浅拷贝,如果B没有变就是深拷贝。原因是浅拷贝,A和B对应着同一个引用地址
//拷贝对象
var obj = {
a:{
a1:{
a11:1
},
a2:{
a21:2,
a22:{
a221:2
}
}
},
b:{
b1:1
}
}
浅拷贝
浅拷贝
//第一种方法
function shallowCopy1(obj){
let copy = Array.isArray(obj)?[]:{}
for(let key in obj)
{
copy[key]=obj[key]
}
return copy
}
// 方式2 解构
let shallowObj2 = { ...obj }
//方式3 使用Object.assign(targe,...sources)
let shallowObj3 = Object.assign({},obj)
let shallowObj1 = shallowCopy1(obj)
shallowObj1.b.b1=3
//第二层开始改变浅拷贝对象,其拷贝对象也发生改变
console.log(shallowObj1.b.b1) //3
console.log(obj.b.b1) //3
//第1层浅拷贝对象和拷贝对象互相独立 所以对第一层层是深拷贝,对第二层之后是浅拷贝
shallowObj1.a=1
console.log(shallowObj1.a) //1
console.log(obj.a) //{a1:{a11:11},a2:{a21:2,a22:{a221:2}}}
深拷贝
//深拷贝
//方法一 使用递归 判断对象和函数,解决循环引用问题
function isObject(obj){
let objType = Object.prototype.toString.call(obj)
return (objType === '[object Object]' || objType === '[object Array]')
}
function deepCopy1(obj,hash=new Map()){
let deepObj=Array.isArray(obj) ? [] : {}
let objType = Object.prototype.toString.call(obj)
//不为对象和数组的直接返回本身
if(!isObject(obj))
return obj
//防止循环引用 let a = {}; a.a=a; clone(a)
if(hash.has(obj)) return hash.get(obj)
hash.set(obj,deepObj)
for ( let key in obj )
{
//为对象或数组继续递归
if(isObject(obj[key])) deepObj[key] = deepCopy1(obj[key],hash)
//不为对象或数组,直接赋值
else deepObj[key] = obj[key]
console.log(deepObj)
}
return deepObj
}
let deepObj1 = deepCopy1(obj)
console.log(deepObj1)
//方法二 使用JSON.stringify()实现深拷贝
function deepCopy2(obj){
return JSON.parse(JSON.stringify(obj))
}
let deepObj2 = deepCopy2(obj)
console.log(deepObj2)
单线程,同步异步
为什么JS是单线程的?
如果js为多线程,当两个进程同时对同一个DOM元素进行操作时,一个执行删除,一个执行修改,那么这两个命令就会产生矛盾,从而浏览器就不知道该听谁的了
为什么JS需要异步?
如果JS不存在异步,只能从上到下执行,如果上一行解析需要很长时间,那么下面的代码就会被阻塞,对于用户而言就是卡死状态,从而让用户体验很差
JS通过什么实现了异步
通过事件循环机制(event loop)
事件循环机制
eventLoop就是沟通JS引擎线程和浏览器线程的桥梁,也是浏览器实现异步非阻塞模型的关键。
event loop 主要分三部分组成:主线程、宏队列、微队列
宏任务(macrotask):
- setTimeout
- setInterval
- setImmediate (Node独有)
- requestAnimationFrame (浏览器独有)
- I/O
- UI rendering (浏览器独有)
微任务(microtask):
- process.nextTick (Node独有)
- Promise.then
- Object.observe
- MutationObserver
浏览器EventLoop的具体流程:
- js引擎将所有代码放入执行栈,并依次弹出并执行,这些任务有的是同步有的是异步(宏任务或微任务)。
- 如果在执行 栈中代码时发现宏任务则交给浏览器相应的线程去处理,浏览器线程在正确的时机(比如定时器最短延迟时间)将宏任务的消息(或称之为回调函数)推入宏任务队列。而宏任务队列中的任务只有执行栈为空时才会执行。
- 如果执行 栈中的代码时发现微任务则推入微任务队列,和宏任务队列一样,微任务队列的任务也在执行栈为空时才会执行,但是微任务始终比宏任务先执行。
- 当执行栈为空时,eventLoop转到微任务队列处,依次弹出首个任务放入执行栈并执行,如果在执行的过程中又有微任务产生则推入队列末尾,这样循环直到微任务队列为空。
- 当执行栈和微任务队列都为空时,eventLoop转到宏任务队列,并取出队首的任务放入执行栈执行。需要注意的是宏任务每次循环只执行一个。
重复1-5过程
…直到栈和队列都为空时,代码执行结束。引擎休眠等待直至下次任务出现。
Node的 EventLoop的具体流程
1.执行全局Script的同步代码。
2.执行microtask微任务,先执行所有Next Tick Queue中的所有任务,再执行Other Microtask Queue中的所有任务。
3.执行macrotask宏任务,共6个阶段,从第1个阶段开始执行相应每一个阶段macrotask中的所有任务,注意,这里是所有每个阶段宏任务队列的所有任务,在浏览器的Event Loop中是只取宏队列的第一个任务出来执行,每一个阶段的macrotask任务执行完毕后,开始执行微任务,也就是步骤2。
4.Timers Queue -> 步骤2 -> I/O Queue -> 步骤2 -> Check Queue -> 步骤2 -> Close Callback Queue -> 步骤2 -> Timers Queue …
5.重复1 - 4过程。
chrome浏览器环境下
setTimeout(function() {
console.log('timeout1');
new Promise(function(resolve) {
console.log('timeout1_promise');
resolve();
}).then(function() {
console.log('timeout1_then')
})
})
setImmediate(function() {
console.log('immediate1');
new Promise(function(resolve) {
console.log('immediate1_promise');
resolve();
}).then(function() {
console.log('immediate1_then')
})
})
new Promise(function(resolve) {
console.log('glob1_promise');
resolve();
}).then(function() {
console.log('glob1_then')
})
setTimeout(function() {
console.log('timeout2');
new Promise(function(resolve) {
console.log('timeout2_promise');
resolve();
}).then(function() {
console.log('timeout2_then')
})
})
new Promise(function(resolve) {
console.log('glob2_promise');
resolve();
}).then(function() {
console.log('glob2_then')
})
setImmediate(function() {
console.log('immediate2');
new Promise(function(resolve) {
console.log('immediate2_promise');
resolve();
}).then(function() {
console.log('immediate2_then')
})
})
执行结果
glob1_promise
glob2_promise
glob1_then
glob2_then
immediate1
immediate1_promise
immediate1_then
immediate2
immediate2_promise
immediate2_then
timeout1
timeout1_promise
timeout1_then
timeout2
timeout2_promise
timeout2_then
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
async1()
console.log('script start')
//执行到await时,如果返回的不是一个promise对象,await会阻塞下面代码(当前async代码块的代码),会先执行async外的同步代码(在这之前先看看await中函数的同步代码,先把同步代码执行完),等待同步代码执行完之后,再回到async内部继续执行
//执行到await时,如果返回的是一个promise对象,await会阻塞下面代码(当前async代码块的代码),会先执行async外的同步代码(在这之前先看看await中函数的同步代码,先把同步代码执行完),等待同步代码执行完之后,再回到async内部等promise状态达到fulfill的时候再继续执行下面的代码
//所以结果为
//async1 start
//async2
//script start
//async1 end
node环境下(v10.15.3)
setTimeout(function() {
console.log('timeout1');
process.nextTick(function() {
console.log('timeout1_nextTick');
})
new Promise(function(resolve) {
console.log('timeout1_promise');
resolve();
}).then(function() {
console.log('timeout1_then')
})
})
setImmediate(function() {
console.log('immediate1');
process.nextTick(function() {
console.log('immediate1_nextTick');
})
new Promise(function(resolve) {
console.log('immediate1_promise');
resolve();
}).then(function() {
console.log('immediate1_then')
})
})
process.nextTick(function() {
console.log('glob1_nextTick');
})
new Promise(function(resolve) {
console.log('glob1_promise');
resolve();
}).then(function() {
console.log('glob1_then')
})
setTimeout(function() {
console.log('timeout2');
process.nextTick(function() {
console.log('timeout2_nextTick');
})
new Promise(function(resolve) {
console.log('timeout2_promise');
resolve();
}).then(function() {
console.log('timeout2_then')
})
})
process.nextTick(function() {
console.log('glob2_nextTick');
})
new Promise(function(resolve) {
console.log('glob2_promise');
resolve();
}).then(function() {
console.log('glob2_then')
})
setImmediate(function() {
console.log('immediate2');
process.nextTick(function() {
console.log('immediate2_nextTick');
})
new Promise(function(resolve) {
console.log('immediate2_promise');
resolve();
}).then(function() {
console.log('immediate2_then')
})
})
结果
glob1_promise
glob2_promise
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then
timeout1
timeout1_promise
timeout2
timeout2_promise
timeout1_nextTick
timeout2_nextTick
timeout1_then
timeout2_then
immediate1
immediate1_promise
immediate2
immediate2_promise
immediate1_nextTick
immediate2_nextTick
immediate1_then
immediate2_then
当promise后嵌套.then时
setTimeout(() => {
console.log('e')
})
Promise.resolve().then(() => {
console.log('a')
}).then(()=>{
return Promise.resolve('p1').then(data=>{
setTimeout(()=>{
console.log('x')
})
console.log('p2')
return data
})
}).then(data => {
console.log(da)
})
执行结果:
a
p2
p1
e
x
参考
https://www.jianshu.com/p/8e4584763159
https://www.cnblogs.com/yuanyingke/p/10280681.html
Promise
- 特点
- 对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:
- pending: 初始状态,不是成功或失败状态。
- fulfilled: 意味着操作成功完成。
- rejected: 意味着操作失败。
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。
- 对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:
- 作用
- 有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。
promise.all
- 用于将多个promise实例合并成一个新的promise实例
var p = Promise.all([p1,p2,p3]);
- p 的状态由 p1、p2、p3 决定,分成两种情况。
- 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
- 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
promise.race
- 用于将多个promise对象合并成一个promise对象
var p = Promise.race([p1,p2,p3]);
- 上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的返回值。
手写promise
class AlleyPromise{
// 1、Promise三种状态
static PENDING = 'PENDING';
static FULFILED = 'FULFILED';
static REJECTED = 'REJECTED';
constructor(callback){
// 容错处理
if(typeof callback !== 'function'){
throw new TypeError('Promise resolver undefined is not a function')
}
// 初始状态
this.promiseStatus = AlleyPromise.PENDING;
// 定义resolve函数队列 reject函数队列
this.resolveQueues = [];
this.rejectQueues = [];
//定义初始值
this.value;
//调用callback函数
callback(this._resolve.bind(this),this._reject.bind(this))
}
_resolve(val){
window.addEventListener('message',()=>{
// 更改成功状态
if(this.promiseStatus !== AlleyPromise.PENDING) return;
this.promiseStatus = AlleyPromise.FULFILED;
this.value = val;
let handler;
while(handler = this.resolveQueues.shift()){
handler(this.value)
}
})
window.postMessage('')
}
_reject(val){
window.addEventListener('message',()=>{
// 更改失败状态
if(this.promiseStatus !== AlleyPromise.PENDING) return;
this.promiseStatus = AlleyPromise.REJECTED;
this.value = val;
let handler;
while(handler = this.rejectQueues.shift()){
handler(this.value)
}
})
window.postMessage('')
}
then(resolveHandler,rejectHandler) {
return new AlleyPromise((resolve,reject)=>{
function newResolveHandler(val){
// 首先判断 resolveHandler是否是一个函数
if(typeof resolveHandler === 'function') {
/*
获取resolveHandler 函数的返回值进行判断
如果是promise则继续.then,不是则直接将结果返回
*/
let result = resolveHandler(val);
if(result instanceof AlleyPromise){
result.then(resolve,reject)
} else{
resolve(result);
}
} else {
resolve(val);
}
}
function newRejectHandler(val){
if(typeof rejectHandler === 'function') {
let result = rejectHandler(val);
if(result instanceof AlleyPromise){
result.then(resolve,reject)
}else{
reject(result);
}
} else {
reject(val);
}
}
this.resolveQueues.push(newResolveHandler)
this.rejectQueues.push(newRejectHandler)
})
}
catch(rejectHandler){
return this.then(undefined,rejectHandler)
}
static all(iterator){
let len = iterator.length;
let n = 0;
let vals = [];
return new AlleyPromise((resolve,reject)=>{
iterator.forEach((item)=>{
item.then((val)=>{
++n;
vals.push(val);
if(len === n) {
resolve(vals);
}
}).catch((e)=>{
reject(e);
})
})
})
}
static race(iterator){
return new AlleyPromise((resolve,reject)=>{
iterator.forEach((item)=>{
item.then((val)=>{
resolve(val);
}).catch((e)=>{
reject(e);
})
})
})
}
static resolve(val){
return new AlleyPromise((resolve)=>{
resolve(val)
})
}
static reject(val){
return new AlleyPromise((resolve,reject)=>{
reject(val)
})
}
}
参考
https://www.jianshu.com/p/2a0de70a841e
https://www.runoob.com/w3cnote/javascript-promise-object.html
变量提升
变量和函数怎么进行提升的?优先级是怎么样的?
- 函数变量声明
js enumerable(可枚举属性)
参考
https://segmentfault.com/a/1190000002953364
forEach 、for in 、for of 和的区别
forEach:
- 参数(当前元素,当前元素索引,当前元素所属数组)
- 不能进行continue、break 但可以使用some(有一个满足条件就返回true)和every(所有都满足条件就返回true)解决
- 在forEach回调函数中使用return,跳当前循环后面语句,进入到下一个循环
- 没有返回值
some: 对数组中每一项运行给定函数,如果该函数对任一项返回true,则返回true。
every: 对数组中每一项运行给定函数,如果该函数对每一项返回true,则返回true。
for-in:是ES5标准,遍历对象、数组或字符串的key(即索引)
- index索引为字符串型数字,不能直接进行几何计算。
- 遍历顺序有可能不是按照实际数组内部的顺序
- 使用for-in会遍历数组所有的可枚举属性,包括原型。例如上例的原型方法和method和name都会被遍历出来,通常要配合hasOwnProperty()方法判断某个属性是否该对象的实例属性,来将原型对象从循环中剔除。
for (var key in myObject) { if(myObject.hasOwnProperty(key)){ console.log(key); } }
- 从而for-in更适合遍历对象,同时不要使用for-in遍历数组
for-of:是ES6标准,可遍历对象、数组或字符串的value
-
- for-of可以简单遍历数组不会遍历所有可枚举属性
- 避开for-in循环的所有缺陷
- 适合遍历数组
- 可以遍历Map和Set
-
map:map() 方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。
参考
https://www.cnblogs.com/yanggb/p/11455127.html
改变原数组的方法以及不改变的方法
改变原数组:sort,splice,push,pop,shift,unshift,reverse
不改变数组:join,Slice,concat ,split
防抖和节流
防抖和节流:都是限制函数的执行次数
防抖:n秒后在执行该事件,若在n秒内被重复触发,则重新计时
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<button id="btn">提交</button>
<script>
const btnEl = document.getElementById('btn')
btnEl.addEventListener('click', debounce(fn, 3000, true), false)
function fn () {
console.log(this)
console.log(11111111)
}
function debounce (fn, delay, immediate) {
let timer = null
return function () {
//如果有计时器就清空
if (timer) {
//清除了计数器,但timer没有变为null
clearTimeout(timer)}
//立即执行时
if (immediate) {
//计时器为空时,立即执行
if(!timer) {
console.log(11111)
fn.apply(this.arguments)}
//延时计时器为空
timer = setTimeout(() => {
timer = null
}, delay)
}
//不立即执行时
else {
timer = setTimeout(() => {
fn.apply(this, arguments)
}, delay)
}
}
}
</script>
</body>
</html>
防抖:n秒内只运行一次,若在n秒内重复触发,只有一次生效
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<button id="btn">节流提交</button>
<script>
const btnEl = document.getElementById('btn')
btnEl.addEventListener('click', throttleByTimer(fn, 3000), false)
function fn () {
console.log(this)
console.log(11111111)
}
//节流通过时间戳
function throttleByDate (fn, delay) {
let oldTime = 0
return function () {
let newTime = new Date()
if (newTime - oldTime > delay) {
fn.apply(this, arguments)
oldTime = newTime
}
}
}
//节流通过计时器
function throttleByTimer (fn, delay) {
let timer = null
let first = true
return function () {
//为第一次点击时立刻执行
if (first) {
first = false
fn.apply(this, arguments)
}
//后续点击进行节流操作
else if (!timer && !first) {
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = null
}, delay);
}
}
}
</script>
<style>
</style>
</body>
</html>
ajax
var Ajax = {
get: function (url, callback) {
let xhr = XMLHttpRequest();
xhr.open("get", url, false)
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status == 200 || xhr.status == 304) {
console.log(xhr.responseText);
callback(xhr.responseText)
}
}
}
xhr.send()
},
post: function (url, data, callback) {
let xhr = new XMLHttpRequest()
// 第三个参数为是否异步执行
xhr.open('post', url, true)
// 添加http头,设置编码类型
xhr.setRequestHeader("Content-type","x-www-form-urlencoded")
xhr.onreadystatechange = function () {
if(xhr.readyState == 4) {
if(xhr.status == 200 || xhr.status == 304) {
console.log(xhr.responseText);
callback(xhr.responseText)
}
}
}
xhr.setRequestHeader('Content-type', "application/x-www-urlencoded")
xhr.send(data)
}
}
axois
优点:
1、从浏览器中创建XMLHttpRequests
2、从node.js创建http请求
3、支持PromiseAPI
4、拦截请求和响应
5、转换请求数据和响应数据
6、取消请求
7、自动转换JSON数据
8、客户端支持防御XSRF
axios支持请求方式
- axios.request(config)
//一般多用于获取数据 - axios.get(url[, config])
//用于删除 - axios.delete(url[, config])
- axios.head(url[, config])
// 主要提交表单数据和上传文件 - axios.post(url[, data[, config]])
//put对数据全部进行更新 - axios.put(url[, data[, config]])
//patch只对更改过的数据进行更新 - axios.patch(url[, data[, config]])
axois.all 和axios.spread
作用:解决同时请求多个接口,出现的高并发问题
//searchTopic(), searchs()为两个axois请求
axios.all([searchTopic(), searchs()])
.then(axios.spread(function (allSearchTopic, allSearchs) {
debugger//打印可以拿到所有的返回值
allSearchTopic == 方法一的返回值
allSearchs == 方法二的返回值
}));
如何取消请求
使用CancelToken构造函数把其回调函数中的c函数保存起来,而后使用只需调用
var CancelToken = axios.CancelToken;
var cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});
// 取消请求
cancel();
js常见的设计模式
-
单例模式、工厂模式、构造函数模式、发布订阅者模式、迭代器模式、代理模式
-
单例模式
- 不管创建多少个对象都只有一个实例
-
var Single = (function () { var instance = null function Single(name) { this.name = name } return function (name) { if (!instance) { instance = new Single(name) } 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()
-
代理模式
-
迭代器模式
commandJS规范
1.每个模块内部,module变量代表当前模块
2.modle变量是一个对象,它的exports属性(即moudle.exports)是对外接口
3.加载某个模块,其实就是加载该模块的modle.exports属性
正则表达式
参考