本文共 23150 字,大约阅读时间需要 77 分钟。
温故而知新,保持空杯心态
object number function boolean undefined string
typeof null; // objecttypeof isNaN; // functiontypeof isNaN(123); //booleantypeof []; // objectArray.isArray(); // falsetoString.call([]); // [object Array]var arr = [];arr.constructor; // ƒ Array() { [native code] }
js 提供了以下几种转型函数:
转换的类型 | 函数 |
---|---|
数值类型 | Number(mix),parseInt(string,radix),parseFloat(string); |
字符串类型 | toString(radix),String(mix) |
布尔类型 | Boolean(mix) |
true
和 false
分别被转换为 1 和 0null
,返回 0undefined
,返回 NaN
valueOf()
方法,然后按照前面的规则进行转换返回的值,如果是转换结果是 NaN
,则调用对象的 toString()
方法,然后再一次按照前面的规则进行返回的字符串值的转换下表是对象的 valueOf() 的返回值
对象 | 返回值 |
---|---|
Array | 数组的元素被转换为字符串,这些字符串由逗号分隔,连接在一起。其操作与 Array.toString 和 Array.join 方法相同。 |
Boolean | Boolean 值。 |
Date | 存储的时间是从 1970 年 1 月 1 日午夜开始计的毫秒数 UTC。 |
Function | 函数本身。 |
Number | 数字值。 |
Object | 对象本身。这是默认情况。 |
String | 字符串值。 |
由于 Number()函数在转换字符串时原理比较复杂,且不够合理,因此在处理字符串时,更常用的是 parseInt()
函数
NaN
(也就是遇到空字符会返回 NaN)与parseInt()函数类似,parseFloat()也是从第一个字符(位置0)开始解析每个字符。而且也是一直解析到字符串末尾,或者解析到遇见一个无效的浮点数字字符为止。也就是说,字符串中的第一个小数点是有效的,而第二个小数点就是无效的了,因此它后面的字符串将被忽略。
除 undefined
和 null
之外的所有类型的值都具有 toString()
方法,其作用是返回对象的字符串表示。
多数情况下,调用toString()方法不必传递参数。但是,在调用数值的toString()方法时,可以传递一个参数:输出数值的基数。默认情况下,toString()方法以十进制格式返回数值的字符串表示。
对象 | 操作 |
---|---|
Array | 将 Array 的元素转换为字符串。结果字符串由逗号分隔,且连接起来。 |
Boolean | 如果 Boolean 值是 true,则返回 “true”。否则,返回 “false”。 |
Date | 返回日期的文字表示法。 |
Error | 返回一个包含相关错误信息的字符串。 |
Function | 返回如下格式的字符串,其中 functionname 是被调用 toString 方法函数的名称:function functionname( ) { [native code] } |
Number | 返回数字的文字表示。 |
String | 返回 String 对象的值。 |
默认 | 返回 “[object objectname]”,其中 objectname 是对象类型的名称。 |
在不知道要转换的值是不是null或undefined的情况下,还可以使用转型函数String(),这个函数能够将任何类型的值转换为字符串。
以下值会被转换为false
:false、”"、0、NaN、null、undefined,其余任何值都会被转换为true
。
在某些情况下,即使我们不提供显示转换,Javascript也会进行自动类型转换,主要情况有:
isNaN()
函数,经测试发现,该函数会尝试将参数值用 Number()
进行转换,如果结果为“非数值”则返回 true
,否则返回 false
。
Number()
),再执行加减1的操作,字符串变量变为数值变量。NaN
,字符串变量变成数值变量。false
,先将其转换为0再执行加减1的操作,布尔值变量编程数值变量。true
,先将其转换为1再执行加减1的操作,布尔值变量变成数值变量。valueOf()
方法,然后对该返回值应用前面的规则。如果结果是 NaN
,则调用 toString()
方法后再应用前面的规则。对象变量变成数值变量。加号运算操作符在Javascript也用于字符串连接符,所以加号操作符的规则分两种情况:
如果两个操作值都是数值,其规则为:
NaN
,则结果为 NaN
Infinity+Infinity
,结果是 Infinity
-Infinity+(-Infinity)
,结果是 -Infinity
Infinity+(-Infinity)
,结果是 NaN
+0+(+0)
,结果为 +0
(-0)+(-0)
,结果为 -0
(+0)+(-0)
,结果为 +0
如果有一个操作值为字符串,则:
可以看出,加法运算中,如果有一个操作值为字符串类型,则将另一个操作值转换为字符串,最后连接起来。
这些操作符针对的是运算,所以他们具有共同性:如果操作值之一不是数值,则被隐式调用Number()
函数进行转换。具体每一种运算的详细规则请参考ECMAScript中的定义。
逻辑非(!)操作符首先通过Boolean()函数将它的操作值转换为布尔值,然后求反。
逻辑与(&&)操作符,如果一个操作值不是布尔值时,遵循以下规则进行转换:
Boolean()
转换后为 true
,则返回第二个操作值,否则返回第一个值(不是 Boolean()
转换后的值)null
,返回 null
NaN
,返回 NaN
undefined
,返回 undefined
逻辑或(||)操作符,如果一个操作值不是布尔值,遵循以下规则
Boolean()
转换后为 false
,则返回第二个操作值,否则返回第一个操作值(不是 Boolean()
转换后的值)undefined
、null
和 NaN
的处理规则与逻辑与(&&)相同与上述操作符一样,关系操作符的操作值也可以是任意类型的,所以使用非数值类型参与比较时也需要系统进行隐式类型转换:
valueOf()
方法(如果对象没有 valueOf()
方法则调用 toString()
方法),得到的结果按照前面的规则执行比较注:NaN
是非常特殊的值,它不和任何类型的值相等,包括它自己,同时它与任何类型的值比较大小时都返回 false
。
相等操作符会对操作值进行隐式转换后进行比较:
valueOf()
方法,得到的结果按照前面的规则进行比较null
与 undefined
是相等的NaN
,则相等比较返回 false
前者是切割成数组的形式
后者是将数组转换为字符串
数组方法 | 描述 |
---|---|
pop() | 删除原数组最后一项,并返回删除元素的值;如果数组为空则返回undefined |
push() | 将参数添加到原数组末尾,并返回数组的长度 |
unshift() | 将参数添加到原数组开头,并返回数组的长度 |
shift() | 删除原数组第一项,并返回删除元素的值;如果数组为空则返回undefined |
普通事件中的onclick是DOM0级事件只支持单个事件,会被其他onclick事件覆盖,而事件绑定中的addEventListener是DOM2级事件可以添加多个事件而不用担心被覆盖
普通添加事件的方法:
var btn = document.getElementById("hello");btn.onclick = function(){ alert(1);}btn.onclick = function(){ alert(2);}
执行上面的代码只会alert 2
事件绑定方式添加事件:
var btn = document.getElementById("hello");btn.addEventListener("click",function(){ alert(1);},false);btn.addEventListener("click",function(){ alert(2);},false);
执行上面的代码会先alert 1 再 alert 2
事件
HTML元素事件是浏览器内在自动产生的,当有事件发生时html元素会向外界(这里主要指元素事件的订阅者)发出各种事件,如click,onmouseover,onmouseout等等。
DOM事件流
DOM(文档对象模型)结构是一个树型结构,当一个HTML元素产生一个事件时,该事件会在元素结点与根结点之间的路径传播,路径所经过的结点都会收到该事件,这个传播过程可称为DOM事件流。
冒泡型事件(Bubbling)
这是IE浏览器对事件模型的实现。冒泡,顾名思义,事件像个水中的气泡一样一直往上冒,直到顶端。从DOM树型结构上理解,就是事件由叶子结点沿祖先结点一直向上传递直到根结点;从浏览器界面视图HTML元素排列层次上理解就是事件由具有从属关系的最确定的目标元素一直传递到最不确定的目标元素.
捕获型事件(Capturing)
Netscape Navigator的实现,它与冒泡型刚好相反,由DOM树最顶层元素一直到最精确的元素,直观上的理解应该如同冒泡型,事件传递应该由最确定的元素,即事件产生元素开始。
DOM标准事件模型
因为两个不同的模型都有其优点和解释,DOM标准支持捕获型与冒泡型,可以说是它们两者的结合体。它可以在一个DOM元素上绑定多个事件处理器,并且在处理函数内部,this关键字仍然指向被绑定的DOM元素,另外处理函数参数列表的第一个位置传递事件event对象。
首先是捕获式传递事件,接着是冒泡式传递,所以,如果一个处理函数既注册了捕获型事件的监听,又注册冒泡型事件监听,那么在DOM事件模型中它就会被调用两次。
实例:
冒泡:button
-> div
-> body
(IE 事件流)
捕获:body
-> div
-> button
(Netscape事件流)
DOM: body
-> div
-> button
-> button
-> div
-> body
(先捕获后冒泡)
事件侦听函数的区别
// IE使用: [Object].attachEvent("name_of_event_handler", fnHandler); //绑定函数 [Object].detachEvent("name_of_event_handler", fnHandler); //移除绑定 // DOM使用: [Object].addEventListener("name_of_event", fnHandler, bCapture); //绑定函数 [Object].removeEventListener("name_of_event", fnHandler, bCapture); //移除绑定
如何取消浏览器事件的传递与事件传递后浏览器的默认处理
取消事件传递是指,停止捕获型事件或冒泡型事件的进一步传递。
事件传递后的默认处理是指,通常浏览器在事件传递并处理完后会执行与该事件关联的默认动作(如果存在这样的动作)。例如,如果表单中input type 属性是 “submit”,点击后在事件传播完浏览器就就自动提交表单。又例如,input 元素的 keydown 事件发生并处理后,浏览器默认会将用户键入的字符自动追加到 input 元素的值中。
要取消浏览器的事件传递,IE与DOM标准又有所不同。
在IE下,通过设置 event
对象的 cancelBubble
为 true
即可。
function someHandle() { window.event.cancelBubble = true; }
DOM标准通过调用 event
对象的 stopPropagation()
方法即可。
function someHandle(event) { event.stopPropagation(); }
因些,跨浏览器的停止事件传递的方法是:
function someHandle(event) { event = event || window.event; if(event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true; }
取消事件传递后的默认处理,IE与DOM标准又不所不同。
在IE下,通过设置 event
对象的 returnValue
为 false
即可。
function someHandle() { window.event.returnValue = false; }
DOM标准通过调用 event
对象的 preventDefault()
方法即可。
function someHandle(event) { event.preventDefault(); }
因些,跨浏览器的取消事件传递后的默认处理方法是:
function**` `someHandle(event) { event = event || window.event; if(event.preventDefault) event.preventDefault(); else event.returnValue = false; }
var ev = ev || window.eventdocument.documentElement.clinetWidth || document.body.clientWidthvar target = ev.srcElement || ev.target
call 和 apply 相同点:
都是为了用一个本不属于一个对象的方法,让这个对象去执行基本使用
call()
function.call(obj[,arg1[, arg2[, [,.argN]]]]])
call
的对象必须是个函数functioncall
的第一个参数将会是function改变上下文后指向的对象.如果不传,将会默认是全局对象window
call
的方法会立即执行apply()
function.apply(obj[,argArray])
与call
方法的使用基本一致,但是只接收两个参数,其中第二个参数必须是一个数组或者类数组,这也是这两个方法很重要的一个区别
数组与类数组小科普
数组我们都知道是什么,它的特征都有哪些呢?
array[0]
length
forEach
方法进行遍历类数组顾名思义,具备的特征应该与数组基本相同,那么可以知道,一个形如下面这个对象的对象就是一个类数组
var arrayLike = { 0: 'item1', 1: 'item2', 2: 'item3', length: 3}
类数组arrayLike
可以通过角标进行调用,具有length
属性,同时也可以通过 for 循环进行遍历
我们经常使用的获取dom节点的方法返回的就是一个类数组,在一个方法中使用 arguments
关键字获取到的该方法的所有参数也是一个类数组
但是类数组却不能通过forEach
进行遍历,因为forEach
是数组原型链上的方法,类数组毕竟不是数组,所以无法使用
不同点
call
方法从第二个参数开始可以接收任意个参数,每个参数会映射到相应位置的func的参数上,可以通过参数名调用,但是如果将所有的参数作为数组传入,它们会作为一个整体映射到func对应的第一个参数上,之后参数都为空
function func (a,b,c) {}func.call(obj, 1,2,3)// function接收到的参数实际上是 1,2,3func.call(obj, [1,2,3])// function接收到的参数实际上是 [1,2,3],undefined,undefined
apply
方法最多只有两个参数,第二个参数接收数组或者类数组,但是都会被转换成类数组传入func中,并且会被映射到func对应的参数上
func.apply(obj, [1,2,3])// function接收到的参数实际上是 1,2,3func.apply(obj, { 0: 1, 1: 2, 2: 3, length: 3})// function接收到的参数实际上是 1,2,3
方法一:对象冒充
function Parent(username){ this.username = username; this.hello = function(){ console.log(this.username); }}function Child(username,password){ this.method = Parent; // this.method 作为一个临时的属性,并且指向了 Parent所指向的对象函数 this.method(username); // 执行 this.method 方法,即执行了 Parent 所指向的对象函数 delete this.method; // 销毁 this.method 属性,即此时 Child 就已经拥有了 Parent 的所有方法和属性 this.password = password; this.world = function(){ console.log(this.password); }}const parent = new Parent('hello parent');const child = new Child('hello child','123456');console.log(child);parent.hello();child.hello();child.world();
方法二:call()
call 方法是 Function 类中的方法
call 方法的第一个参数的值赋值给类(即方法)中出现的 this call 方法的第二个参数开始依次赋值给类(即方法)所接受的参数function Parent(username){ this.username = username; this.hello = function(){ console.log(this.username); }}function Child(username,password){ Parent.call(this,username); this.password = password; this.world = function(){ console.log(this.password); }}const parent = new Parent('hello parent');const child = new Child('hello child','123456');parent.hello();child.hello();child.world();
方法三:apply()
apply方法接受2个参数
第一个参数与call方法的第一个参数一样,即赋值给类(即方法)中出现的this
第二个参数为数组类型,这个数组中的每个元素依次赋值给类(即方法)所接受的参数
function Parent(username){ this.username = username; this.hello = function(){ console.log(this.username); }}function Child(username,password){ Parent.apply(this,new Array(username)); this.password = password; this.world = function(){ console.log(this.password); }}const parent = new Parent('hello parent');const child = new Child('hello child','123456');parent.hello();child.hello();child.world();
方法四:原型链
即子类通过 prototype 将所有在父类中通过 prototype 追加的属性和方法都追加到 Child ,从而实现继承
function Parent(){}Parent.prototype.hello = "hello";Parent.prototype.sayHello = function(){ console.log(this.hello);}function Child(){}Child.prototype = new Parent();// 将 Parent 中所有通过 prototype 追加的属性和方法都追加到 Child 从而实现了继承Child.prototype.world = "world";Child.prototype.sayWorld = function(){ console.log(this.world);}const child = new Child();child.sayHello();child.sayWorld();
方法五:混合方式,call()+ 原型链
function Parent(hello){ this.hello = hello;}Parent.prototype.sayHello = function(){ console.log(this.hello);}function Child(hello,world){ Parent.call(this,hello); // 将父类的属性继承过来 this.world = world;}Child.prototype = new Parent(); //将父类的方法继承过来Child.prototype.sayWorld = function(){ // 新增方法 console.log(this.world);}const child = new Child("hello","world");child.sayHello();child.sayWorld();
在函数执行时,this 总是指向调用该函数的对象。要判断 this 的指向,其实就是判断 this 所在的函数属于谁。
在《javaScript语言精粹》这本书中,把 this 出现的场景分为四类,简单的说就是:
1)有对象就指向调用对象
var myObject = { value: 123 }myObject.getValue = function(){ console.log(this.value); // 123 console.log(this); // {value: 123, getValue: ƒ}}myObject.getValue();
2)没调用对象就指向全局对象
var myObject = { value: 123 }myObject.getValue = function(){ var foo = function(){ console.log(this.value); // undefined console.log(this); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …} // foo函数虽然定义在getValue 函数体内,但是不属于 getValue也不属于 myObject,所以调用的时候,它的 this 指针指向了全局对象 } foo(); return this.value;}console.log(myObject.getValue()); // 123
3) 用new构造就指向新对象
// js 中通过 new 关键词来调用构造函数,此时 this 会绑定杂该新对象上var someClass = function(){ this.value = 123;}var myCreate = new someClass();console.log(myCreate.value); // 123
4)通过 apply 或 call 或 bind 来改变 this 的指向
var myObject = { value: 123 };var foo = function(){ console.log(this);}foo(); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}foo.apply(myObject); // {value: 123}foo.call(myObject); // {value: 123}var newFoo = foo.bind(myObject);newFoo(); // {value: 123}
闭包英文是 Closure ,简而言之,闭包就是
作为局部变量都可以被函数内的代码访问,这个和静态语言是没有差别的,闭包的差别在于局部变量可以在函数执行结束后仍然被函数外的代码访问,这意味着函数必须返回一个指向闭包的“引用”,或将这个“引用”赋值给某个外部变量,才能保证闭包中局部变量被外部代码访问,当然包含这个引用的实体应该是一个对象。但是ES并没有提供相关的成员和方法来访问包中的局部变量,但是在ES中,函数对象中定义的内部函数是可以直接访问外部函数的局部变量,通过这种机制,可以用如下方式完成对闭包的访问。
function greeting(name){ var text = "Hello " + name; // 局部变量 // 每次调用时,产生闭包,并返回内部函数对象给调用者 return function(){ console.log(text); }}var sayHello = greeting('Closure');// 通过闭包访问到了局部变量textsayHello(); // 输出Hello Closure
在 ECMAscript 的脚本函数运行时,每个函数关联都有一个执行上下文场景(Exection Context),这个执行上下文包括三个部分
其中第三点this绑定与闭包无关,不在本文中讨论。文法环境中用于解析函数执行过程使用到的变量标识符。我们可以将文法环境想象成一个对象,该对象包含了两个重要组件,环境记录(Enviroment Recode),和外部引用(指针)。环境记录包含包含了函数内部声明的局部变量和参数变量,外部引用指向了外部函数对象的上下文执行场景。全局的上下文场景中此引用值为NULL。这样的数据结构就构成了一个单向的链表,每个引用都指向外层的上下文场景。
例如上面我们例子的闭包模型应该是这样,sayHello函数在最下层,上层是函数greeting,最外层是全局场景。如下图:
因此当sayHello被调用的时候,sayHello会通过上下文场景找到局部变量text的值,因此在屏幕的对话框中显示出”Hello Closure”
针对一些例子来帮助大家更加深入的理解闭包,下面共有5个样例,例子来自于。
例子1:闭包中局部变量是引用而非拷贝
function say667(){ var num = 666; var sayConsole = function(){ console.log(num); } num++; return sayConsole;}var sayConsole = say667();sayConsole(); // 667
例子2:多个函数绑定同一个闭包,因为他们定义在同一个函数内。
function setupSomeGlobals(){ var num = 666; gConsoleNumber = function() { console.log(num); } gIncreaseNumber = function() { num++; } gSetNumber = function(x) { num = x; }}setupSomeGlobals();gConsoleNumber(); // 666gIncreaseNumber();gConsoleNumber(); // 667gSetNumber(12);gConsoleNumber(); // 12
例子3:当在一个循环中赋值函数时,这些函数将绑定同样的闭包
function buildList(list){ var result = []; for(var i = 0; i < list.length; i++){ var item = 'item' + list[i]; result.push(function(){ console.log(item+' '+list[i]); }) } return result;}function testList(){ var fnList = buildList([1,2,3]); for(var j = 0; j < fnList.length; j++){ fnList[j](); }}testList(); // 输出3次 item3 undefined
testList的执行结果是弹出item3 undefined窗口三次,因为这三个函数绑定了同一个闭包,而且item的值为最后计算的结果,但是当i跳出循环时i值为4,所以list[4]的结果为undefined.
例子4:外部函数所有局部变量都在闭包内,即使这个变量声明在内部函数定义之后。
function sayAlice(){ var sayConsole = function(){ console.log(alice); } var alice = "Hello Alice"; return sayConsole;}var helloAlice=sayAlice();helloAlice();
执行结果输出”Hello Alice”的窗口。即使局部变量声明在函数sayAlert之后,局部变量仍然可以被访问到。
例子5:每次函数调用的时候创建一个新的闭包
function newClosure(someNum,someRef){ var num = someNum; var anArray = [1,2,3]; var ref = someRef; return function(x){ num += x; anArray.push(num); console.log('num: ' + num +'\nanArray ' + anArray.toString() +'\nref.someVar ' + ref.someVar); }}closure1=newClosure(40,{someVar:'closure 1'});closure2=newClosure(1000,{someVar:'closure 2'});closure1(5); // num: 45 anArray 1,2,3,45 ref.someVar closure 1closure2(-10); // num: 990 anArray 1,2,3,990 ref.someVar closure 2
(1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
(2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
例子:
function Cars(){ this.name = "Benz"; this.color = ["white","black"];}Cars.prototype.sayColor = function(){ var outer = this; return function(){ return outer.color };}; var instance = new Cars();console.log(instance.sayColor()())
改造:
function Cars(){ this.name = "Benz"; this.color = ["white","black"];}Cars.prototype.sayColor = function(){ var outerColor = this.color; //保存一个副本到变量中 return function(){ return outerColor; //应用这个副本 }; outColor = null; //释放内存}; var instance = new Cars();console.log(instance.sayColor()())
在JS当中一个变量的作用域(scope)是程序中定义这个变量的区域。变量分为两类:全局(global)的和局部的。其中全局变量的作用域是全局性的,即在JavaScript代码中,它处处都有定义。而在函数之内声明的变量,就只在函数体内部有定义。它们是局部变量,作用域是局部性的。函数的参数也是局部变量,它们只在函数体内部有定义。
我们可以借助JavaScript的作用域链(scope chain)更好地了解变量的作用域。每个JavaScript执行环境都有一个和它关联在一起的作用域链。这个作用域链是一个对象列表或对象链。当JavaScript代码需要查询变量x的值时(这个过程叫做变量解析(variable name resolution)),它就开始查看该链的第一个对象。如果那个对象有一个名为x的属性,那么就采用那个属性的值。如果第一个对象没有名为x的属性,JavaScript就会继续查询链中的第二个对象。如果第二个对象仍然没有名为x的属性,那么就继续查询下一个对象,以此类推。如果查询到最后(指顶层代码中)不存在这个属性,那么这个变量的值就是未定义的。
var a,b;(function(){ alert(a); // undefined alert(b); // undefined var a = b = 3; alert(a); // 3 alert(b); // 3})(); alert(a); // undefined alert(b); // 3
以上代码相当于
var a,b;(function(){ alert(a); alert(b); var a = 3; b = 3; alert(a); alert(b);})(); alert(a);
概述
什么叫做事件委托,别名叫事件代理,JavaScript 高级程序设计上讲。事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
实际例子:
有三个同事预计会在周一收到快递,为签收快递,有两种方法:一是三个人在公司门口等快递,二是委托给前台的小姐代为签收。现实生活中,我们大多采用委托的方案(公司也不会容忍那么多人站在门口)。前台小姐收到快递后,会判断收件人是谁,按照收件人的要求签收,甚至是代付。这种方案还有一个好处就是,即使公司来了很多新员工(不管多少),前台小姐也会在收到寄给新员工们的快递后核实代为签收。
这里有2层意思:
第一、现在委托前台的小姐是可以代为签收的,即程序中的现有的 DOM 节点是有事件的。第二、新员工也可以被前台小姐代为签收,即程序中新添加的 DOM 节点也是有事件的。
为什么要使用事件委托
一般来说,DOM 需要有事件处理程序,就会直接给它设处理程序,但是如果是很多 DOM 需要添加处理事件呢?例如我们有100个 li,每个 li 都有相同的 click 点击事件,可能我们会用到 for 循环,来遍历所有 li ,然后给它们添加事件,那么会存在什么样的问题?
在 JavsScript 中,添加到页面上的事件处理程序数量将直接影响到整体运行性能,因为需要不断地与 DOM 进行交互,访问 DOM 的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间,这就是为什么性能优化的主要思想之一就是减少 DOM 操作的原因、如果要用到事件委托,就会将所有的操作都放在 js 程序里面,与 DOM 的操作就只需要交互一次,这样就可以大大减少与 DOM 的交互次数,提高性能。
每个函数都是一个对象,是对象就会占用内存,对象越多,内存占用率就越大,自然性能就越差了(内存不够用,是硬伤,哈哈),比如上面的100个li,就要占用100个内存空间,如果是1000个,10000个呢,那只能说呵呵了,如果用事件委托,那么我们就可以只对它的父级(如果只有一个父级)这一个对象进行操作,这样我们就需要一个内存空间就够了,是不是省了很多,自然性能就会更好。
事件委托的原理
事件委托是利用事件的冒泡原理来实现的,何为事件冒泡?就是事件从最深的节点开始执行,然后逐步向上传播事件,例子:
页面上有一个节点树,div>ul>li>a,比如给最里面的 a 加一个 click 点击事件,那么这个事件就会一层一层的往外执行,执行顺序 a>li>ul>div,有这么一个机制,那么我们给最外面的 div 加点击事件,那么里面的 ul,li,a 做点击事件的时候,都会冒泡到最外层的 div 上面,都会触发,这就是事件委托,委托他们父级代为执行事件。
事件委托怎么实现
实现功能是点击li,弹出123:
window.onload = function(){ var oUl = document.getElementById('ul'); var aLi = oUl.getElementsByTagName('li'); for(var i = 0; i < aLi.length; i++){ aLi[i].onclick = function(){ alert(123); } }}
上面的代码的意思很简单,相信很多人都是这么实现的,我们看看有多少次的dom操作,首先要找到ul,然后遍历li,然后点击li的时候,又要找一次目标的li的位置,才能执行最后的操作,每次点击都要找一次li;
那么我们用事件委托的方式做又会怎么样呢?
window.onload = function(){ var oUl = document.getElementById('ul'); oUl.onclick = function(){ alert(123); }}
这里用父级ul做事件处理,当li被点击时,由于冒泡原理,事件就会冒泡到ul上,因为ul上有点击事件,所以事件就会触发,当然,这里当点击ul的时候,也是会触发的,那么问题就来了,如果我想让事件代理的效果跟直接给节点的事件效果一样怎么办,比如说只有点击li才会触发,不怕,我们有绝招:
Event对象提供了一个属性叫target,可以返回事件的目标节点,我们成为事件源,也就是说,target就可以表示为当前的事件操作的dom,但是不是真正操作dom,当然,这个是有兼容性的,标准浏览器用ev.target,IE浏览器用event.srcElement,此时只是获取了当前节点的位置,并不知道是什么节点名称,这里我们用nodeName来获取具体是什么标签名,这个返回的是一个大写的,我们需要转成小写再做比较(习惯问题):
window.onload = function(){ var oUl = document.getElementById("ul"); oUl.onclick = function(ev){ var ev = ev || window.event; var target = ev.target || ev.srcElement; if(target.nodeName.toLowerCase() == "li"){ alert(target.innerHTML); } }}
这样改下就只有点击li会触发事件了,且每次只执行一次dom操作,如果li数量很多的话,将大大减少dom的操作,优化的性能可想而知!
上面的例子是说li操作的是同样的效果,要是每个li被点击的效果都不一样,那么用事件委托还有用吗?
var Add = document.getElementById("add");var Remove = document.getElementById("remove");var Move = document.getElementById("move");var Select = document.getElementById("select");Add.onclick = function(){ alert('添加');};Remove.onclick = function(){ alert('删除');};Move.onclick = function(){ alert('移动');};Select.onclick = function(){ alert('选择');}
上面实现的效果我就不多说了,很简单,4个按钮,点击每一个做不同的操作,那么至少需要4次dom操作,如果用事件委托,能进行优化吗?
var oBox = document.getElementById("box");oBox.onclick = function(ev){ var ev = ev || window.event; var target = ev.target || ev.srcElement; if(target.nodeName.toLowerCase() == 'input'){ switch(target.id) { case 'add': alert('添加'); break; case 'remove': alert('删除'); break; case 'move': alert('移动'); break; case 'select': alert('选择'); break; default : alert('业务错误'); break; } }}
用事件委托就可以只用一次dom操作就能完成所有的效果,比上面的性能肯定是要好一些的
现在讲的都是document加载完成的现有dom节点下的操作,那么如果是新增的节点,新增的节点会有事件吗?也就是说,一个新员工来了,他能收到快递吗?
现在是移入li,li变红,移出li,li变白,这么一个效果,然后点击按钮,可以向ul中添加一个li子节点
window.onload = function(){ var oBtn = document.getElementById("btn"); var oUl = document.getElementById("ul"); var aLi = oUl.getElementsByTagName("li"); var num = 4; // 鼠标移入变红,移出变白 for(var i = 0; i < aLi.length; i++){ aLi[i].onmouseover = function(){ this.style.background = 'red'; } aLi[i].onmouseout = function(){ this.style.background = '#fff'; } } // 新增节点 oBtn.onclick = function(){ num++; var oLi = document.createElement('li'); oLi.innerHTML = 111 * num; oUl.appendChild(oLi); }}
这是一般的做法,但是你会发现,新增的li是没有事件的,说明添加子节点的时候,事件没有一起添加进去,这不是我们想要的结果,那怎么做呢?一般的解决方案会是这样,将for循环用一个函数包起来,命名为mHover,如下:
// 将鼠标移出移入包装为一个函数window.onload = function(){ var oBtn = document.getElementById("btn"); var oUl = document.getElementById("ul"); var aLi = oUl.getElementsByTagName("li"); var num = 4; // 鼠标移入变红,移出变白 function mHover(){ for(var i = 0; i < aLi.length; i++){ aLi[i].onmouseover = function(){ this.style.background = 'red'; } aLi[i].onmouseout = function(){ this.style.background = '#fff'; } } } mHover(); // 新增节点 oBtn.onclick = function(){ num++; var oLi = document.createElement('li'); oLi.innerHTML = 111 * num; oUl.appendChild(oLi); mHover(); }}
虽然功能实现了,看着还挺好,但实际上无疑是又增加了一个dom操作,在优化性能方面是不可取的,那么有事件委托的方式,能做到优化吗?
window.onload = function(){ var oBtn = document.getElementById("btn"); var oUl = document.getElementById("ul"); var aLi = oUl.getElementsByTagName("li"); var num = 4; // 事件委托 鼠标移入变红,移出变白 // 添加的子元素也有事件 oUl.onmouseover = function(){ var ev = ev || window.event; var target = ev.target || ev.srcElement; if(target.nodeName.toLowerCase() == 'li'){ target.style.background = 'red'; } } oUl.onmouseout = function(){ var ev = ev || window.event; var target = ev.target || ev.srcElement; if(target.nodeName.toLowerCase() == 'li'){ target.style.background = '#fff'; } } // 新增节点 oBtn.onclick = function(){ num++; var oLi = document.createElement('li'); oLi.innerHTML = 111 * num; oUl.appendChild(oLi); }}
另外一个思考的问题
现在给一个场景 ul > li > div > p,div占满li,p占
转载地址:http://frnea.baihongyu.com/