JavaScript世界的一等公民 - 函数

本文深入浅出地介绍了JavaScript函数的特点及应用,从基本概念到高级用法,包括第一类函数、闭包、构造函数等内容,并探讨了ECMAScript 5的新特性如严模式和JSON支持。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

http://software.intel.com/zh-cn/articles/javascript-first-class-citizen-function/?cid=sw:prccsdn229032

http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/


简介

在很多传统语言(C/C++/Java/C#等)中,函数都是作为一个二等公民存在,你只能用语言的关键字声明一个函数然后调用它,如果需要把函数作为参数传给另一个函数,或是赋值给一个本地变量,又或是作为返回值,就需要通过函数指针(function pointer)、代理(delegate)等特殊的方式周折一番。
而在JavaScript世界中函数却是一等公民,它不仅拥有一切传统函数的使用方式(声明和调用),而且可以做到像简单值一样赋值、传参、返回,这样的函数也称之为 第一级函数(First-class Function)。不仅如此,JavaScript中的函数还充当了类的构造函数的作用,同时又是一个Function类的实例(instance)。这样的多重身份让JavaScript的函数变得非常重要。

一、JavaScript函数入门级

JavaScript函数像一般语言一样也是遵循先声明后使用的原则,函数名只能包含 字母、数字、下划线或$,且不能以数字开头。函数常见的声明方式有以下两种:
  
1// 直接声明函数myfunc
2function myfunc(/* arguments */) {
3}
4 
5// 把匿名函数赋值给本地变量myfunc
6var myfunc = function(/* arguments */) {
7}
注意,上面两种函数声明方式存在细微的差别:第一种方式在声明时就是一个 命名的函数,无论是声明在 调用之前调用之后,甚至是 不会执行到的位置(例如return语句之后或是永远不会为真的分支里),都在整个作用域可访问;第二种方式是通过把匿名函数赋值给变量的方式,严格意义上说这不是一个函数的声明(function declaration)而是一个函数表达式(function expression),在赋值之前这个函数不能被任何代码访问到,也就是说这个 赋值必须在调用之前完成,否则调用时会出现错误:"TypeError: undefined is not a function"。例如:
  
1myfunc1(); // 能够正常调用,因为myfunc1采用直接声明的方式
2 
3function myfunc1() {
4}
5 
6myfunc2(); // 出错 TypeError: undefined is not a function
7 
8var myfunc2 = function() {
9};
函数的基本调用方式与传统语言相同用一对括号调用: myfunc()。JavaScript的函数也支持直接或间接的递归(recursive)调用,例如经典的斐波那契函数用JavaScript可以这样实现:
  
1function fib(n) {
2  if (n == 1 || n == 2) {
3    return 1;
4  } else {
5    return fib(n - 2) + fib(n - 1);
6  }
7}
在JavaScript的函数可以处理变长参数,在函数内部都拥有一个名为arguments的局部变量,它是一个类数组(array-liked)的对象,里面包含了所有调用时传入的参数,有length属性表示参数的个数。例如:
  
1function test() {
2  alert(arguments.length);
3}
4 
5test(1); // 1
6test(1, 'a'); // 2
7test(true, [], {}); // 3
利用arguments可以实现类似C语言printf的功能,也可以用来实现方法的多态。

二、JavaScript函数进阶

2.1 匿名函数和嵌套函数

在JavaScript可以声明一个没有名称的函数,称为匿名函数(Anonymouse Function)。同时JavaScript还允许在函数内部声明函数,称为嵌套函数(Nested Function),嵌套函数的作用域为整个父函数。
在前面函数声明的部分就看到了匿名函数和嵌套函数的一种用法,由于匿名函数没有名称,不会引入新的变量污染上下文环境,而且会带来新的变量作用域,因此匿名函数常被用来防止全局环境污染。

JavaScript运行时中有一个特殊的全局环境(global object),这个对象上面存放全局的函数和变量,实际开发中经常会使用若干第三方的库或多个js文件,若不小心在全局对象引入重复的变量或函数声明,则会造成代码执行混乱。例如先后引入两个js文件,分别定义了自己的函数log作为内部使用,则第二引入的函数会 覆盖第一个的定义且 不会抛出任何错误,在后续的执行中调用log函数可能会造成错误。这时候使用一个匿名函数将整个js内的逻辑包装起来,就可以避免这种错误,这种方法已经被绝大多数开源js库使用。
  
1(function() { // 匿名函数
2 
3function log(msg) {
4    console.log(msg);
5}
6 
7// 其他代码
8 
9}()); // 立即执行
以上代码就是一个简单的示例,log函数的作用域被限制在这个匿名函数之内,而匿名函数则因为被外面一对小括号()包括起来,形成一个函数表达式,表达式的值是一个函数,紧接着一对小括号表示立即执行这个函数,让原有的代码正常执行一次。不过,这种方式声明的函数、通过var声明的变量等等都是内部的,不能被任何匿名函数以外的代码访问到。如果你需要对外暴露一些函数作为接口的话有如下几种方法:
  
01var mylib = (function(global) {
02 
03function log(msg) {
04  console.log(msg);
05}
06 
07log1 = log// 法一:利用没有var的变量声明的默认行为,在log1成为全局变量(不推荐)
08 
09global.log2 = log// 法二:直接在全局对象上添加log2属性,赋值为log函数(推荐)
10 
11return // 法三:通过匿名函数返回值得到一系列接口函数集合对象,赋值给全局变量mylib(推荐)
12   log: log
13};
14 
15}(window));
2.2 高阶函数(High-order Function)
如果函数作为参数或返回值使用时,就称为高阶函数,JavaScript中的函数都可以作为高阶函数来使用,这也是第一类函数的特征。下面我们就分别分析一下这两种使用方法。
  
