函数声明、函数表达式、匿名函数

( function(){…} )()和( function (){…} () )是两种javascript立即执行函数的常见写法,最初我以为是一个括号包裹匿名函数,再在后面加个括号调用函数,最后达到函数定义后立即执行的目的,后来发现加括号的原因并非如此。要理解立即执行函数,需要先理解一些函数的基本概念。

函数声明、函数表达式、匿名函数

函数声明:function fnName () {…};使用function关键字声明一个函数,再指定一个函数名,叫函数声明。

函数表达式 var fnName = function () {…};使用function关键字声明一个函数,但未给函数命名,最后将匿名函数赋予一个变量,叫函数表达式,这是最常见的函数表达式语法形式。

匿名函数:function () {}; 使用function关键字声明一个函数,但未给函数命名,所以叫匿名函数,匿名函数属于函数表达式,匿名函数有很多作用,赋予一个变量则创建函数,赋予一个事件则成为事件处理程序或创建闭包等等。

函数声明和函数表达式不同

一、Javascript引擎在解析javascript代码时会‘函数声明提升’(Function declaration Hoisting)当前执行环境(作用域)上的函数声明,而函数表达式必须等到Javascirtp引擎执行到它所在行时,才会从上而下一行一行地解析函数表达式,

二、函数表达式后面可以加括号立即调用该函数,函数声明不可以,只能以fnName()形式调用 。以下是两者差别的两个例子。

fnName();
function fnName(){
    ...
}//正常,因为‘提升’了函数声明,函数调用可在函数声明之前

fnName();
var fnName=function(){
    ...
}//报错,变量fnName还未保存对函数的引用,函数调用必须在函数表达式之后

var fnName=function(){
    alert('Hello World');
}();//函数表达式后面加括号,当javascript引擎解析到此处时能立即调用函数

function fnName(){
    alert('Hello World');
}();//语法错误,Uncaught SyntaxError: Unexpected token ),这个函数会被js引擎解析为两部分:
    //1.函数声明 function fnName(){ alert('Hello World'); } 
    //2.分组表达式 () 但是第二部分作为分组表达式语法出现了错误,因为括号内没有表达式,把“()”改为“(1)”就不会报错
    //但是这么做没有任何意义,只不过不会报错,分组表达式请见:
    //分组中的函数表达式http://www.nowamagic.net/librarys/veda/detail/1664

function(){
    console.log('Hello World');    
}();//语法错误,Uncaught SyntaxError: Unexpected token (

在理解了一些函数基本概念后,回头看看( function(){…} )()和( function (){…} () )这两种立即执行函数的写法

最初我以为是一个括号包裹匿名函数,并后面加个括号立即调用函数,当时不知道为什么要加括号,后来明白,要在函数体后面加括号就能立即调用,则这个函数必须是函数表达式,不能是函数声明。

function(a){
        console.log(a);   //报错,Uncaught SyntaxError: Unexpected token (
}(12);
(function(a){
    console.log(a);   //firebug输出123,使用()运算符
})(123);

(function(a){
    console.log(a);   //firebug输出1234,使用()运算符
}(1234));

!function(a){
    console.log(a);   //firebug输出12345,使用!运算符
}(12345);

+function(a){
    console.log(a);   //firebug输出123456,使用+运算符
}(123456);

-function(a){
    console.log(a);   //firebug输出1234567,使用-运算符
}(1234567);

var fn=function(a){
    console.log(a);   //firebug输出12345678,使用=运算符
}(12345678)           
//需要注意的是:这么写只是一个赋值语句,即把函数匿名函数function(a){...}()的返回值赋值给了fn,如果函数没有返回值,那么fn为undefined,
//下面给出2个例子,用来解答读者的疑惑:
var fn=function(a){
    console.log(a);   //firebug输出12345678,使用=运算符
}(12345678);
console.info(fn);//控制台显示为undefined;
fn(123);//函数未定义报错,fn is undefiend 

var fn=function(a){
    console.log(a);   //firebug输出12345678,使用=运算符
    return 111;
}(12345678);
console.info(fn);//会发现fn就是一个返回值111,而不是一个函数
fn(123);//报错,因为fn不是一个函数

可以看到输出结果,在function前面加!、+、 -甚至是逗号等到都可以起到函数定义后立即执行的效果,而()、!、+、-、=等运算符,都将函数声明转换成函数表达式,消除了javascript引擎识别函数表达式和函数声明的歧义,告诉javascript引擎这是一个函数表达式,不是函数声明,可以在后面加括号,并立即执行函数的代码。

加括号是最安全的做法,因为!、+、-等运算符还会和函数的返回值进行运算,有时造成不必要的麻烦。

不过这样的写法有什么用呢?

