深入学习JavaScript(二)

函数表达式和函数声明

函数声明

 function 函数名(参数){函数体}

函数表达式

function 函数名(可选)(参数){函数体}

###示例:
function foo(){} // 声明,因为它是程序的一部分
var bar = function foo(){}; // 表达式,因为它是赋值表达式的一部分
new function bar(){}; // 表达式,因为它是new表达式

(function(){
function bar(){} // 声明,因为它是函数体的一部分
  })();

另外一种不太常见的函数表达式

function foo(){}//函数声明
(function foo(){});//函数表达式,因为在外面加上了一个分组操作符,分组操作符有一个作用是将括号中的函数声明转化成为函数表达式
//验证分组操作符内是否一定要是函数声明
try{
	(var x=5);  //var x=5是一个语句,不是函数声明
}catch(err){
	alert(err);
}

函数表达式与函数声明各自的作用?

函数声明会在函数表达式被解析和求和之前先被解析,如果是函数表达式在函数声明之前那么函数声明也会在其之前解析

示例:

alert(foo());
function foo(){
	return "Hello World";
}

提示:函数声明最好不要在判断语句中使用,因为在判断语句中函数声明是没有被标准化的,在不同的浏览器环境中可能会有不同的效果

函数声明只能出现在程序或函数体内。从句法上讲,它们 不能出现在Block(块)({ ... })中,例如不能出现在 if、while 或 for 语句中。因为 Block(块) 中只能包含Statement语句, 而不能包含函数声明这样的源元素。另一方面,仔细看一看规则也会发现,唯一可能让表达式出现在Block(块)中情形,就是让它作为表达式语句的一部分。但是,规范明确规定了表达式语句不能以关键字function开头。而这实际上就是说,函数表达式同样也不能出现在Statement语句或Block(块)中(因为Block(块)就是由Statement语句构成的)。


函数表达式的名字只在新定义的函数作用域内有效,因为规范规定了标示符不能在外围的作用域内有效:

示例:

var t=text(){
	return typeof text; //text在内部作用域有效
};
typeof text;  //undefined
t();  //undefined

如果是只对方法的内部有限的话,话句话说也就是外部没法引用,那么我们为什么不用匿名函数呢?原因是因为显示函数有利于我们的调试,因为调试的过程中我们可以很清楚的知道调试栈中的对象是那个,相比于匿名函数比较方便

另外,还有一个函数表达式跟函数声明之间的区别是:函数声明是不能进行自执行的,但是函数表达式却是可以的

webkit的displayName属性

WebKit引入了一个“特殊的”displayName属性(本质上是一个字符串),如果开发人员为函数的这个属性赋值,则该属性的值将在调试器或性能分析器中被显示在函数“名称”的位置上

JavaScript的Module模式

基本用法

//相当于是一个类
var module=function(eq){
	//定义变量
	var VALUE="6",
		my={};
	//定义属性
	my.Name="xiecanyong";
	my.money=function(data){
		text(data);//调用方法
	}
	//定义函数
	function text(data){
		console.log(data);
	}
	//暴露公开成员
	/*return {
		add:function(x,y){
			var count=x+y;
		}
	}*/
	return my;//暴露公开的成员活或者是属性方法
}
var MO=new module(); //创建对象
var NAME=MO.Name;  //获取属性
var MONEY=MO.money(1);//给属性值传参

另外由于我们可以在方法中声明一个全局变量,为了保证内部环境不对外部环境造成污染,所以我们可以通过以下两种方法来实现:

1、引入全局变量

2、匿名闭包

引入全局变量和匿名函数

最好的例子就是jquery的源码了,简单的了解一下

(function($){
	//jquery源码
}(jquery));

Module模式高级用法

拓展

我们一般使用闭包都是将相关的代码写在同一个文件下面,但是如果是项目工程比较大的话,需要多人合作共同完成,那么怎样将一个文件分离,达到多人协作呢?

