函数就是最基本的一种代码抽象的方式。
1 函数定义和参数
1.1 函数的定义
JavaScript函数定义的方法有两种:
function name (x) {
...
}
第一种是像上述这样的
function
:指出这是一个函数定义name
:定义这个函数名(x)
:是函数参数{...}
:是函数代码块
第二种是使用匿名函数对变量赋值,function (x) {...}
这样的模式就是匿名函数,具体如下:
var name = function (x) {
...
};
两者使用时有区别,最大区别在用函数声明会前置,而变量赋值就不会前置,还有注意第二种方法尾部需要添加分号。还有的就是与python相同,函数体内部一旦执行到return
语句时,函数就执行完毕,并将结果返回,假如没有return
语句,函数执行完毕后也会返回结果,只是结果为undefined
。
由于JavaScript引擎在行末自动添加分号的机制,
return
语句的连接不要分行,除了使用花括号。
1.2 函数的参数
函数的调用与python一样,只是想讲一下关于参数的问题。
python参数类型有:位置参数,默认参数,比较重要的可变参数以及关键字参数,还有命名关键字参数。
对比python,JavaScript本身函数也就完全不同,其函数本身可以接受比定义参数还多的传入参数,甚至不接受参数也照样不会报错,最多返回NaN
。不过仔细对比,还是能找到相似的地方:
arguments
:可以,也只能在函数内部起作用,指向当前函数的所有传入参数,类型像是一个Array(运用它可以制造出默认参数的效果)。
arguments
有一个callee
属性,用来指向当前函数
...rest
:rest
参数可以类比可变参数*args
,用法也差不多(...
标识),跟可变参数一样只能放在最后,多余的参数以数组形式交给变量rest
,如果没有多余的参数会返回空集{}
。
function func (x,y,...rest) {
console.log('a = ' + a);
console.log('b = ' + b);
console.log(rest);
}
2 变量作用域
2.1 全局变量
不在任何函数内定义的变量就具有全局作用域(全局变量)。
实际上,JavaScript默认有一个全局对象window
,全局作用域的变量实际上被绑定到window
的一个属性(包括一些自带的函数)
var course = 'Learn JavaScript';
alert(course); // 'Learn JavaScript'
alert(window.course); // 'Learn JavaScript'
全局变量会绑定到window
上,网页导入不同的JavaScript文件,如果都使用了相同的全局变量,都会造成命名冲突。
减少冲突的一个方法是把自己的所有变量和函数全部再绑定到一个全局变量中。例如:
// 唯一的全局变量MYAPP:
var MYAPP = {};
// 其他变量:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;
注:与python不同,python的全局变量在函数内进行修改是不会影响到函数外的值,然而JavaScript若是修改了,则是会直接影响到全局变量的值
2.3 局部变量
JavaScript的局部变量作用域实际上是函数内部。
一个变量在函数体内部申明,则该变量的作用域为整个函数体,在函数体外不可引用该变量。
即在for循环等语句块中是无法定义具有局部作用域的变量。(比如在for循环语句定义了一个变量,只要是在相同的函数环境内,在循环外部也是可以调用该变量)。
为了解决块级作用域,ES6引入了let
,用let
替代var
可以申明一个块级作用域的变量,这样子在循环语句内使用let
,在外部就无法调用该值了。
'use strict';
function foo() {
var sum = 0;
for (let i=0; i<100; i++) {
sum += i;
}
i += 1; // SyntaxError语法错误
}
2.4 变量提升
JavaScript定义函数与变量,它会先扫描整个文件:
- 第一步:参数传入。传入的参数会马上声明,赋值给响应的变量,而没有传入的,则初始化为
undefined
。 - 第二步:函数声明提前。注意是函数声明而不是函数表达式,当发生命名冲突时,函数声明会覆盖之(比如与传入的参数名字相同导致冲突)
- 第三步:变量声明提前。变量声明提前当初始化的变量值为
undefined
,如果发生命名冲突,会被忽略掉(比如与传入参数名字相同则变量声明会被忽略掉,注意变量声明并不包含赋值动作,赋值动作要等到执行代码的时候才会发生)
(注:由于没有使用let
声明的话,JS是没有块级作用域的,也就是说循环语句中的变量,函数都会提前)
也因此,所以在JS中,可以提前调用函数,提前调用变量也是不会报错的,只是假如调用变量在赋值代码之前,变量值会设置为undefined
,因为申明提前了,并没有真正意义上的赋值(注:前面说到函数声明发生冲突会覆盖,但是到了执行代码阶段,如果发生赋值了,那么函数也是会被取缔的)。
看个代码加深印象:
alert(x); //function
var x=10;
alert(x); //10
x=20;
function x () {}
alert(x); //20
if (true) {
var a=1;
} else {
var b=2;
}
alert(a); //1
alert(b);
//undefined,这里注意了,由于没有块级作用区,所以变量b还是提前声明,所以这里调用b不会报错,但由于没有进行赋值,所以b为undefined。
所以在编辑函数时,请严格遵守“在函数内部首先申明所有变量”的规则。最常见的做法是用一个var申明函数内部用到的所有变量(变量之间用 逗号 隔开,最后才跟上分号),如
function func() {
var
x = 1,
y = x + 1,
z, i,
a,b,c;
for (i=0; i<100; i++) {
...
}
}
3 方法
在python里头,对一个类绑定变量叫做属性,绑定函数叫做方法。
在JavaScript里头,所有设定的变量都是绑定在window
的属性,而绑定函数叫做方法(尽管可能绑定在属性上,但属性也是对象)
3.1 this
this
总是指向 函数的直接调用者 (而非间接调用者);
如果有new
关键字,this 指向new
出来的那个对象;
在事件绑定中,this
指向触发这个事件的对象
在一个方法的内部,有一个特殊的变量this
,它始终指向当前对象(类似于python中的self
),使用它可以调出当前对象的一些属性值。(讲解)
当前对象四个字很重要,随着调用this
变量的环境不同,this
指向的对象也就不一样了,比如说:
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1990,
age: getAge//注意没有括号,有括号就报错了
};
/* 正常调用 */
xiaoming.age(); // 25, 正常结果
/* 单独调用 */
getAge(); // NaN
/* 赋值调用 */
var fn = xiaoming.age; // 先拿到xiaoming的age函数
fn(); // NaN
/* 同时在一个返回函数里面,this的属性值也是会变化 */
/* 要保证this指向正确,必须用obj.xxx()的形式调用!*/
要保证this指向正确,必须用
obj.xxx()
的形式调用!同时调用函数里还不能有重构返回函数
这其实是JavaScript设置时一个重大的错误,ECMA决定,在strict
模式下让函数的this
指向undefined
,即上式在strict
模式会报错:
var fn = xiaoming.age;
fn(); // Uncaught TypeError: Cannot read property 'birth' of undefined
但这只是让错误及时暴露出来而已,并没有解决。
3.2 func.apply(obj,array)
在构建闭包函数而导致this
指向错误的情况下,大可以新建一个that
属性对this
进行临时的保存。
而函数本身的apply()
方法,可以指定函数的this
指向哪个对象。 apply()
方法接收两个参数,第一个参数就是需要指向的对象,第二个参数是Array类型,表示函数参数(参数为空[]
)。
getAge.apply(xiaoming,[]);
3.3 func.call(obj,args)
call()
方法与apply
方法类似,唯一区别是:
apply()
把参数打包成Array再传入;call()
把参数按顺序传入。
eg:
Math.max.apply(null,[1,2,3]);
Math.max.call(null,1,2,3);
对普通函数调用,我们通常把
this
绑定为null
。
3.4 func.bind(obj,args)
func.bind(obj,args)
同样起到一个绑定指向对象作用,不过与前两者不同,返回的是一个函数
this.a=1;
var module = {a:2};
function geta () {
return this.a
}
var geta2 = geta.bind(module);//返回是一个函数
geta2();//2
因为bind
返回的是函数,所以还可以传入参数绑定以达到预设参数的效果
3.5 箭头函数
箭头函数与python的匿名函数有点类似,JavaScript本身也有匿名函数的写法,如:function (x) {...}
,而箭头函数可以是:(x,y)=>{...}
(只包含一个表达式的,连return
语句都可以省,像是这样x=>x*x
)
(x,y)
:代表函数参数,没有参数传入时写成()
{...}
:表示函数语句,所以当函数只是返回对象object时,要加括号区分,如({name:seiei})
箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的this
是词法作用域,由上下文确定。
可以简单的理解,JS 每一个 function 有自己独立的运行上下文,而箭头函数不属于普通的 function,所以没有独立的上下文。所以在箭头函数里写的
this
其实是包含该箭头函数最近的一个 function 上下文中的this
(如果没有最近的 function,就是全局)。
因此前文提到的构建闭包函数导致this
值指向错误的问题除了另外创建绑定that
之外,还可以使用箭头函数:
/* 闭包,此时调用xiaoming.age(),会报错 */
var xiaoming={
name:xiaoming;
brith:19;
age:function () {
function f () {
return new Date().getFullYear()-this.brith;//指向window或undefined
}
return f;//闭包
}
};
/* 使用箭头函数 */
var xiaoming = {
name:xiaoming;
brith:19;
age:function () {
function f () {
return ()=>{return new Date().getFullYear()-this.brith;}//this总是指向词法作用域,也就是外层调用者obj
}
return f;
}
};
注:使用了箭头函数定义的
this
值,不能在通过apply
以及call
再重新定义指向了
4 高阶函数
一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
以下函数都是对象的方法,设置的函数都要接受三个参数
item,index,array
4.1 map/reduce
4.1.1 map
python中的map作用完全一样,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Array返回,语法如下:
array.map(func);
注:
map()
接收的回调函数可以有3个参数(其他几个介绍的高阶函数也是一样):callback(currentValue, index, array)
(值,键,array本身),通常我们仅需要第一个参数,而忽略了传入的后面两个参数。但是当传入的是parseInt()
这样可以接受两个数值的函数时,问题就出来了。
4.1.2 reduce
python中的reduce作用完全一样,把一个函数作用在一个Array,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算(reduce正如其名)。语法如下:
array.reduce(func);
4.2 filter
python中的map作用完全一样,它用于把Array的某些元素过滤掉,然后返回剩下的元素。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是true还是false决定保留还是丢弃该元素(true
留下),语法如下:
array.filter(func);
4.3 sort
python中的map作用完全一样,只不过JavaScript的排序有点问题,它对number的排序是会先将其转化为字符串在比较。。。
幸好它是一个高阶函数,可以传入一个函数对其进行自定义排序:
var arr=[5,1,8,3];
arr.sort(function (x,y) {
if (x<y) {
return -1;//如果想第一个参数值位于第二个之前返回-1
}
else if (x>y) {
return 1;
}
else {
return 0;
}
}
);
//[1,3,5,8],如果想由大到小,可以把-1变为1,1变为-1
注:sort()方法会直接对Array进行修改,它返回的结果仍是当前Array
4.4 every 与 some
every 与 some的作用都是把数组逐个放进一个判断函数中返回true或false,其中every是数组中每一个都符合才返回true,而some就只需某一个符合就可以了
var nums=[1,2,3,4];
var everynums = nums.every(function (item,index,array) {
return (item>2);
});
alert(everynums);//false
var somenums = nums.every(function (item,index,array) {
return (item>2);
})
alert(somenums);//true
5 闭包
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。而相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”。
闭包最大的作用就是可以封装一个私有变量,要注意的是闭包切记使用循环变量,因为闭包本身并不是立即执行,很可能当函数返回执行的时候,变量已经变了。一定要使用的话,就要在创建一个函数来返回一个会立即执行的匿名函数。
(function(x) {...}(arg))
/*
立即执行的匿名函数语法,就是后跟加括号、参数,另外要把全体用括号框起来以区分
*/
6 生成器
ES6定义generator标准借鉴了Python的generator的概念和语法。
JavaScript定义generator使用function*
,同样使用return
以及yield
。不过要注意的是当执行完return
语句后,生成器就结束了,再调用也不会往下执行。此外使用for in
形式输出生成器,是无法输出return
语句的,而使用next()
语句就可以输出return
语句,使用next()
语句还可以输出生成器结束没有的信息,如:
{ value: 0, done: false }
{ value: '1', done: true }
注:JavaScript使用
next()
语句语法与python不同它的语法是:generator.next()