01function negative(n) {
02  return -n; // 取n的相反值
03}
04 
05function square(n) {
06  return n*n; // n的平方
07}
08 
09function process(nums, callback) {
10  var result = [];
11 
12  for(var i = 0, length = nums.length; i < length; i++) {
13    result[i] = callback(nums[i]); // 对数组nums中的所有元素传递给callback进行处理,将返回值作为结果保存
14  }
15 
16  return result;
17}
18 
19var nums = [-3, -2, -1, 0, 1, 2, 3, 4];
20var n_neg = process(nums, negative);
21// n_neg = [3, 2, 1, 0, -1, -2, -3, -4];
22var n_square = process(nums, square);
23// n_square = [9, 4, 1, 0, 1, 4, 9, 16];
以上代码展示了把函数作为参数传入另一个函数process调用的示例,在process函数的实现中,把callback作为一个黑盒子看待,负责把参数传给它,然后获取返回值,在调用之前并不清楚callback的具体实现。只有当执行到20行和22行时,callback才被分别代表negative或square,分别对每个元素进行取相反值或平方值的操作。
  
01function generator() {
02  var i = 0;
03  return function() {
04    return i++;
05  };
06}
07 
08var gen1 = generator(); // 得到一个自然数生成器
09var gen2 = generator(); // 得到另一个自然数生成器
10var r1 = gen1(); // r1 = 0
11var r2 = gen1(); // r2 = 1
12var r3 = gen2(); // r3 = 0
13var r4 = gen2(); // r4 = 1
上面的代码展示了把函数作为返回值的示例,generator是一个自然数生成器函数,返回值是一个自然数生成函数。每次调用generator时都会把一个匿名函数作为结果返回,这个匿名函数在被实际调用时依次返回每个自然数。在generator里的变量i在每次调用这个匿名函数时都会自增1,这其实就是一个闭包。下面我们来介绍一下闭包.
2.3 闭包(Closure)
闭包(Closure)并不是一个新鲜的概念,很多函数式语言中都使用了闭包。在JavaScript中,当你在内嵌函数中使用外部函数作用域内的变量时,就是使用了闭包。用一个常用的类比来解释闭包和类(Class)的关系:类是带函数的数据,闭包是带数据的函数。
闭包中使用的变量有一个特性,就是它们不在父函数返回时释放,而是随着闭包生命周期的结束而结束。比如像上一节中generator的例子,gen1和gen2分别使用了相互独立的变量i(在gen1的i自增1的时候,gen2的i并不受影响,反之亦然),只要gen1或gen2这两个变量没有被JavaScript引擎垃圾回收,他们各自的变量i就不会被释放。在JavaScript编程中,不知不觉就会使用到闭包,闭包的这个特性在带来易用的同时,也容易带来类似内存泄露的问题。例如:
  