javascript中没用私有作用域的概念,如果在多人开发的项目上,你在全局或局部作用域中声明了一些变量,可能会被其他人不小心用同名的变量给覆盖掉,根据javascript函数作用域链的特性,可以使用这种技术可以模仿一个私有作用域,用匿名函数作为一个“容器”,“容器”内部可以访问外部的变量,而外部环境不能访问“容器”内部的变量,所以( function(){…} )()内部定义的变量不会和外部的变量发生冲突,俗称“匿名包裹器”或“命名空间”。

JQuery使用的就是这种方法,将JQuery代码包裹在 ( function (window,undefined){…jquery代码…} (window))中,在全局作用域中调用JQuery代码时,可以达到保护JQuery内部变量的作用。

模拟事件和自定义事件

事件

  • 事件类型 :表示事件的类型。如:MouseEvent(鼠标事件)、KeyboardEvent(键盘事件)
  • 事件名称 :表示事件的名称。如:click(单击)、dblclick(双击)。
  • 事件目标 : 表示与发生事件相关的目标对象。
  • 事件处理程序(event handler):指处理事件的函数,即发生事件时,需调用的函数。

常用方法

  • addEventListener()
  • removeEventListener()
  • dispatchEvent()
  • detachEvent()

Event对象

属性:

bubbles、cancelable、currentTarget、eventPhase、target、timeStamp、type

方法:

preventDefault()
阻止默认事件的方法,调用此方法是,连接不会被打开,但是会发生冒泡,冒泡会传递到上一层的父元素;

stopPropagation():

阻止事件的冒泡方法,不让事件向documen上蔓延,但是默认事件任然会执行,当你掉用这个方法的时候,如果点击一个连接,这个连接仍然会被打开

stopImmediatePropagation():

阻止事件捕获


另外:
return false ;

同时阻止事件冒泡也会阻止默认事件;连接不会被打开,事件也不会传递到上一层的父元素;可以理解为return false就等于同时调用了event.stopPropagation()和event.preventDefault()

currentTarget 与 target 属性的区别

currentTarget :获取正在处理此事件的对象(也可以理解为绑定此事件的对象)

target :获取触发此事件的对象。

冒泡阶段时两者的区别:

假设body和Button元素都注册了click事件;当点击Button元素时,body的click事件也会触发,此时在body的click事件内,currentTarget指向body元素,target指向Button元素。

示例:点击Button元素时的显示结果

document.body.onclick = function (e) {
    console.log(e.currentTarget); // 指向body元素
    console.log(e.target); // 若是冒泡事件时,指向最开始触发的元素。否则为元素自身。
};

document.getElementById('btn').onclick = function (e) {
    console.log(e.currentTarget);
    console.log(e.target);
};

事件的注册与注销

注册事件:
addEventListener(),attachEvent()

注销事件:
removeEventListener()、detachEvent()

注册事件的方式大致有2种:一种属性注册,另一种通过方法注册。

  • 属性注册方式又可分为在HTML元素内的事件属性赋值和通过JS指定元素对象的事件属性

    <button onclick="sayHello()">点击</button>
    
  • 方法注册方式可通过addEventListener()或attachEvent()方法进行事件的注册

通过JS设置元素对象的属性为事件处理程序

示例1:事件属性注册的演示

// 注册body的click事件
document.body.onclick = function (e) {
    alert(1);
};

示例2:事件属性注册的唯一性

document.body.onclick=function(e){
    console.log(1);
}

// 会覆盖前面注册的事件处理程序
document.body.onclick=function(e){
    console.log(2);
}

document.body.click(); // => 2 :只输出后面属性注册的

addEventListener()方法注册事件

示例1:多次注册同一事件,按注册顺序执行,先输出1,再输出2

document.body.addEventListener('click',function(e){
     console.log('1');
});

document.body.addEventListener('click',function(e){
    console.log('2');
});

document.body.click(); // => 1,2

示例2:使用函数对象多次注册同一事件:只当注册一次

function sayHello(){
    console.log('hello');
}

document.body.addEventListener('click',sayHello);
// 使用处理程序多次注册同一事件,只当注册一次
document.body.addEventListener('click',sayHello);

document.body.click(); // => hello :只输出一遍

IE9之前的IE版本可通过attachEvent()方法注册事件。

注销事件

示例1:通过removeEventListener()注销事件

function sayHello(e) {
    console.log('1');
}

// 注册body click事件
document.body.addEventListener('click', sayHello);

// 注销body click事件的sayHello函数
document.body.removeEventListener('click',sayHello);

document.body.click(); // 触发click事件,不输出任何结果

若第二个参数为函数体,将不会注销

detachEvent(eventName, function Object)

说明:注销通过attachEvent()注册的事件处理程序。

