事件循环(Event Loop)
主要是分三部分:主线程、宏任务队列(macrotask)、微任务队列(microtask)
- 主线程,就是访问到的script标签里面包含的内容,或者是直接访问某一个js文件的时候,里面的可以在当前作用域直接执行的所有内容(执行的方法,new出来的对象等)
- 宏队列(
macrotask),setTimeout、setInterval、setImmediate、UI rendering等 - 微队列(
microtask),promise.then、mutaionObserver、process.nextTick等
执行顺序:
- 先执行主线程 =>
- 遇到宏任务放到宏队列(macrotask)=>
- 遇到微任务放到微队列(microtask)=>
- 主线程执行完毕,调用栈被清空,这个时候就会从微队列里取出位于首位的回调放入执行栈开始执行,微队列长度-1,然后依次执行队列里的回调任务直到所有任务被执行完毕,此时微队列为空,调用栈也为空=>
- 这时再从宏队列里取出位于首位的一个任务,然后放入调用栈执行,执行完毕之后,再去取微队列里的任务,按照之前的步骤循环。
作用域
什么是作用域?
作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。 换句话说,作用域决定了代码区块中变量和其他资源的可见性。
ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6的到来,为我们提供了‘块级作用域’,可通过新增命令let和const来体现。
全局作用域和函数作用域
1. 全局作用域
- 最外层函数 和在最外层函数外面定义的变量拥有全局作用域
- 所有未定义直接赋值的变量自动声明为拥有全局作用域
- 所有window对象的属性拥有全局作用域
2. 函数作用域
指声明在函数内部的变量,和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到。 作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行。
3. 块级作用域
块级作用域可通过新增命令let和const声明,所声明的变量在指定块的作用域外无法被访问。
3.1. 块级作用域在如下情况被创建:
- 在一个函数内部
- 在一个代码块(由一对花括号包裹)内部
3.2. 块级作用域有以下几个特点:
- 声明变量不会提升到代码块顶部
- 禁止重复声明
- 循环中的绑定块作用域的妙用,变量i 每一轮都是一个新的变量,可在内部引用
在ES5中,立即执行函数用来模拟块级作用域
4.作用域链
一级一级朝上查找变量的过程。变量朝上查找要到创建这个函数的“域”中查找。
5.作用域和执行上下文
作用域和执行上下文之间最大的区别是: 执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变。
一个作用域下可能包含若干个上下文环境。有可能从来没有过上下文环境(函数从来就没有被调用过);有可能有过,现在函数被调用完毕后,上下文环境被销毁了;有可能同时存在一个或多个(闭包)。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。
原型、原型链、继承
原型/构造函数/实例
- 原型(prototype): 一个简单的对象,用于实现对象的 属性继承。可以简单的理解成对象的爹。在 Firefox 和 Chrome 中,每个JavaScript对象中都包含一个__proto__ (非标准)的属性指向它爹(该对象的原型),可obj.__proto__进行访问。
- 构造函数: 可以通过new来 新建一个对象 的函数。
- 实例: 通过构造函数和new创建出来的对象,便是实例。 实例通过__proto__指向原型,通过constructor指向构造函数。
实例.__proto__ === 原型
原型.constructor === 构造函数
构造函数.prototype === 原型
// 这条线其实是是基于原型进行获取的,可以理解成一条基于原型的映射线
// 例如:
// const o = new Object()
// o.constructor === Object --> true
// o.__proto__ = null;
// o.constructor === Object --> false
实例.constructor === 构造函数
原型链
如果试图引用对象(实例instance)的某个属性,会首先在对象内部寻找该属性,直至找不到,然后才在该对象的原型(instance.prototype)里去找这个属性。
举例来说
constructor1.prototype = instance2
如果试图引用constructor1构造的实例instance1的某个属性p1:
- 首先会在instance1内部属性中找一遍;
- 接着会在
instance1.__proto__(constructor1.prototype)中找一遍,而constructor1.prototype实际上是instance2, 也就是说在instance2中寻找该属性p1; - 如果instance2中还是没有,此时程序不会灰心,它会继续在
instance2.__proto__(constructor2.prototype)中寻找...直至Object的原型对象
搜索轨迹: instance1--> instance2 --> constructor2.prototype…-->Object.prototype
这种搜索的轨迹,形似一条长链, 又因prototype在这个游戏规则中充当链接的作用,于是我们把这种实例与原型的链条称作 原型链 。
确定原型和实例的关系
使用原型链后, 我们怎么去判断原型和实例的这种继承关系呢? 方法一般有两种。
instanceof
第一种是使用 instanceof 操作符, 只要用这个操作符来测试实例(instance)与原型链中出现过的构造函数,结果就会返回true.
以下几行代码就说明了这点.
alert(instance instanceof Object);//true
alert(instance instanceof Father);//true
alert(instance instanceof Son);//true
由于原型链的关系, 我们可以说instance 是 Object, Father 或 Son中任何一个类型的实例. 因此, 这三个构造函数的结果都返回了true.
isPrototypeOf()
第二种是使用 isPrototypeOf() 方法, 同样只要是原型链中出现过的原型,isPrototypeOf() 方法就会返回true, 如下所示:
alert(Object.prototype.isPrototypeOf(instance));//true
alert(Father.prototype.isPrototypeOf(instance));//true
alert(Son.prototype.isPrototypeOf(instance));//true
继承
1. 借用构造函数(constructor stealing)(经典继承)
基本思想:即在子类型构造函数的内部调用超类型构造函数.
function Father(){
this.colors = ["red","blue","green"];
}
function Son(){
Father.call(this);//继承了Father,且向父类型传递参数
}
var instance1 = new Son();
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
var instance2 = new Son();
console.log(instance2.colors);//"red,blue,green"
可见引用类型值是独立的 很明显,借用构造函数一举解决了原型链的两大问题:
-
保证了原型链中引用类型值的独立,不再被所有实例共享;
-
子类型创建时也能够向父类型传递参数
随之而来的是, 如果仅仅借用构造函数,那么将无法避免构造函数模式存在的问题:
- 方法都在构造函数中定义, 因此函数复用也就不可用了
- 超类型(如Father)中定义的方法,对子类型而言也是不可见的.
考虑此,借用构造函数的技术也很少单独使用。
2. 组合继承(伪经典继承)
将原型链和借用构造函数的技术组合到一块,从而发挥两者之长的一种继承模式.
基本思路: 使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承.
这样既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性
function Father(name){
this.name = name;
this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function(){
alert(this.name);
};
function Son(name,age){
Father.call(this,name);//继承实例属性,第一次调用Father()
this.age = age;
}
Son.prototype = new Father();//继承父类方法,第二次调用Father()
Son.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new Son("louis",5);
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
instance1.sayName();//louis
instance1.sayAge();//5
var instance1 = new Son("zhai",10);
console.log(instance1.colors);//"red,blue,green"
instance1.sayName();//zhai
instance1.sayAge();//10
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式. 而且, instanceof 和 isPrototypeOf( )也能用于识别基于组合继承创建的对象.
同时我们还注意到组合继承其实调用了两次父类构造函数, 造成了不必要的消耗, 那么怎样才能避免这种不必要的消耗呢?
3. 寄生式继承
创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象.
function createAnother(original){
var clone = object.create(original);//通过调用object函数创建一个新对象
clone.sayHi = function(){//以某种方式来增强这个对象
alert("hi");
};
return clone;//返回这个对象
}
这个例子中的代码基于person返回了一个新对象--anotherPerson. 新对象不仅具有 person 的所有属性和方法, 而且还被增强了, 拥有了sayHi()方法. 注意: 使用寄生式继承来为对象添加函数, 会由于不能做到函数复用而降低效率,这一点与构造函数模式类似。
4. 寄生组合式继承
前面讲过,组合继承是 JavaScript 最常用的继承模式; 不过, 它也有自己的不足. 组合继承最大的问题就是无论什么情况下,都会调用两次父类构造函数: 一次是在创建子类型原型的时候, 另一次是在子类型构造函数内部。
寄生组合式继承就是为了降低调用父类构造函数的开销而出现的。 其背后的基本思路是: 不必为了指定子类型的原型而调用超类型的构造函数 先通过调父类的构造函数继承属性,然后创建父类原型的副本,为创建的副本添加constructor属性,指向子类型,最后 将创建的对象副本赋值给子类型的原型。
这样就只用调用一次 父类的构造函数。
function extend(subClass,superClass){
var prototype = object.create(superClass.prototype);//创建对象
prototype.constructor = subClass;//增强对象
subClass.prototype = prototype;//指定对象
}
继承原型上的属性和方法时,没有调用superClass构造函数,因此避免了在subClass.prototype上面创建不必要多余的属性. 于此同时,原型链还能保持不变; 因此还能正常使用 instanceof 和 isPrototypeOf() 方法.
以上,寄生组合式继承,集寄生式继承和组合继承的优点于一身,是实现基于类型继承的最有效方法。
下面我们来看下extend的另一种更为有效的扩展.
function extend(subClass, superClass) {
var F = function() {};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
subClass.superclass = superClass.prototype;
if(superClass.prototype.constructor == Object.prototype.constructor) {
superClass.prototype.constructor = superClass;
}
}
我一直不太明白的是为什么要 "new F()", 既然extend的目的是将子类型的 prototype 指向超类型的 prototype,为什么不直接做如下操作?
subClass.prototype = superClass.prototype;//直接指向超类型prototype
显然, 基于如上操作, 子类型原型将与超类型原型共用, 根本就没有继承关系。
es6的语法糖class/extend
具体的实现原理,其实是一个寄生组合继承方式。
主要核心 就是 创建一个父类prototype的对象副本,并将这个对象副本指向子类构造函数的prototype,将父构造函数指向子构造函数的_proto_,然后用一个闭包做子类的增强,用当前this调用父类构造函数,返回一个已经被父类赋值完的this,之后就是class相同的原理,执行子类class内部的变量和函数赋给this,执行子类constructor内部逻辑。
从这里也可以看出为什么子类一定要在在构造方法内执行super,因为class继承的底层是寄生组合模式,子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
new都干了什么?
可以描述new一个对象的详细过程,手动实现一个new操作符
- 创建一个空对象(var o=new Object();)
- 将空对象的原型赋值为构造函数Constructor的原型(o.proto=Constructor.prototype;)
- 执行构造函数中的代码,为空对象添加属性(Constructor.call(o);),也可以理解为将构造器函数内部this指向新建的空对象。
- 返回添加属性后的对象。
function New(obj){
var o = new Object(),
Constructor = obj;
o.__proto__ = Constructor.prototype;
// FF 支持用户引用内部属性 [[Prototype]]
Constructor.call(o);
return o;
}
属性查找
避免原型链查找, 建议使用 hasOwnProperty 方法。
因为 hasOwnProperty 是 JavaScript 中唯一一个处理属性但是不查找原型链的函数。
对比: isPrototypeOf 则是用来判断该方法所属的对象是不是参数的原型对象,是则返回true,否则返回false。
instanceof
instanceof 可以判断一个引用是否属于某构造函数,还可以在继承关系中用来判断一个实例是否属于它的父类型。
instanceof的判断逻辑是:
从当前引用的proto一层一层顺着原型链往上找,能否找到对应的prototype。找到了就返回true。
function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
var O = R.prototype; // 取 R 的显示原型
L = L.__proto__; // 取 L 的隐式原型
while (true) {
if (L === null)
return false;
if (O === L) // 当 O 显式原型 严格等于 L隐式原型 时,返回true
return true;
L = L.__proto__;
}
}
访问对象属性的方法,以及是否是原型链属性
在JavaScript中,可以使用“ . ”和“ [ ] ”来访问对象的属性。
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && (name in object)
}
原理:
hasOwnproperty()方法会返回一个布尔值,检测属性是否存在于对象中(实例),但不能检测原型中的属性
in操作符 如果指定的属性在指定的对象或其原型链中,则 in 运算符返回true
如何使属性不可改变
1.对象常量
结合writable: false和configurable:false就可以创建一个真正的常量属性(不可修改、重定义或者删除):
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 23, writable: false, configurable: false
});
2.禁止扩展
如果想要禁止一个对象添加新属性并且保留已有属性,可以使用Object.preventExtensions(…)
var myObject = { a: 2 };
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined,非严格模式下 !!严格模式下,将抛出TypeError错误
3.密封
Object.seal(…)会创建一个“密封”的对象,实际上是会在一个现有对象上调用Object.preventExtensions(…)并把所有现有属性标记为configurable: false。
所以,密封后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(但是可以修改属性的值)。
4.冻结
Object.freeze(…)会创建一个冻结对象,实际上会在一个现有对象上调用Object.seal(…)并把所有“数据访问”属性标记为writable: false,这样就无法修改它们的值了。
跨域
跨域的原因就是浏览器的同源策略
1.同源策略
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。 浏览器是从两个方面去做这个同源策略的,一是针对接口的请求,二是针对Dom的查询。 不同协议,不同端口,不同域名(主域名不同或者主域名相同下子域名不同)均为跨域。
2.跨域的方法
2.1 JSONP
动态创建script标签,然后定义回调函数
<script url=‘http://xxx.xxx.com/xxx/xxx?param=xxx&callback=xxxx'></script>
但是jsonp只能发get请求
2.2空iframe+form
2.3.CORS
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下两大条件,就属于简单请求。
(1) 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
(2)HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
2.4.nginx反向代理
2.5postMessage
跨域传递消息,但是不是接口请求
垃圾回收机制
为什么会有垃圾回收机制?
JS 程序每次创建字符串,数组,对象时,解释器都要动态给他们分配内存空间来存储这个实体。像这样需要动态来分配内存空间的,最终都要释放这个内存,以便这个内存能够被再次使用、不然的话,JS 解释器会消耗完系统中可用的内存。造成内存泄漏。
老垃圾回收机制
常用的有两种:
1. 标记清除
当变量进入执行环境,就会被标记为进入环境,离开环境后会被标记为离开环境,在环境中的变量不会被释放,因为随时可能被用到,垃圾收集器在运行时会给内存中所有的变量加上标记,但是如果是环境中的变量或者被环境中的变量引用的变量会被去除标记。标记完之后,垃圾收集器就会销毁这些带标记的变量,回收他们所占的内存。
2. 引用计数
引用计数的意思是说,当声明一个变量后,这个变量指向的内存地址就会标记为1(因为被这个值被引用了一次)如果还有变量等于这个值,就会再加1,相反就会减1。当等于0时,就表示没有被引用了,就会被回收。这种方法有缺陷。如果两个变量相互引用时,就会导致标记值永远不会等于0,内存永远不会被回收。如果很多就会出内存泄漏。
新垃圾回收机制
V8 划分新生代和老生代,node垃圾回收机制是基于V8引擎的内存管理机制
新生代
新生代用来存放一些存活时间较短的对象,老生代存放一些存活时间较长的对象,
新生代划分为from to两个区域,在使用的是from,空闲的是to,如果某个变量要被回收,就会留在from,其他所有的转到to,然后交换两个空间,然后垃圾清理时,清理掉to的空间
如果在新生代空间里发现某个变量被清理过,会把他移到老生代里,认为是存活时间长的对象 在 From 空间和 To 空间进行反转的过程中,如果 To 空间中的使用量已经超过了 25%,那么就将 From 中的对象直接晋升到老生代内存空间中。
老生代
老生代空间里没有区域划分,他的垃圾回收是标记清除和标记合并。
1. 标记清除
当变量进入执行环境,就会被标记为进入环境,离开环境后会被标记为离开环境,在环境中的变量不会被释放,因为随时可能被用到,垃圾收集器在运行时会给内存中所有的变量加上标记,但是如果是环境中的变量或者被环境中的变量引用的变量会被去除标记。标记完之后,垃圾收集器就会销毁这些带标记的变量,回收他们所占的内存。
2. 标记合并
和新生代的划分有点类似,把还在存活的对象收集起来,需要被清除的收集起来,然后整体回收。
闭包
1.概念
闭包是函数和声明该函数的词法环境的组合。这个环境包含了这个闭包创建时所能访问的所有局部变量。 闭包就是一个函数,这个函数能够访问其他函数的作用域中的变量。
2.闭包的用途
- 读取函数内部的变量
- 让这些变量的值始终保持在内存中。
3.缺陷
- 闭包对脚本性能有负影响,不能滥用
- 闭包会在父函数外部,改变父函数内部变量的值
4.实际用例
1. 给对象设置私有变量并且利用特权方法去访问私有属性
function Fun(){
var name = 'tom';
this.getName = function (){
return name;
}
}
var fun = new Fun();
console.log(fun.name);//输出undefined,在外部无法直接访问name
console.log(fun.getName());//可以通过特定方法去访问
2. 采用函数引用方式的setTimeout调用
//原生的setTimeout传递的第一个函数不能带参数
setTimeout(function(param){
alert(param)
},1000)
//通过闭包可以实现传参效果
function func(param){
return function(){
alert(param)
}
}
var f1 = func(1);
setTimeout(f1,1000);
3. 单例模式
loading/jquery/vuex-store/reduc-store都可以借鉴 实行单一职责,确保全局只有一个实例对象。
// 获取单个实例,惰性单例
var getInstance = function(fn) {
var result; // 因为是闭包,所以这个变量一直都在,不会每次都重新赋值,下一次进来的时候result已经被赋值过了的,直接只会执行return出去方法
return function(){
return result || (result = fn.call(this,arguments));
}
};
形参fn是我们的构造函数,我们只要传入任何自己需要的构造函数,就能生成一个新的惰性单例。比如说传入创建一个女朋友的构造函数,并且调用getSingle(),就能生成一个新的女朋友。如果以后再调getSingle(),也只会返回刚才创建的那个女朋友。至于新女朋友——不存在的。
4. module仿模块化
使用闭包封装“私有”状态和组织。只用返回一个公有的API,其他的所有规则都在私有闭包里,防止泄露到全局作用域,并且可以减少与别的开发人员的接口发生冲突。
var myNameSpace = (function() {
// 私有计数器变量
var myPrivateCar = 0
// 记录所有参数的私有函数
var myPrivateMethod = function(foo) {
console.log(foo)
}
return {
// 公有变量
myPublicVar: ‘foo’,
// 调用私有变量和方法的公有函数
myPublicFunction: function(bar) {
myPrivateVar ++
myPrivateMethod(bar)
}
}
})()
防抖与节流
防抖,固定时间段内没有操作了,才执行,在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
节流,持续在操作,每个固定时间段执行一次,
防抖与节流函数是一种最常用的 高频触发优化方式,能对性能有较大的帮助。
防抖 (debounce)
将多次高频操作优化为只在最后一次执行,通常使用的场景是:用户输入,只需再输入完成后做一次输入校验即可。
function debounce(fn, wait, immediate) {
let timer = null
return function() {
let args = arguments
let context = this
if (immediate && !timer) {
fn.apply(context, args)
}
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(context, args)
}, wait)
}
}
节流(throttle)
每隔一段时间后执行一次,也就是降低频率,将高频操作优化成低频操作,通常使用场景: 滚动条事件 或者 resize 事件,通常每隔 100~500 ms执行一次即可。
function throttle(fn, wait, immediate) {
let timer = null
let callNow = immediate
return function() {
let context = this,
args = arguments
if (callNow) {
fn.apply(context, args)
callNow = false
}
if (!timer) {
timer = setTimeout(() => {
fn.apply(context, args)
timer = null
}, wait)
}
}
}
this 一图胜千言
浅拷贝和深拷贝
浅拷贝
可以通过 Object.assign 或者 ... 扩展运算符来实现,但是有缺陷,如果属性值依然是对象的话,还是拷贝的地址引用。
深拷贝
常见做法是 JSON.parse(JSON.stringify(object))
原理是消除引用地址,再新建引用地址,但是这个也有缺陷:
- 会忽略 undefined
- 会忽略 symbol
- 不能序列化函数
- 不能解决循环引用的对象
let obj = {
a: 1,
b: {
c: 2,
d: 3,
},
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)
在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化
let a = {
age: undefined,
sex: Symbol('male'),
jobs: function() {},
name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}
你会发现在上述情况中,该方法会忽略掉函数和 undefined 。
更推荐使用 lodash 的深拷贝函数
简易的深拷贝:
function deepClone(obj) {
// 判断是不是对象或函数
function isObject(o) {
return (typeof o === 'object' || typeof o === 'function') && o !== null
}
// 非对象不处理
if (!isObject(obj)) {
throw new Error('非对象')
}
// 是不是数组
let isArray = Array.isArray(obj)
// 拷贝当前层
let newObj = isArray ? [...obj] : { ...obj }
Reflect.ownKeys(newObj).forEach(key => {
// 属性值是对象接着深拷贝
newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
})
return newObj
}
let obj = {
a: [1, 2, 3],
b: {
c: 2,
d: 3
}
}
let newObj = deepClone(obj)
newObj.b.c = 1
console.log(obj.b.c) // 2
事件处理机制
一次事件的发生包含三个过程:1. 事件捕获阶段,2. 事件目标阶段,3. 事件冒泡阶段
事件捕获
当某个元素触发某个事件(如onclick),顶层对象document就会发出一个事件流,随着DOM树的节点向目标元素节点流去,直到到达事件真正发生的目标元素。在这个过程中,事件相应的监听函数是不会被触发的。
事件目标
当到达目标元素之后,执行目标元素该事件相应的处理函数。如果没有绑定监听函数,那就不执行。
事件冒泡
从目标元素开始,往顶层元素传播。途中如果有节点绑定了相应的事件处理函数,这些函数都会被一次触发。
事件的传播是可以阻止的:
- 在W3c中,使用stopPropagation()方法
- 在IE下设置eve.cancelBubble = true;
事件委托
事件委托是利用事件的冒泡原理来实现的,何为事件冒泡呢?
就是事件从最深的节点开始,然后逐步向上传播事件,举个例子:
页面上有这么一个节点树,div>ul>li>a;比如给最里面的a加一个click点击事件,那么这个事件就会一层一层的往外执行,执行顺序a>li>ul>div,有这样一个机制,那么我们给最外面的div加点击事件,那么里面的ul,li,a做点击事件的时候,都会冒泡到最外层的div上,所以都会触发,这就是事件委托,委托它们父级代为执行事件。
那什么样的事件可以用事件委托,什么样的事件不可以用呢?
适合用事件委托的事件:click,mousedown,mouseup,keydown,keyup,keypress。
值得注意的是,mouseover和mouseout虽然也有事件冒泡,但是处理它们的时候需要特别的注意,因为需要经常计算它们的位置,处理起来不太容易。
不适合的就有很多了,举个例子,mousemove,每次都要计算它的位置,非常不好把控,在不如说focus,blur之类的,本身就没用冒泡的特性,自然就不能用事件委托了。
函数的尾递归和尾调用
尾调用
尾调用之所以与其他调用不同,就在于它的特殊调用位置。
一般来说,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果还是内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就会形成一个“调用栈”(call stack)
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置,内部变量等信息都不会再用到,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了
function f() {
let m = 1;
let n = 2;
return g(m + n)
}
//等同于
function f() {
return g(3);
}
f()
//等同于
g(3)
上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量m和n的值,但是由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除f(x)的调用帧,只保留g(3)的调用帧
这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧,如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存,这就是“尾调用优化”的意义
只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则无法进行“尾调用优化”
尾递归
尾递归:函数调用自身,称为递归。如果尾调用自身就称为尾递归。
递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但是对于尾递归来说,由于只存在一个调用帧。所以永远不会发生“栈溢出”错误
一个比较著名的例子,就是计算Fibonacci数列(斐波那契数列),也能充分说明尾递归优化的重要性
function Fibonacci(n) {
if (n <= 1) { return 1 };
return Fibonacci(n - 1) + Fibonacci(n - 2)
}
console.log(Fibonacci(10)); //89
console.log(Fibonacci(100)); //堆栈溢出
尾调用优化过后的Fibonacci数列实现如下
function Fibonacci(n, ac1 = 1, ac2 = 1) {
if (n <= 1) { return ac2 };
return Fibonacci(n - 1, ac2, ac1 + ac2);
}
console.log(Fibonacci(10)); //89
console.log(Fibonacci(30)); //1346269
console.log(Fibonacci(100)); //10946