1var elem = document.getElementById('test');
2elem.addEventListener('click', function() {
3  alert('You clicked ' + elem.tagName);
4});
这段代码的作用是点击一个结点时显示它的标签名称,它把一个匿名函数注册为一个DOM结点的click事件处理函数,函数内引用了一个DOM对象elem,就形成了闭包。这就会产生一个循环引用,即:DOM->闭包->DOM->闭包...DOM对象在闭包释放之前不会被释放;而闭包作为DOM对象的事件处理函数存在,所以在DOM对象释放前闭包不会释放,即使DOM对象在DOM tree中删除,由于这个循环引用的存在,DOM对象和闭包都不会被释放。可以用下面的方法可以避免这种内存泄露:
  
1var elem = document.getElementById('test');
2elem.addEventListener('click', function() {
3  alert('You clicked ' + this.tagName); // 不再直接引用elem变量
4});
上面这段代码中用this代替elem(在DOM事件处理函数中this指针指向DOM元素本身),让JS运行时不再认为这个函数中使用了父类的变量,因此不再形成闭包。
闭包还会带来很多类似的 内存泄露问题,只有在写代码的时候着重注意一下闭包,尽量避免此类的问题产生。
2.4 类构造函数
JavaScript的函数同时作为类的构造函数,因此只要声明一个函数就可以使用new关键字创建类的实例。
  
1function Person(name) {
2  this.name = name;
3  this.toString = function() {
4    return 'Hello, ' + this.name + '!';
5  };
6}
7 
8var p = new Person('Ghostheaven');
9alert(p); // Hello, Ghostheaven!
在以上实例中Person函数作为类的构造函数使用,此时this指向新创建的实例对象,可以为实例增加属性和方法,关于详细的面向对象的JavaScript编程可以参考 这篇文章。这里我想要说的是,JavaScript函数作为类构造函数使用时的返回值问题。
  
1function MyClass(name) {
2  this.name = name;
3  return name;  // 构造函数的返回值?
4}
5 
6var obj1 = new MyClass('foo');
7var obj2 = MyClass('foo');
8var obj3 = new MyClass({});
9var obj4 = MyClass({});
上面的构造函数比较特别,有返回语句,那么obj1~obj4分别指向什么对象呢?实际结果是这样的:
  • obj1 = MyClass对象
  • obj2 = 'foo'
  • obj3 = {}
  • obj4 = {}
具体原因 这篇文章有解释,本文不再赘述,由于带返回值的构造函数会产生奇怪的结果,因此不要在构造函数中调用有返回值的返回语句(空return可以)。

三、JavaScript函数妖怪级

欢迎来到妖怪级函数授课区,在这里会交给你如何淡定自如地面对老怪。。。

3.1 Function类
在JavaScript运行时中有一个内建的类叫做Function,用function关键字声明一个函数其实是创建Function类对象的一种简写形式,所有的函数都拥有Function类所有的方法,例如call、apply、bind等等,可以通过instanceof关键字来验证这个说法。
既然Function是一个类,那么它的构造函数就是Function(它本身也是Function类的对象),应该可以通过new关键字来生成一个函数对象。第一个妖怪来了,那就是如何用Function类构造一个函数。Function的语法如下:
 
1new Function ([arg1[, arg2[, ... argN]],] functionBody)
其中arg1, arg2, ... argN是字符串,代表参数名称,functionBody也是字符串,表示函数体,前面的参数名称是可多可少的,Function的构造函数会把最后一个参数当做函数体,前面的都当做参数处理。
 