语法:EventTarget.detachEvent(eventName, eventHandlerObj)

参数:

  • eventName {string} :所要注销的事件名称,区分大小写。这里的名称跟事件属性一样,以”on”开头,后面跟着事件名称。如:onclick、onload。
  • eventHandlerObj {function Object} ::函数对象。传入一个函数体是没有效果的。
通过detachEvent()注销事件
function sayHello() {
    console.log('1');
}
document.body.attachEvent('onclick', sayHello);

document.body.detachEvent('onclick', sayHello); // 注销事件

document.body.click(); // 不输出结果     

若第二个参数为函数体,将不会注销

多次注册与多次注销

因为attachEvent()可以把一个函数对象多次注册到元素同一个事件上,所以调用一次detachEvent()只能注销掉一次。

function sayHello() {
    console.log('1');
}
document.body.attachEvent('onclick', sayHello);
document.body.attachEvent('onclick', sayHello); // 注册了2次

document.body.click(); // => 1 1:输出2次

document.body.detachEvent('onclick', sayHello); // 注销1次

document.body.click(); // => 1 :输出结果为1,只注销了1次
给对象的事件属性赋值为null,可取消此事件的所有注册过的处理事件程序。

示例:

document.body.addEventListener('onclick', function(e){
    console.log(1);
});
document.body.addEventListener('onclick', function(e){
    console.log(2);
});

document.body.onclick=null; // onclick属性赋值为null,相当于注销了onclick事件

document.body.click(); // 无操作

事件流与事件委托

三个阶段

  • 捕获阶段(Capture Phase):事件从最外层的window对象到目标节点的父节点依次触发的阶段。(从外到内)
  • 目标阶段(Target Phase):事件在目标节点上触发时的阶段。
  • 冒泡阶段(Bubbing Phase):事件从目标节点的父节点到最外层的window对象依次触发的阶段。(从内到外)

阻止事件流的传播

Event 事件对象的stopPropagation()、stopImmediatePropagation()方法可阻止事件流的后续传播。

1.在捕获阶段调用

说明:在捕获阶段调用stopPropagation()方法时,此元素后续的事件流都会阻止,包括捕获阶段、目标阶段、冒泡阶段。

document.body.addEventListener('click',function(e){
    console.log("事件阶段:"+e.eventPhase+';target:'+e.target+';currentTarget:'+e.currentTarget)
    e.stopPropagation();
},true);

结果:事件流在body的捕获阶段就截至了,后续的阶段都没有执行

2.在目标阶段调用

说明:在目标段调用stopPropagation()方法时,捕获阶段和目标阶段会执行完毕,冒泡阶段不会被执行。

document.getElementById('btn').addEventListener('click',function(e){
console.log("捕获阶段注册:事件阶段:"+e.eventPhase+';target:'+e.target+';currentTarget:'+e.currentTarget)
e.stopPropagation();
    },false);
    document.getElementById('btn').addEventListener('click',function(e){
console.log("冒泡阶段注册:事件阶段:"+e.eventPhase+';target:'+e.target+';currentTarget:'+e.currentTarget)
e.stopPropagation();
    },true);

结果:捕获阶段和目标阶段执行完毕,冒泡阶段未被执行。

3.在冒泡阶段调用

说明:在冒泡段调用stopPropagation()方法时,捕获阶段和目标阶段会执行完毕,元素后续的冒泡阶段不会被执行。

document.body.addEventListener('click',function(e){
    console.log("事件阶段:"+e.eventPhase+';target:'+e.target+';currentTarget:'+e.currentTarget)
    e.stopPropagation();
},false);

结果:捕获阶段和目标阶段执行完毕,body后续的冒泡阶段未被执行

事件委托(Event Delegate)

HTML元素含有嵌套关系,并且事件流含有冒泡阶段。子元素的触发事件会冒泡到父元素的相同事件上。

一般情况只需给子元素注册特定的事件处理程序即可,但当子元素过多或频繁的进行增减操作怎么办?

比如一个ul包含了几十个li元素,对每个li元素进行单独的事件注册会影响性能。而现只要在父元素注册事件监听器,等待li事件触发后的冒泡阶段即可。

简单来说事件委托就是父元素监听子元素的冒泡事件。

Div容器包含了多个li子元素,在Div容器注册事件委托。

HTML代码:

<div id="div">
    <ul id="ul" >
        <li data-key="北京">北京</li>
        <li data-key="上海">上海</li>
        <li data-key="杭州">杭州</li>
    </ul>
</div>

JS代码:

document.getElementById('div').addEventListener('click',function(e){
            var value=e.target.attributes['data-key'].value; // 获取目标阶段元素的'data-key'属性的值
        console.log(value);
            });

