一. 函数声明(分类)
1.1 function命令
function
命令声明的代码块就被称为一个函数。语法格式为:
function 函数名 (形参列表) {
// 执行体
};
// 求两数之和的add函数
function add (a,b) {
return a + b;
};
1.2 函数表达式
实质是把一个匿名函数赋值给变量。这个匿名函数又称为函数表达式。
var 变量名 = function (形参列表) {
// 执行体
};
// 求两数之和的add函数
var add = function (a,b) {
return a + b;
};
1.3 Function构造函数
Function构造函数可以接收任意数量的参数,只有最后一个参数才会被认定为执行体。这个方法不太直观,不建议使用。
// 上述add函数等价于
var add = new Function(
'a',
'b',
'return a + b'
);
1.4 ES6箭头函数
1.4.1 基本语法
ES6标准中新增了一种更加简洁的匿名函数书写方式 —— 箭头函数(Arrow Function),一般用做函数表达式或者回调函数。基本语法是:
( 形参列表 ) => { 执行体 }
// 变量 = 函数表达式
let add = (a,b) => {
return a + b;
};
// 回调函数
["zevin",21,"code"].forEach(item => console.log(item));
——————OUTPUT——————
zevin
21
code
箭头函数最特色的当然是它的简写规则:
- 当函数参数只有一个,圆括号
()
可以省略;但是没有参数时,圆括号()
不可以省略。
var num = a => {
return a + 1;
};
var index = () => {
return true;
};
- 当执行体只有一行
return
语句时,可以省略大括号{}
;有多行代码时大括号{}
就不能省略。
var num = a => {
return a + 1;
};
// 等价于
var num = a => a + 1;
var index = () => {
var str = "hello world";
return str;
};
// {}不可省略
结合一下上述的两个极端条件:只有一个参数x,返回x值。你就可以看到最最简洁的写法:
var a = x => x;
- 如果返回一个对象,需要特别注意,如果是单表达式要返回自定义对象,不写括号会报错,因为和函数体的
{ ... }
有语法冲突。
注意,用小括号包含大括号则是对象的定义,而非函数主体。
x => {key: x} // 报错
x => ({key: x}) // 正确
1.4.2 箭头函数内部的this指向
箭头函数本身并没有this,它的this指向包含它的外部函数的this。
具体的在之后讲函数的this指向(回到目录)的时候会一起讲。或者也可以看另一篇详细讲箭头函数的博客:
想写的有点多,写完就更新
二. 函数的变量提升
函数声明与var类似,都会有变量提升。但是根据具体的函数声明方式的不同,具体的提升情况也不同:
- function命令声明的函数会整体提升,允许提前调用:
console.log(add(1,2));
function add(a,b){
return a+b;
};
// 3
- 使用函数表达式声明的函数只会提升函数名,并不能提前调用,会报
TypeError: xxx is not a function
的错误:
console.log(add(1,2));
var add = function(a,b){
return a+b;
};
// TypeError: add is not a function
// 等价于
var add;
console.log(add(1,2));
add = function(a,b){
return a+b;
};
❀拓展一下❀
- 可以深入理解一下以下代码中的两者报错信息为什么不同 ?
bar(); // bar is not defined
foo(); // foo is not a function
var foo = function(){};
- 所以在JavaScript中并不存在“方法重载”(overload),重复声明的函数只会覆盖前者。
function foo(a,b){
return a*b;
};
function foo(a,b,c){
return a+b+c;
};
console.log(foo(1,2));
——————OUTPUT——————
NaN
三. 函数的调用(实例方法)
严格来说函数调用的方法只有前三种:圆括号()
,call()
方法和apply()
方法。第四种bind()
方法只是绑定函数的作用域,其实并没有立即调用函数的功能。
3.1 圆括号()
最常见的函数调用方法,默认作用域是全局对象:
函数名(实参列表);
// 例如
add(1,2);
3.2 Function.prototype.call()
函数实例方法call()
的作用是让函数在指定的作用域中携带指定的参数执行。基本语法为:
函数名.call(thisValue,实参列表);
var obj = {
base:10
};
function add(a, b) {
console.log(this.base + a + b);
};
add.call(obj, 1, 2); // 13
上述代码就相当于是给add函数指定了一个作用域obj,即add函数的内部属性this指向obj对象,可以调用obj中的属性。this看不懂的话可以继续看后续章节有介绍。(回到目录)
如果call()
方法没有参数,或者参数为null
或undefined
,则等同于指向全局对象。
var num = 1;
var obj = { num: 2 };
function func() {
console.log(this.num);
};
func.call() // 1
func.call(null) // 1
func.call(undefined) // 1
func.call(window) // 1
func.call(obj) // 2
3.3 Function.prototype.apply()
apply()
方法和call()
方法很类似,唯一的区别就是接收的实参形式不同,call()
方法接收的是实参列表,apply()
方法接收的是一个实参数组。
函数名.apply(this,实参数组);
// 例如
add.apply(this,[1,2]);
我们可以利用apply()
实参数组这个特性做到很多操作:
- 找出数组最大元素
结合使用apply
方法和Math.max
方法,就可以返回数组的最大元素。
var arr = [11, 7, 12, 98, 5];
Math.max.apply(null, arr)
// 98
- 将数组的空元素变为undefined
Array.apply(null, ['a', ,'b'])
// [ 'a', undefined, 'b' ]
空元素与undefined
的差别在于,数组的forEach方法会跳过空元素,但是不会跳过undefined
。因此,遍历内部元素的时候,会得到不同的结果。
var arr = ['a', , 'b'];
arr.forEach((item) => console.log(item));
// a
// b
Array.apply(null, arr).forEach((item) => console.log(item));
// a
// undefined
// b
- 把类数组对象转换为真数组
其实call()
方法也可以,两者作用一样。顺便复习一下类数组对象都有哪些 ?
(字符串,arguments,DOM元素集,jQuery对象)
Array.prototype.slice.call({0: 1, length: 1}) // [1]
Array.prototype.slice.apply({0: 1, length: 1}) // [1]
3.4 Function.prototype.bind()
bind()
方法用于将函数体内的this绑定到某个对象,然后返回一个新函数。需要注意的是,bind()
方法并没有立即调用函数的作用,要注意和call()
,apply()
区分开。
var number = {
num: 0,
add: function () {
this.num++;
console.log(this.num);
}
};
var func = number.add;
func();
// NaN
// 函数add内部this指向函数func继而指向全局对象
// 全局对象中num未定义,undefined++ == NaN
var func = number.add.bind(number);
func();
// 1
// bind方法指定作用域为number
// 等价于
number.add.call(number);
// 1
四. 函数的属性和方法
4.1 name属性
函数的name属性用于返回函数的名称:
function name () {};
name.name
// "name"
如果是通过函数表达式(匿名函数)声明的函数,那么name属性返回变量名。
var name = function () {};
name.name
// "name"
当然函数表达式也可以是具名函数,此时name属性返回的就是函数名,但是其实真正的函数名还是变量名,而func这个名字只在函数体内部可用。
var name = function func () {};
name.name
// "func"
name属性的一个用处,就是获取参数函数的名字。下面代码中,函数test内部通过name属性,就可以知道传入的参数是什么函数。
var myFunc = function () {};
function test(pro) {
console.log(pro.name);
};
test(myFunc) // myFunc
4.2 length属性
length属性返回函数的形参个数。
function func(a, b) {};
func.length // 2
4.3 toString()方法
函数的toString()
方法返回一个字符串,内容是函数的源码。
function func(){
console.log("hello world");
};
console.log(func.toString());
——————OUTPUT——————
function func(){
console.log("hello world");
}
当然这仅限于我们自己定义的函数,对于系统内置函数会统一返回function xxx() {[native code]}
并不会得到源码。
console.log(Array.from.toString());
——————OUTPUT——————
function from() { [native code] }
五. 内部属性
内部属性就是函数执行的时候才能确定的值。
5.1 this指向问题
5.1.1 普通函数中的this
具体函数内部的this指向跟函数的当前执行环境密切相关。 大致可分为以下三种:
① 圆括号()
调用;
② 作为引用数据类型的值(数组的元素或对象的属性值),通过数组或对象调用;
③ call()
,apply()
或者bind()
指定this作用域调用。
- 当我们如下直接用圆括号
()
调用函数时:
内部的this会指向全局对象,这个全局对象在在Node.js环境下是global
,浏览器环境下是window
。
function num(){
console.log(this);
};
num();
Node.js环境:
浏览器环境:
- 当函数作为引用数据类型的值时:
比如某个数组的元素,对象的属性值。如果此时通过数组或者对象来调用函数,那么函数的内部this就指向外部的数组或对象。
数组元素:
function num(){
console.log(this);
};
var arr = [1,2,num];
arr[2]();
对象的属性值:
function num(){
console.log(this);
};
var obj = {
name:"zevin",
num:num
};
obj.num();
3. call(),apply()或者bind()指定this作用域调用:
此时num函数的this就指向我们自定义的{name:"zevin"}
对象。
function num(){
console.log(this);
};
num.call({name:"zevin"});
5.1.2 箭头函数中的this
箭头函数本身并没有this,具体的this取决于词法作用域,由上下文确定,总是指向外部调用者。 而且箭头函数的this按照词法作用域绑定好之后,就无法再通过call()
,apply()
或者bind()
修改this的值了,这些方法传入的新作用域参数会被无视。
来比较一下普通函数和箭头函数中this指向的区别:
var out = {
name:"zevin",
getName:function(){
console.log("getName",this);
var num = function(){
console.log("num",this);
};
return num();
}
};
out.getName();
上述代码中,先是定义了一个对象out,然后out对象内部有一个getName函数,getName函数内部又定义了一个名为num的回调函数。这里num函数是普通函数表达式的写法,所以代码第八行中用圆括号的形式调用num函数,this就指向了全局对象。
可以在num函数调用之前,使用bind()方法给num函数绑定out作用域:
var out = {
name:"zevin",
getName:function(){
console.log("getName",this);
var num = function () {
console.log("num",this);
}.bind(out);
return num();
}
};
out.getName();
如果num函数换成箭头函数的形式,按照词法作用域,this会向上寻找外层调用者getName函数的this,getName函数由对象out调用,所以箭头函数的this最终就指向了对象out。
var out = {
name:"zevin",
getName:function(){
console.log("getName",this);
var num = () => {
console.log("num",this);
};
return num();
}
};
out.getName();
此时再用那三种方法修改this作用域就会无效,比如用call()
方法:
var out = {
name:"zevin",
getName:function(){
console.log("getName",this);
var num = () => {
console.log("num",this);
};
return num.call({});
}
};
out.getName();
5.2 arguments对象
5.2.1 本质——类数组对象
arguments对象是函数中的实参类数组对象。 保存着函数真正执行的时候的参数信息。
function func(a,b,c){
console.log(arguments);
};
func(1,2,3);
从上述输出结果也可以看出arguments确实是一个类数组对象。
运用一下讲了好多遍的类数组转换真数组的方法,forEach遍历输出arguments:
function func(a,b,c){
Array.prototype.slice.call(arguments).forEach(item => console.log(item));
};
func(1,2,3);
——————OUTPUT——————
1
2
3
5.2.2 callee属性
注意看上图的输出结果里,有一个callee属性,它指向当前函数,常用于递归。但在严格模式下无法使用。不建议使用。
function foo(num){
if(num === 1){
return 1;
} else {
return arguments.callee(num - 1)*num;
}
};
六. 函数作用域
6.1 定义
作用域(scope)指的是变量存在的范围。 JavaScript 有两种作用域:
① 全局作用域:变量在整个script标签或者一个单独的js文件内起作用;
② 函数作用域:变量只在函数内部起作用;
(ES6新增的块级作用域现在不讨论)
6.2 全局变量,局部变量
根据作用域的不同,变量可以分为全局变量和局部变量。
- 全局变量:
在全局作用域下的变量,在全局下都可以使用,包括函数内部。全局变量只有浏览器关闭的时候才会销毁,比较占内存资源。
var num = 1;
function func() {
console.log(num);
};
func();
// 1
- 局部变量:
在函数内部声明的变量。局部变量在程序执行完毕后就会立即销毁,比较节约内存资源。
function func() {
var num = 1;
};
console.log(num);
// Uncaught ReferenceError: num is not defined
还有三个小点要注意:
- 函数内部定义的变量,会在该作用域内覆盖同名全局变量。
var num = 1;
function func() {
var num = 2;
console.log(num);
};
func();
// 2
- 不存在块级作用域(函数除外):
对于var命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量。
if (true) {
var num = 5;
}
console.log(num);
// 5
- 函数内部的变量提升
与全局作用域一样,函数作用域内部也会产生“变量提升”现象。var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。
function fun(){
console.log(a);
if(true){
var a = 3;
return a++;
};
};
fun();
// undefined
七. 函数的运用
函数与其他数据类型(Number,String,Boolean…)地位平等,凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。
7.1 作为参数
函数作为参数的用法常见于各种API的回调函数,写法以箭头函数居多。
比如上一篇中详细介绍过的数组原型中的一些实例方法:
- Array.prototype.map()
console.log([1, 2, 3].map((elem, index) => elem * index));
——————OUTPUT——————
[0, 2, 6]
- Array.prototype.forEach()
["zevin",21,"code"].forEach(item => console.log(item));
——————OUTPUT——————
zevin
21
code
- Array.prototype.reduce()
[1, 2, 3, 4, 5].reduce(function (a, b) {
return a + b;
}, 10);
// 25
具体的API讲解上一篇有👇:
【JavaScript笔记(六)】Array全家桶(引用数据类型中的数组 / Array对象 / Array.prototype)
7.2 作为返回值
之前在Array.prototype.sort()方法的进阶实例那一篇中我们讲过一个数组自定义条件排序的问题。当时的sort()
方法中需要传入的比较器函数就是作为一个匿名函数的返回值而传入的。
全部代码摘过来如下:
var arr = [{
name:"code",age:19,grade:98
},{
name:"zevin",age:12,grade:94
},{
name:"j",age:15,grade:91
}];
function sort(arr,property){
arr.sort((function(prop){
return function(a,b){
return a[prop] > b[prop] ? -1 : a[prop] < b[prop] ? 1 : 0;
}
})(property));
};
sort(arr,"grade");
console.log(arr);
——————OUTPUT——————
[
{ name: 'code', age: 19, grade: 98 },
{ name: 'zevin', age: 12, grade: 94 },
{ name: 'j', age: 15, grade: 91 }
]