1var func1 = new Function('name', 'return "Hello, " + name + "!";');
2func1('Ghostheaven'); // Hello, Ghostheaven!
以上方法就通过Function构造了一个函数,这个函数跟其他用function关键字声明的函数一模一样。
看到这儿,很多人可能会问为什么需要这样一个妖怪呢?“存在的即是合理的”,Function类有它独特的用途,你可以利用它动态地生成各种函数逻辑,或者代替eval函数的功能,而且能保持当前环境不会被污染*。

3.2 自更新函数(Self-update Function)
在很多语言中,函数一旦声明过就不能再次声明同名函数,否则会产生语法错误,而在JavaScript中的函数不仅可以重复声明,而且还可以自己更新自己。自己吃自己的妖怪来了!
  
01function selfUpdate() {
02  window.selfUpdate = function() {
03    alert('second run!');
04  };
05 
06  alert('first run!');
07}
08 
09selfUpdate(); // first run!
10selfUpdate(); // second run!
这种函数可以用于只运行一次的逻辑,在第一次运行之后就整个替换成一段新的逻辑。

小结

JavaScript的函数灰常强大,在漂亮地解决很多问题的同时,也带来很多负面问题。妖怪级别的函数使用方法通常是一些鲜为人知的用法,除非特别必要不要轻易使用,否则会造成代码阅读困难,影响团队开发效率。

* 在新的ECMAScript中引入了 严格模式,在严格模式下eval函数受到了很大的限制,也能够保证环境不被污染




ECMAScript 5 Strict Mode, JSON, and More


Previously I analyzed ECMAScript 5's Object and Property system. This is a huge new aspect of the language and deserved its special consideration.

There are a number of other new features and APIs that need attention, as well. The largest of which are Strict Mode and native JSON support.

Strict Mode

Strict Mode is a new feature in ECMAScript 5 that allows you to place a program, or a function, in a "strict" operating context. This strict context prevents certain actions from being taken and throws more exceptions (generally providing the user with more information and a tapered-down coding experience).

Since ECMAScript 5 is backwards-compatible with ECMAScript 3, all of the "features" that were in ECMAScript 3 that were "deprecated" are just disabled (or throw errors) in strict mode, instead.

Strict mode helps out in a couple ways:

  • It catches some common coding bloopers, throwing exceptions.
  • It prevents, or throws errors, when relatively "unsafe" actions are taken (such as gaining access to the global object).
  • It disables features that are confusing or poorly thought out.

Most of the information about strict mode can be found in the ES5 specification [PDF] on page #235.