顺便附上jquery的方法

$('#div').on('click','li',function(e) {
    var v = $(this).data('key');
    console.log(v);
});

模拟事件

模拟事件,即非实际操作去触发元素的事件。如按钮的点击,不需要实际用鼠标去点击此按钮,而是采用模拟触发此按钮的点击事件。

触发元素的事件可以直接调用事件方法(如:click()触发元素的click事件)。

为何还要单独的模拟触发呢?

与直接触发相比,模拟事件包含以下特点:

  • 模拟特定场景:如触发click事件,可同时模拟是否按下Ctrl、Alt等按键。
  • 可触发自定义事件

模拟事件的创建方式:

  1. 老版:通过document.createEvent()方法创建各事件类型对象。1.
  2. 新版:通过各事件的构造函数创建事件类型对象。

老版本方式将会被新版本方式所替代,不再记录。。

新版创建步骤

  1. 通过各事件类型的构造函数创建一个event对象。
  2. 在元素上注册监听
  3. 调用元素对象的dispatchEvent(event对象)方法进行派发。

    //1、创建事件
    var clickElem = new Event("clickElem");
    
    //2、注册事件监听器
    elem.addEventListener("clickElem",function(e){
        //干点事
    })
    
    //3、触发事件
    elem.dispatchEvent(clickElem);
    

上面用到的Event属于基本事件类型

另外有一种自定义事件的方法叫 观察者模式

即JS设计模式种的 发布者订阅模式

vue的双向数据绑定就用到了此设计模式

js中数组的合并和对象的合并

数组合并

concat 方法

var a=[1,2,3],b=[4,5,6];
var c=a.concat(b);
console.log(c);// 1,2,3,4,5,6
console.log(a);// 1,2,3  不改变本身

循环遍历

var arr1=['a','b'];
var arr2=['c','d','e'];

for(var i=0;i<arr2.length;i++){
      arr1.push(arr2[i]) 
}

console.log(arr1);//['a','b','c','d','e']

apply

合并数组arr1和数组arr2,使用Array.prototype.push.apply(arr1,arr2) or arr1.push.apply(arr1,arr2);

var arr1=['a','b'];
var arr2=['c','d','e'];

Array.prototype.push.apply(arr1,arr2);

//或者

arr1.push.apply(arr1,arr2);

console.log(arr1) //['a','b','c','d','e']

对象合并

$.extend()

var obj1= {'a': 1};
var obj2= {'b': 1};
var c = $.extend(obj1, obj2);
1
console.log(obj1); // {a: 1, b: 1}  obj1已被修改
1
//或者 <br>var obj3 = $.extend({}, obj1, obj2) <br>console.log(obj3); //{a: 1, b: 1} 不会改变obj1,obj2

遍历赋值

var obj1={'a':1};
var obj2={'b':2,'c':3};
for(var key in obj2){
     if(obj2.hasOwnProperty(key)===true){    
   //此处hasOwnProperty是判断自有属性,使用 for in 循环遍历对象的属性时,原型链上的所有属性都将被访问会避免原型对象扩展带来的干扰
           obj1[key]=obj2[key];
} 
}

console.log(obj1);//{'a':1,'b':2,'c':3};
  

Obj.assign()

可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

  Object.assign(target, ...sources)

//a. 复制一个对象<br>var obj = { a: 1 ,b:2};
var copyObj = Object.assign({}, obj);
console.log(copyObj); // { a: 1,b:2 }<br><br>//b.合并多个对象

var o1 = { a: 1 };
var o2 = { b: 2 };
var o3 = { c: 3 };

var obj = Object.assign(o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
console.log(o1);  // { a: 1, b: 2, c: 3 }, 且目标对象自身也会改变。 

对象的深拷贝和浅拷贝

浅拷贝

var obj1={'a':1};
var obj2={'b':{'b1':22,'b2':33}};

$.extend(obj1, obj2);   //obj1拷贝了obj2的属性

console.log(obj1)  // {'a':1,'b'{'b1':22,'b2':33}}
console.log(obj1.b.b1)  // 22

obj2.b.b1=44;   //obj2重新赋值
console.log(obj1.b.b1)  // 44  obj1.b仅拷贝了对象的指引,所以受原obj2的影响

深拷贝

var obj1={'a':1};
var obj2={'b':{'b1':22,'b2':33}};

$.extend(true,obj1, obj2);   //第一个参数设为true表示深复制

console.log(obj1)  // {'a':1,'b'{'b1':22,'b2':33}}
console.log(obj1.b.b1)  // 22

obj2.b.b1=44;   //obj2重新赋值
console.log(obj1.b.b1)  // 22 obj1拷贝了obj2的所有属性以及值,并不受obj2的影响