函数除了实参以外,每次调用还会拥有另一个值 -- 本次调用的上下文,即this关键字的值。
如果函数挂载在对象上作为对象的属性,就称它为对象的方法,通过对象来调用它时候,该对象就是此次调用的上下文(context),也就是该函数this的值。
函数定义有两种方式(结合下面的例子):
- 定义表达式,局部作用域将包含绑定到函数对象的名称;可以被作用域内前面的代码所调用
- 声明语句,尽管变量提前申明了,但是赋值没有提前申明因此是无法使用的
// 定义表达式
function add(){}
// 声明语句
var a = function(){}
method1(); // 1
method2(); // Error: method2 is not function
function method1(){console.log(1)};
var method2 = function(){console.log(2)};
函数的返回值,如果有return语句则返回表达式的值给调用者(如果return后面没跟表达式就是undefined),如果没写return语句则返回undefined。
可以访问嵌套它们的函数的参数和变量
疑问:关于两种定义方式哪种不可以出现在循环、条件判断、或try/catch/finally以及with语句?
ECMAScript3和非严格的ECMASript5,调用的上下文(this的指向)是全局对象,严格模式下则是unfedined。
方法和this关键字是面向对象编程范例的核心。
在对象中调用的函数称为方法调用,通过this指向调用的对象(上下文)。
如果方法调用仍旧返回一个对象,那么又可以继续进行调用,从而形成方法链条的写法,比如:
shape.setX(100).setY(100).setSize(50).setOutline("red").setFill("blue").draw();
在嵌套调用中,this关键字不会从调用它的函数继承this,如果嵌套函数作为方法调用,则this指向它的对象,如果作为函数调用则指向全局变量(非严格模式下)或者unfefined(严格模式下):
var o = {
m: function(){
var self = this;
console.llg(this === o); // true
f();
function f(){
console.log(this === o); // false, this是全局对象或者undefined
console.log(self === o); // true
}
}
}
构造函数会基于构造函数的prototype创建一个新的空对象,并将这个对象作为调用的上下文,因此可以在构造函数中通过this来引用这个新创建的对象。比较有欺骗性的,new o.m()看起来是一个方法调动,但调动上下文不是o。
构造函数不用return返回新构造的对象,如果显示地使用return返回一个对象,那么调用表达式的值就是这新对象,如果没有返回值或者返回的是一个原始的值,它将忽略返回值并将仍旧使用新对象作为调用结果。
可以用call和apply来显示地指定调用时this所指向的值,哪怕这个函数并不属于那个对象。
this始终指向定义所在上下文的this, call/apply/bind都不能改变它的指向
var o = {m: function(){var f = ()=>{console.log(this);}; f(); }}
o.m() // {m:f}
var m = o.m; m(); //window
var o = {m: function(){function f(){console.log(this);};f(); }}
o.m() // window
var f = ()=>{console.log(this);};
f.call({x:1}); // window
本节介绍调用函数的实参个数和定义的形参个数不一致时的情形,以及如何测试实参个数类型,并处理非法传入的参数。
当传入的实参可能比定义的形参要少的时候,最好让后面的形参变为可选的,通过注释加以说明,并在传入和不传入的时候都能得到处理:
functiongetPropertyNames(o,/*optional*/a){if(a===undefined)a=[];//如果未定义,则使用新数组for(varpropertyino)a.push(property);returna;}//这个函数调用可以传入1个或2个实参vara=getPropertyNames(o);//将o的属性存储到一个新数组中getPropertyNames(p,a);//将p的属性追加至数组a中
(美)David Flanagan. JavaScript权威指南(原书第6版) (Chinese Edition) (Kindle Locations 3697-3701). 机械工业出版社. Kindle Edition.
当传入函数的实参个数超过形参个数的时候,可以通过arguments获取到实参对象的引用。
实参对象具有一个重要的特性,即在ES3和ES5非严格模式下,arguments对象只是参数的别名,改其中一个会被另外一个同步到。在严格模式下,则无法使用arguments无法给他赋值。
function f(x){
arguments[0] = 2;console.log(x);
}
f(1);
在非严格模式下,通过实参对象还可以获取到两个对象callee和caller,它们分别指向当前正在执行的函数 和 执行函数的函数。
这样的好处是,参数的传递顺序无需在关心。
形参并未声明类型,传入的之前也未作任何检查,可以在函数代码中根据需求对实参类型进行检验。
函数是一种特殊的值,它也可以拥有属性,但我们再函数调用的过程中需要一些”静态“变量来保持某些值不变,最简便的办法就是给函数定义属性,而非定义全局变量。
uniqueInteger.counter = 0;
function uniqueInteger(){
return uniqueInteger.counter++;
}
function factorial(n){
if(!(n in factorial)) {
factorial[n] = n * factorial(n - 1);
}
return factorial[n];
}
factorial[1] = 1;
3.10.1中介绍了在函数中声明的变量在整个函数体内,在函数外部不可见;不在任何函数中声明的变量是全局变量,在所有的JavaScript程序中都可以访问。JavaScript中是无法声明只在一个代码块内可见的变量,基于整个原因,我们将函数用作临时的命名空间,在这个空间内的变量不会污染到全局的命名空间。
技术角度来讲,函数都是闭包,它们都关联到作用域链。函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。真正微妙的是,当定义函数的作用域链 和 调用函数时闭包所指向的作用域链不是指向同一个。如下的函数嵌套中,嵌套的函数被作为返回值返回就发生了这种情况,很多强大的编程技术都用到了它,掌握它至关重要。
先看一个不难理解的例子,返回嵌套函数的执行结果:
var scope = 'global scope'
function checkscope(){
var scope = 'local scope';
function f(){ return scope; }
return f();
}
checkscope();
我们再将代码修改一些看会发生什么:
var scrope = 'global scope'
function checkscope(){
var scope = 'local scope';
function f(){ return scope; }
return f;
}
checkscope()();
我们在定义f这个函数的作用域外面调用这个嵌套的函数,会输出什么呢?
函数的执行用到了作用域链,作用域链是在函数定义的时候创建的,嵌套的函数定义在这个作用域链里,因此scope一定是局部变量,不论何时何地执行f(),这种绑定在执行f()时依然有效,这个特性强大到让人吃惊,它可以捕捉到局部变量(和参数)并一直保存下来。这个规则可以概括为,函数定义的作用域链到函数执行时依然有效。
在前例中uniqueInteger()函数的自身属性保存了每次返回的值得,但是恶意代码很容易篡改它或者将非整数的值赋值给它,我们使用闭包将这个值设置成函数调用的局部变量:
var uniqueInteger = (function(){
var counter = 0;
return function(){
return counter ++;
}
})();
像counter的变量可以在内部定义的多个嵌套函数中访问,这些函数共享一个作用域链:
function counter() {
var n = 0;
return {
count: function(){return n ++;},
reset: function(){n = 0;}
}
}
var c = counter(), d = counter();
c.count(); // 0
d.count(); // 0
c.reset();
c.count(); // 0
d.count(); // 1
每次调用counter()都会创建一个新的作用域链和新的私有变量,不同调用得到的计数器对象包含不同的私有变量。
需要小心的是那些不希望共享的变量会在不经意共享给其他闭包,看如下这个例子:
function constfunc(v) {return function(){return v}}
var funcs = [];
for(var i = 0; i < 10; i ++)funcs[i] = constfunc(i);
funcs[5](); // 5
function constfunc() {
var funcs = [];
for(var i = 0; i < 10; i ++)
funcs[i] = function(){return i}
return funcs;
}
var funcs = constfunc();
funcs[5]; // 10
后面的例子中创建了10个闭包但是它们是在同一个函数中定义的,因此共享一个i值,因此数组中的函数返回值都是同一个,显然不是我们想要的效果。
注:闭包再被嵌套的函数访问时候,是无法访问到this的,arguments同理,通常会定义别名来获取到引用。后续的例8.5中会解释使用编程技巧来定义闭包从而能访问到this和arguments的值。
代表函数形参的个数,可以跟arguments.length比较用于确认参数传递个数是否满足条件。
指向函数的原型对象,每个函数的原型对象都不同。
它们的第一个参数是调用的上下文,在函数中可以通过this获取到。如下两小块代码功能很类似
var f = function(){}
f.call(o);
o.f = function(){};
o.f();
delete o.f();
ES5的严格模式下,第一个参数会变成this的值得,哪怕是null和undefined。
在ES3和ES5的非严格模式下,null和undefined会被全局对象替代,其他原始值则会被包装成相应对象。
关于bind你所不知道的是,除了第一个参数被绑定到this意外,第二个参数会被绑定到对应的实参,这种编程技术称为柯里化:
var sum = function(x,y){
return x + y;
}
var succ = sum.bind(null,1);
succ(2);
关于执行bind后的函数,如果用作构造函数的情形,该节有一些描述,此处略过。
大多数函数返回源码的字符串,内置函数则返回 [navtive code]的字样。
可以使用Function的构造函数来定义函数,如下两行代码是一致的:
var f = new Function('x', 'y', 'return x + y');//最后一个参数会作为函数体
function f(x, y) {return x + y;}
需要注意的是,使用Function所创建的函数,并不使用词法作用域,它作为顶层函数执行。
var o = {
test(){
var f = new Function('x', 'y', 'console.log(this);return x + y');
f();
}
}
o.test();//window
可调用对象可以用函数表达式来调用,所有函数都是可调用的,并非所有可调用对象都是函数。该部分内容已经比较陈旧,不再论述。
举了计算平均值和方差的例子,表明使用 reduce、map方法可以简化代码。
接收一个或多个函数作为函数,然后返回一个新的函数。
该节演示了一些不完全函数,以及和高阶函数的混合调用,作为一种范式不难理解,感兴趣再细看。
array(a,n) {return Array.prototype.slice.call(a,n || 0);}
function partialLeft(f) {
var args = arguments;
return function(){
var a = array(args,1);
a = a.concat(array(arguments));
return f.apply(this, a);
}
}
function partialRight(f) {
var args = arguments;
return function(){
var a = array(arguments);
a = a.concat(array(args, 1));
return f.apply(this,a);
}
}
function partial(f) {
var args = argumnets;
return function(){
var a = array(args, 1);
var i = 0, j = 0;
for(;i < a.length; i++) {
if(a[i]===undefined) a[i] = arguments[j++];
}
a = a.concat(array(arguments, j));
return f.apply(this,a);
}
}
var f = function(x,y,z) {return x*(y-z)}
partialLeft(f, 2)(3,4);//-6
partialRight(f, 2)(3,4);//6
partial(f, undefined, 2)(3,4);//-6
在函数中将曾经计算过的结果缓存起来的技巧称为记忆。