It should be noted that ECMAScript 5's strict mode is different from the strict mode available in Firefox (which can be turned on by going to about:config and enabled javascript.options.strict). ES5's strict mode complains about a completely different set of potential errors (whereas Firefox's existing strict mode tries to enforce some good practices, only).

How do you enable strict mode?

Simple. Toss this at the top of a program to enable it for the whole script:

"use strict";

Or place it within a function to turn on strict mode only within that context.

function imStrict ( ) {
   "use strict";
   // ... your code ...
}

Note the syntax that's used to enable strict mode (I love this!). It's simply a string in a single statement that happens to contain the contents "use strict". No new syntax is introduced in order to enable strict mode. This is huge. This means that you can turn strict mode on in your scripts - today - and it'll have, at worst, no side effect in old browsers.

As you may note from the examples here and in the previous post there are virtually no new syntax additions or changes to the language in ECMAScript 5. This means that you can write your ES5 scripts in a manner that will be able to gracefully degrade for older useragents - something that wasn't possible with ECMAScript 4. The way in which strict mode is enabled is a great illustration of that point in practice.

A neat aspect of being able to define strict mode within a function is that you can now define complete JavaScript libraries in a strict manner without affecting outside code.

// Non-strict code...

(function(){
  "use strict";
  
  // Define your library strictly...
})();

// Non-strict code... 
 

A number of libraries already use the above technique (wrapping the whole library with an anonymous self-executing function) and they will be able to take advantage of strict mode very easily.

So what changes when you put a script into strict mode? A number of things.

Variables and Properties

An attempt to assign foo = "bar"; where 'foo' hasn't been defined will fail. Previously it would assign the value to the foo property of the global object (e.g. window.foo), now it just throws an exception. This is definitely going to catch some annoying bugs.

Any attempts to write to a property whose writable attribute is set to false, delete a property whose configurable attribute is set to false, or add a property to an object whose extensible attribute is set to false will result in an error (these attributes were discussed previously). Traditionally no error will be thrown when any of these actions are attempted, it will just fail silently.

Deleting a variable, a function, or an argument will result in an error.

var foo =  "test";
function test ( ) { }

delete foo; // Error
delete test; // Error

function test2(arg) {
    delete arg; // Error
}

Defining a property more than once in an object literal will cause an exception to be thrown

// Error
{ foo:  true, foo:  false  }

eval

Virtually any attempt to use the name 'eval' is prohibited - as is the ability to assign the eval function to a variable or a property of an object.

// All generate errors...
obj. eval = ...
obj. foo =  eval;
var  eval = ...;
for  (  var  eval  in ...  )  { }
function  eval ( ) { }
function test ( eval ) { }
function ( eval ) { }
new  Function ( "eval" )

Additionally, attempts to introduce new variables through an eval will be blocked.

eval ( "var a = false;" );
print (  typeof a  )// undefined 
 

Functions

Attempting to overwrite the arguments object will result in an error:
arguments = [...]; // not allowed

Defining identically-named arguments will result in an error function( foo, foo ) {}.

Access to arguments.caller and arguments.callee now throw an exception. Thus any anonymous functions that you want to reference will need to be named, like so:

setTimeout ( function later ( ) {
   // do stuff...
  setTimeout ( later,  1000  );
}1000  );

The arguments and caller properties of other functions no longer exist - and the ability to define them is prohibited.

function test ( ) {
   function inner ( ) {
     // Don't exist, either
    test. arguments = ...;  // Error
    inner. caller = ...;  // Error
   }
}

Finally, a long-standing (and very annoying) bug has been resolved: Cases where null or undefined is coerced into becoming the global object. Strict mode now prevents this from happening and throws an exception instead.

( function ( ) { ...  } ). call (  null  )// Exception 
 

with(){}

with(){} statements are dead when strict mode is enabled - in fact it even appears as a syntax error. While the feature was certainly mis-understood and possibly mis-used I'm not convinced that it's enough to be stricken from the record.

The changes made in ECMAScript 5 strict mode are certainly varied (ranging from imposing stylistic preferences, like removing with statements, to fixing legitimately bad language bugs, like the ability to redefine properties in object literals). It'll be interesting to see how people begin to adopt these points and how it'll change JavaScript development.

All that being said, I'm fairly certain that jQuery is ES5-Strict compatible right now. Once an implementation of the language is made available (so that that premise may be tested) I'll happily switch jQuery over to working exclusively in strict mode.

JSON

The second major feature of the language is the addition of native JSON support to the language.

I've been championing this move for a long time and I'm glad to see it finally arrive in a specification.

In the meantime PLEASE start migrating your JSON-using applications over to Crockford'sjson2.js. It is fully compatible with the ECMAScript 5 specification and gracefully degrades if a native (faster!) implementation exists.

In fact, I just landed a change in jQuery yesterday that utilizes the JSON.parsemethod if it exists, now that it has been completely specified.

There are two primary methods for handling JSON: JSON.parse (which converts a JSON string into a JavaScript object) and JSON.stringify (which convert a JavaScript object into a serialized string).

JSON.parse( text )

Converts a serialized JSON string into a JavaScript object.

var obj = JSON. parse ( '{"name":"John"}' );
// Prints 'John'
print ( obj. name  );

JSON.parse( text, translate )

Use a translation function to convert values or remove them entirely.

function translate (key, value )  {
   if  ( key ===  "name"  )  {
     return value +  " Resig";
   }
}

var obj = JSON.parse('{"name":"John","last":"Resig"}', translate);
// Prints 'John Resig'
print( obj.name );

// Undefined
print( obj.last );

JSON.stringify( obj )

Convert an object into a serialized JSON string.

var str = JSON. stringify ( {  name"John"  } );
// Prints {"name":"John"}
print ( str  );

JSON.stringify( obj, ["white", "list"])

Serialize only a specific white list of properties.

var list =  [ "name" ];
var str = JSON. stringify ( { name"John", last:  "Resig" }, list );
// Prints {"name":"John"}
print ( str  );

JSON.stringify( obj, translate )

Serializes the object using a translation function.

function translate (key, value )  {
   if  ( key ===  "name"  )  {
     return value +  " Resig";
   }
}

var str = JSON.stringify({"name":"John","last":"Resig"}, translate);
// Prints {"name":"John Resig"}
print( str );

JSON.stringify( obj, null, 2 )

Adds the specified number of spaces to the output, printing it evenly.

var str = JSON. stringify ( {  name"John"  }null2 );
// Prints:
// {
//   "name": "John"
// }
print ( str  );

JSON.stringify( obj, null, "\t" )

Uses the specified string to do the spacing.

var str = JSON. stringify ( {  name"John"  }null"\t" );
// Prints:
// {\n\t"name": "John"\n}
print ( str  );

Additionally, a few new generic methods have been added to some of the base objects but, frankly, they aren't that interesting. The results from String, Boolean, and Number are just equivalent to calling .valueOf() and the result from Date is equivalent to calling.toISOString()

// Yawn...
String. prototype. toJSON
Boolean. prototype. toJSON
Number. prototype. toJSON
Date. prototype. toJSON

.bind()

A welcomed addition to the language is a built-in .bind() method for enforcing the context of a function (virtually identical to Prototype's .bind implementation).

Function.prototype.bind(thisArg, arg1, arg2....)

Enforces the 'this' of the specified function to a specific object - and passing in any specified arguments.

var obj =  {
  method:  function ( name ) {
     this. name =  name;
   }
};

setTimeout( obj.method.bind(obj, "John")100 );

Considering how long this function (and its equivalents) have been around it's a welcome addition to the language.

Date

Dates are now capable of both parsing and outputting ISO-formatted dates. Thank goodness, about time. rimshot

The Date constructor now attempts to parse the date as if it was ISO-formatted, first, then moves on to the other inputs that it accepts.

Additionally, date objects now have a new .toISOString() method that outputs the date in an ISO format.

var date =  new Date ( "2009-05-21T16:06:05.000Z" );

// Prints 2009-05-21T16:06:05.000Z
print( date.toISOString() );

.trim()

A native, built-in, .trim() is now included for strings. Works identically to all the other trim methods out there - with the potential to possibly work faster.

Steven Levithan has discussed the trim method in great depth.

Array

The JavaScript Array Extras that've been around for, what seems like, forever are finally formally specified. This includes the following methods: indexOf, lastIndexOf, every, some, forEach, map, filter, reduce, and reduceRight.

Additionally a new Array.isArray method is included, providing functionality very similar to the following:

Array. isArray =  function ( array  )  {
   return Object. prototype. toString. call ( array  ) ===  "[object Array]";
};

Altogether I think ECMAScript 5 makes for an interesting package. It isn't the massive leap that ECMAScript 4 promised but it is a series of respectable improvements that reduces the number of obvious bugs while making the language safer and faster. I'm looking forward to when some implementations start to go public.

Posted: May 21st, 2009 · 3978 ► Retweet

If you particularly enjoy my work, I appreciate donations given with Gittip: 

62 Comments (Show Comments)


Comments are closed.
Comments are automatically turned off two weeks after the original post. If you have a question concerning the content of this post, please feel free to contact me.








评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值