var blogModule = (function (my) {
my.AddPhoto = function () {
    //添加内部代码  
};
return my;
} (blogModule));

上面的这一段代码的运行原理:通过创建一个blogModule全局变量,然后每次调用都会将blogModule中的方法添加到blogModule全局变量中,这样待到全部的文件加载完成后,全局变量中也就有了加载的全部方法和变量

但是这一段代码虽然语法上是正确的,但是运行确实会报错,这里面的原因是:因为第一次运行的时候blogMudule是一个已定义但未初始化的变量,所以向其中添加方法会发生异常

所以我们应该这样处理,加上一个判断时候对象未初始化,这样就可以解决问题了

var blogModule = (function (my) {
my.AddPhoto = function () {
    //添加内部代码  
};
return my;
} (blogModule||{}));

如果是所有的调用都是采用这种形式的话,也就意味着不论哪个文件先加载最后的blogModule全局变量都是一样的,这样的过程我们称为松耦合拓展

紧耦合拓展

既然有松耦合拓展那么对应的就有紧耦合拓展,紧耦合拓展顾名思义就是不能打乱文件的加载顺序但是这样比起松耦合拓展来说会使得使用起来更为的不方便,但是却却可以实现方法重载的目的

var blogModule = (function (my) {
    my.oldAddPhotoMethod = my.AddPhoto;
	//将之前调用的方法传递给oldAddPhotoMethod,然后这样我们再重载AddPhoto的时候就不会将原来的函数覆盖掉
    my.AddPhoto = function () {
        // 重载方法,依然可通过oldAddPhotoMethod调用旧的方法
    };	
    return my;
} (blogModule));

紧耦合拓展在使用的时候要注意的是,全局变量中的var一定要加上,否则会出错,这个原因后面将会说明。通过对这个紧耦合拓展的分析我们可以知道,在调用者段代码之前,必须要有另外一段AddPhoto的方法,这里如果是加载方法的时候与前面的方法发生了冲突,那么我们就将前面的方法赋值给另外一个变量,这样就可以实现重载(这里的重载可能与强语言的重载有一些不一样),还有一点也是要注意的,虽然紧耦合的传入参数不需要判断是否为空,但是在紧耦合拓展的文件加载上,第一个加载的文件的传入参数也是要blogModule||{}


JavaScript实现继承

JavaScript虽然没有想其他的语言一样的继承关系,但是我们可以通过将被继承的对象(父类)传递给继承对象(子类)的原型(prototype),这样如果子类没有定义的方法那么就会调用父类中的方法,这原理跟其他语言的继承的定义基本上是一样的,所以也就可以实现继承

function MadDog() {
this.yelp = function() {
      var self = this;          
      setInterval(function() {
            self.wow();      
      }, 500);
  }
}
MadDog.prototype = new Dog();         

//for test
var dog = new Dog();
dog.yelp();
var madDog = new MadDog();
madDog.yelp();

这里只是一个简单的原型继承的应用,下面就来深入讲解一下继承的问题。

JavaScript是一种面向对象的语言,所以JavaScript也就具备了面向对象的3大特性(封装、继承、多态),但是JavaScript的继承与其他语言的继承有些不一样,其他语言的继承是基于类模型继承,而JavaScript是基于原型模型的继承,继承的形式则是通过赋值的方式来进行

其实在JavaScript中还存在着一些关键字的概念,只不过是比较遗憾一些,没有特定申明。例如我们可以通过

$(function(){
var test=new cal(2,1);
test.add(1,2);
});

var cal=function(first,second){
    this.first=first; //相当于将外部的值赋值给cal对象中的first变量
    this.second=second;
};
cal.prototype=function(){}();//将cal对象的父类设置为空
cal.prototype=function(){
    add=function(x,y){
        return x+y+this.first; //this.first是相当于对cal对象方法外面的变量的引用
    },
    subtract=function(x,y){
        return x-y;
    }
    return{
    	add:add,
        subtract:subtract
    }
}();

