JavaScript
# JavaScript事件机制
DOM事件流(event flow )存在三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。
事件捕获(event capturing): 通俗的理解就是,当鼠标点击或者触发dom事件时,浏览器会从根节点开始由外到内进行事件传播,即点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件。
事件冒泡(dubbed bubbling):与事件捕获恰恰相反,事件冒泡顺序是由内到外进行事件传播,直到根节点。
dom标准事件流的触发的先后顺序为:先捕获再冒泡,即当触发dom事件时,会先进行事件捕获,捕获到事件源之后通过事件传播进行事件冒泡
事件冒泡
<html lang="zh-cn">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>js事件机制</title>
<style>
#parent{
width: 200px;
height:200px;
text-align: center;
line-height: 3;
background: green;
}
#child{
width: 100px;
height: 100px;
margin: 0 auto;
background: orange;
}
</style>
</head>
<body>
<div id="parent">
父元素
<div id="child">
子元素
</div>
</div>
<script type="text/javascript">
var parent = document.getElementById("parent");
var child = document.getElementById("child");
document.body.addEventListener("click",function(e){
console.log("click-body");
},false);
parent.addEventListener("click",function(e){
console.log("click-parent");
},false);
child.addEventListener("click",function(e){
console.log("click-child");
},false);
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
通过"addEventListener"方法,采用事件冒泡方式给dom元素注册click事件,点击子元素会发生什么呢?如果你对事件冒泡有一定了解的话那你肯定知道上面的代码会输出的顺序,没错,如下图所示:
这里有同学可能要问了,如果点击子元素不想触发父元素的事件怎么办?肯定可以的,那就是停止事件传播---event.stopPropagation(); 我们可以把代码修改为如下形式
child.addEventListener("click",function(e){
console.log("click-child");
e.stopPropagation();
},false);
2
3
4
事件捕获
var parent = document.getElementById("parent");
var child = document.getElementById("child");
document.body.addEventListener("click",function(e){
console.log("click-body");
},false);
parent.addEventListener("click",function(e){
console.log("click-parent---事件传播");
},false);
//新增事件捕获事件代码
parent.addEventListener("click",function(e){
console.log("click-parent--事件捕获");
},true);
child.addEventListener("click",function(e){
console.log("click-child");
},false);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
事件委托
下面举一个简单的栗子,通过js实现通过parent元素给child元素注册click事件
var parent = document.getElementById("parent");
var child = document.getElementById("child");
parent.onclick = function(e){
if(e.target.id == "child"){
console.log("您点击了child元素")
}
}
2
3
4
5
6
7
虽然没有直接只child元素注册click事件,可是点击child元素时却弹出了提示信息。
JavaScript 详说事件机制之冒泡、捕获、传播、委托 (opens new window)
# JavaScript原型链
下面这张图是重点
图中Parent是构造函数,p1是通过Parent实例化出来的一个对象。
# 构造函数和函数的区别
函数其实是对象的一种,任何函数都可以作为构造函数,但是并不能将任意函数叫做构造函数,只有当一个函数通过new关键字调用的时候才可以成为构造函数
var Parent = function(){
}
//定义一个函数,那它只是一个普通的函数,下面我们让这个函数变得不普通
var p1 = new Parent();
//这时这个Parent就不是普通的函数了,它现在是一个构造函数。因为通过new关键字调用了它
//创建了一个Parent构造函数的实例 p1
2
3
4
5
6
# 然后是三个重要属性
三个属性如下 __proto__
、prototype
、 constructor
。这三个属性有以下几个重要的注意事项
1.__proto__
、 constructor
属性是对象所独有的;
2.prototype
属性是函数独有的;
3.上面说过js中函数也是对象的一种,那么函数同样也有属性__proto__
、 constructor
;
# prototype属性
prototype
设计之初就是为了实现继承,让由特定函数创建的所有实例共享属性和方法,也可以说是让某一个构造函数实例化的所有对象可以找到公共的方法和属性。有了prototype
我们不需要为每一个实例创建重复的属性方法,而是将属性方法创建在构造函数的原型对象上(prototype)。那些不需要共享的才创建在构造函数中。
下面我贴一个代码
let Parent = function (){
console.log('hello')
}
Parent.prototype.name = "公共属性"
let p1 = new Parent()
console.log(p1.name)
console.log(p1.__proto__.name)
console.log(p1)
2
3
4
5
6
7
8
实际运行效果如下
# proto属性
__proto__
属性是对象(包括函数)独有的。从图中可以看到__proto__
属性是从一个对象指向另一个对象,即从一个对象指向该对象的原型对象(也可以理解为父对象)。显然它的含义就是告诉我们一个对象的原型对象是谁。
Parent.prototype
上添加的属性和方法叫做原型属性和原型方法,该构造函数的实例都可以访问调用。那这个构造函数的原型对象上的属性和方法,怎么能和构造函数的实例联系在一起呢,就是通过__proto__
属性。每个对象都有__proto__
属性,该属性指向的就是该对象的原型对象。比如上面那个图就很好的解释了这个个属性。
__proto__
通常称为隐式原型,prototype
通常称为显式原型,那我们可以说一个对象的隐式原型指向了该对象的构造函数的显式原型。那么我们在显式原型上定义的属性方法,通过隐式原型传递给了构造函数的实例。这样一来实例就能很容易的访问到构造函数原型上的方法和属性了。
我们之前也说过__proto__
属性是对象(包括函数)独有的,那么Parent.prototype
也是对象,那它有隐式原型么?有的,Parent的原型对象就继承了Object的原型对象。由此我们可以验证一个结论,万物继承自Object.prototype。这也就是为什么我们可以实例化一个对象,并且可以调用该对象上没有的属性和方法了。如:
//我们并没有在Parent中定义任何方法属性,但是我们可以调用
p1.toString();//hasOwnProperty 等等的一些方法
2
# 原形链
说了这些,其实我们就可以引出原形链的概念了
当我们调用p1.toString()
的时候,先在p1
对象本身寻找,没有找到则通过p1.__proto__
找到了原型对象Parent.prototype
,也没有找到,又通过Parent.prototype.__proto__
找到了上一层原型对象Object.prototype。在这一层找到了toString方法。返回该方法供p1
使用。
当然如果找到Object.prototype上也没找到,就在Object.prototype.__proto__
中寻找,但是Object.prototype.__proto__ === null
所以就返回undefined。这就是为什么当访问对象中一个不存在的属性时,返回undefined了。
# constructor属性
这个其实就是函数本身,比如下面这个代码,运行后会打出两个 hello
let Parent = function (){
console.log('hello')
}
let p1 = new Parent()
p1.constructor()
// Function的构造函数
console.log(Function.constructor)
2
3
4
5
6
7
constructor的作用是从一个对象指向一个函数,这个函数就是该对象的构造函数。通过栗子我们可以看到,p1
的constructor
属性指向了Parent
,那么Parent
就是p1
的构造函数。同样Parent
的constructor
属性指向了Function
,那么Function
就是Parent
的构造函数,然后又验证了Function
就是根构造函数。
一张图搞定JS原型&原型链 - SegmentFault 思否 (opens new window)
# JavaScript作用域和作用域链
# 作用域
在JavaScript中,我们可以将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找(这里的标识符,指的是变量名或者函数名)
- JavaScript中只有全局作用域与函数作用域(因为eval我们平时开发中几乎不会用到它,这里不讨论)。
- 作用域与执行上下文是完全不同的两个概念。我知道很多人会混淆他们,但是一定要仔细区分。
JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。
# 作用域链
JavaScript的生命周期如下
我们知道函数在调用激活时,会开始创建对应的执行上下文,在执行上下文生成的过程中,变量对象,作用域链,以及this的值会分别被确定。
作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。
比如下面这段代码,我们执行后会输出40
var a = 20;
function test() {
var b = a + 10;
function innerTest() {
var c = 10;
return b + c;
}
return innerTest();
}
console.log(test())
2
3
4
5
6
7
8
9
10
11
在上面的例子中,全局,函数test,函数innerTest的执行上下文先后创建。我们设定他们的变量对象分别为VO(global),VO(test), VO(innerTest)。而innerTest的作用域链,则同时包含了这三个变量对象,所以innerTest的执行上下文可如下表示。
innerTestEC = {
VO: {...}, // 变量对象
scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域链
}
2
3
4
我们可以直接用一个数组来表示作用域链,数组的第一项scopeChain[0]为作用域链的最前端,而数组的最后一项,为作用域链的最末端,所有的最末端都为全局变量对象。
很多人会误解为当前作用域与上层作用域为包含关系,但其实并不是。以最前端为起点,最末端为终点的单方向通道我认为是更加贴切的形容。如图。
注意,因为变量对象在执行上下文进入执行阶段时,就变成了活动对象,这一点在上一篇文章中已经讲过,因此图中使用了AO来表示。Active Object
是的,作用域链是由一系列变量对象组成,我们可以在这个单向通道中,查询变量对象中的标识符,这样就可以访问到上一层作用域中的变量了。
# 闭包
闭包是一种特殊的对象。它由两部分组成。执行上下文(代号A),以及在该执行上下文中创建的函数(代号B)。
当B执行时,如果访问了A中变量对象中的值,那么闭包就会产生。
比如下面这个例子
function foo() {
var a = 20;
var b = 30;
function bar() {
return a + b;
}
return bar;
}
var bar = foo();
console.log(bar())
2
3
4
5
6
7
8
9
10
会输出50,首先有执行上下文foo,在foo中定义了函数bar,而通过对外返回bar的方式让bar得以执行。当bar执行时,访问了foo内部的变量a,b。因此这个时候闭包产生。
var fn = null;
function foo() {
var a = 2;
function innnerFoo() {
console.log(a);
}
fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}
function bar() {
fn(); // 此处的保留的innerFoo的引用
}
foo();
bar(); // 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在上面的例子中,foo()
执行完毕之后,按照常理,其执行环境生命周期会结束,所占内存被垃圾收集器释放。但是通过fn = innerFoo
,函数innerFoo的引用被保留了下来,复制给了全局变量fn。这个行为,导致了foo的变量对象,也被保留了下来。于是,函数fn在函数bar内部执行时,依然可以访问这个被保留下来的变量对象。所以此刻仍然能够访问到变量a的值。
这样,我们就可以称foo为闭包。下图展示了闭包foo的作用域链。
我们可以在chrome浏览器的开发者工具中查看这段代码运行时产生的函数调用栈与作用域链的生成情况。如下图。
在上面的图中,红色箭头所指的正是闭包。其中Call Stack为当前的函数调用栈,Scope为当前正在被执行的函数的作用域链,Local为当前的局部变量。
**所以,通过闭包,我们可以在其他的执行上下文中,访问到函数的内部变量。**比如在上面的例子中,我们在函数bar的执行环境中访问到了函数foo的a变量。个人认为,从应用层面,这是闭包最重要的特性。利用这个特性,我们可以实现很多有意思的东西。