在上面的例子中,我们可以看到父类声明的变量在子类中是能够被访问到的(this.first的引用)而且我们也就定义了一个类似于其他语言(如:java、C#)中的类概念,但是如果我们想对这个类进行拓展,我们可以这样来修改。

$(function(){
var test=new cal(2,1);
test.add(1,2);
});

var cal=function(first,second){
    this.first=first;
    this.second=second;
};

cal.prototype=function(){
    add=function(x,y){
        return x+y+this.first;
    },
    subtract=function(x,y){
        return x-y;
    }
    return{
    	add:add,
        subtract:subtract
    }
}();
cal.prototype.mul=function(x,y){
	return x*y;
};
cal.prototype.add=function(x,y){
	return x+y+this.first+this.second;
};

但是如果像上面一样添加了一个原型中已经存在的方法,那么会对这个方法进行重写

通过这样一层一层的继承,形成了一个继承的链式关系,我们称这种链式关系为原型链。通过上面的学习,我们知道子类是可以调用父类,而且这样的一种继承方式属于单继承(区别于C++的多继承方式),但是如果是原型链比较长,而且在这条链中也包含了两个或者以上的属性、方法,那么就会向上查找直到查找到object。万一还是没有找到的话,那么就只能返回undefined,如果找到,则返回最原型链最底层出现的方法、属性。

function foo() {
        this.add = function (x, y) {
            return x + y;
        }
    }

    foo.prototype.add = function (x, y) {
        return x + y + 10;
    }

    Object.prototype.subtract = function (x, y) {
        return x - y;
    }

    var f = new foo();
    alert(f.add(1, 2)); //结果是3,而不是13
    alert(f.subtract(1, 2)); //结果是-1

还有一点我们也是要注意的,原型链上面是不能直接进行赋值的,例如:

function Foo() {}
Foo.prototype = 1; // 无效

hasOwnProperty函数:

这个方法我们在第一节中已经提过了,这里也就不再重复,我们就直接贴代码运行一下就行了

// 修改Object.prototype
Object.prototype.bar = 1; 
var foo = {goo: undefined};

foo.bar; // 1
'bar' in foo; // true

foo.hasOwnProperty('bar'); // false
foo.hasOwnProperty('goo'); // true

如果是发生了像上面一样被重写的事情,那么我们是不能像这样直接使用的,在object类上进行获取

var foo = {
hasOwnProperty: function() {
    return false;
},
bar: 'Here be dragons'
};

foo.hasOwnProperty('bar'); // 总是返回 false

// 使用{}对象的 hasOwnProperty,并将其上下为设置为foo
{}.hasOwnProperty.call(foo, 'bar'); // true

<font size="4">详细的原理图如下所示<br>

通过这个图我们知道,我们直接调用的是f00对象下面被重写的hasOwenProperty,所有要调用系统提供的hasOwenProperty方法就需要在Object上调用,创建一个空的对象,因为所有的对象都会继承自Object,所以在空的对象上调用时再合适不过的

跨文件共享私有对象

通过上面的拓展的介绍,你一定是对JS的团队开发有了一定的了解,但是如果是团队之间要共享一些方法,那么我们应该怎样去实现呢?这就需要我们能够对代码里面的变量进行共享

var blogModule = (function (my) {
var _private = my._private = my._private || {},
    //应用声明周期内,调用开锁
    _unseal = my._unseal = my._unseal || function () {
        my._private = _private;
        my._seal = _seal;
        my._unseal = _unseal;
    };
	//加载完成后上锁,阻止外部访问
	_seal = my._seal = my._seal || function () {
        delete my._private;
        delete my._seal;
        delete my._unseal;
        
    },
    
	return my;
} (blogModule || {}));

子模块

我们在上面讨论的是一个单模块的应用,但是在实际的项目中,我们有可能会因为工程过大,所以我们需要再单模块的基础上去创建多个子模块

示例:

blogModule.test=(function (){
	
}());

值得一提的是方法后面的自执行的括号是可以去掉的,这个在正常的开发中比较少见,所以在此一提

自执行

一般我们的自执行是这样定义的

var foo=function(){/*代码*/};

这一段代码是可以正常运行的,但是不是所有的代码都可以加个()就自执行

function (){/*代码*/}()

这段代码执行会报错,因为解析器在解析全局function或者是function内部function关键字的时候,默认解析为函数声明,但是由前面的知识我们可以知道,函数声明是必须要命名的,这样一来,解析器就解析成为未命名的function,这样就出错了

但是如果你将代码写为如下的这种形式,那这样是否就能正常运行呢?

function foo(){/*代码*/}()

答案是不能正常运行,从这一段代码上来看,是好像一段带自执行的函数声明,但是函数声明后面的括号不想函数表达式是自执行的意思,而是分组操作符,分组操作符是不允许为空的, 所以这段代码出错

function foo(){ /* code */ }( 1 );

上面的这一段代码就不会发生错误,但是也不会执行

为了解决上面的问题,我们应该在其外面加上括号,这个的原理在上面我们已经有提过了,就是因为分组操作符会将里面的函数声明解析成为函数表达式,而函数表达式后面的括号指的是自执行

(function foo(){/*代码*/}())

下面我们就来通过例子了解什么是自执行和立即调用

// 这是一个自执行的函数,函数内部执行自身,递归
	function foo() { foo(); }

// 这是一个自执行的匿名函数,因为没有标示名称
// 必须使用arguments.callee属性来执行自己
	var foo = function () { arguments.callee(); };

// 这可能也是一个自执行的匿名函数,仅仅是foo标示名称引用它自身
// 如果你将foo改变成其它的,你将得到一个used-to-self-execute匿名函数
	var foo = function () { foo(); };

// 有些人叫这个是自执行的匿名函数(即便它不是),因为它没有调用自身,它只是立即执行而已。
	(function () { /* code */ } ());

// 为函数表达式添加一个标示名称,可以方便Debug
// 但一定命名了,这个函数就不再是匿名的了
	(function foo() { /* code */ } ());

// 立即调用的函数表达式(IIFE)也可以自执行,不过可能不常用罢了
	(function () { arguments.callee(); } ());
	(function foo() { foo(); } ());

// 另外,下面的代码在黑莓5里执行会出错,因为在一个命名的函数表达式里,他的名称是undefined
// 呵呵,奇怪
	(function foo() { foo(); } ());

这里我们出现了arguments.callee()之类的方法,下面我们就来介绍一下这些是什么?

aruguments对象代表正在执行的函数和调用它的函数的参数,calle方法返回正在执行的对象,其他的方法参数和用法详见

js的隐含参数(arguments,callee,caller)使用方法

js中的caller和callee属性

最后我们就贴出一个Module模型的例子来分享一下

// 创建一个立即调用的匿名函数表达式
// return一个变量,其中这个变量里包含你要暴露的东西
// 返回的这个变量将赋值给counter,而不是外面声明的function自身

var counter = (function () {
var i = 0;

return {
    get: function () {
        return i;
    },
    set: function (val) {
        i = val;
    },
    increment: function () {
        return ++i;
    }
};
} ());

// counter是一个带有多个属性的对象,上面的代码对于属性的体现其实是方法

counter.get(); // 0
counter.set(3);
counter.increment(); // 4
counter.increment(); // 5

counter.i; // undefined 因为i不是返回对象的属性
i; // 引用错误: i 没有定义(因为i只存在于闭包)

好的关于这篇文章的内容在此就结束了,如果喜欢的小伙伴请点个赞,你的前进的动力

posted @ 2016-03-16 10:50  热衷理财的技术渣渣  阅读(2514)  评论(0编辑  收藏  举报