不知道你有无听说过“函数式编程”。函数是 JS 中最有特点的部分,因为在 JS 中函数是一个对象,每一个对象都是一个 Function 类型的实例。函数名其实就是一个指向该实例的指针。
所有函数都会暴露一个只读的name 属性(显示赋值无效),它包含了函数的信息。一般保存的是一个字符串化的函数名:
function test() {
console.log(1)
}
test.name = "fn";
console.log(test.name)//test
这是 JS 中最常用的定义函数的方式。这里函数末尾是没有分号的,
function add(a, b) {
return a + b;
}
这里函数末尾是有分号的,相当于是将函数赋值给一个变量。
let sum = function (a, b) {
return a + b;
};
函数表达式和一般的函数声明唯一的区别就是,函数声明会有函数声明提升,但是函数表达式没有,这也就意味着函数声明的的函数可以先使用在定义,而函数表达式不行。
这段代码会被解释两次,第一次将它当做常规 ECMAscript 代码执行,第二次是解释传给构造函数的字符串。所以不建议使用这种方式定义函数。
let add = new Function('a', 'b', 'return a + b');
箭头函数的特点
let add = () => {
return a + b;
}
如果只有一个参数可以省略小括号;如果函数体只有一句 return 语句,那么大括号也可以省略。
let add1 = a => {
return a + 10;
}
let add2 = a => return a + 10;
JS 中函数传入的参数个数和类型(多余的参数会自动忽略),这就意味着 JS 中函数没有重载机制。
function add(a, b) {
console.log(a + b);
}
add(1, 2);//3
add('No ', 1);//No 1
add(1);//NaN
add('No ', 1, 2);//No 1
虽然不能重载,但是函数可以通过类数组对象 arguments 取得传入的参数值,arguments.length 就是传入的参数个数,实现不同的功能,如下:
function add() {
if (arguments.length == 1) {
console.log(arguments[0]);
} else if (arguments.length == 2) {
console.log(arguments[0] + arguments[1]);
} else {
console.log(arguments[0] + arguments[1] + arguments[2]);
}
}
add(1, 2);//3
add('No ', 1);//No 1
add(1);//1
add('No ', 1, 2);//No 12
arguments 对象可以和命名参数一起使用。并且 arguments 对象的值会自动同步到对应的命名参数,但是修改命名参数的值不会影响 arguments 对象中对应的值。
function add(a) {
console.log(a + arguments[1]);
}
add(1, 2);//3
add('No ', 1);//No 1
function add(a) {
arguments[0] = 2;
console.log(a + arguments[1]);
}
add(1, 2);//4
只传入一个参数,设置 arguments[1] 的值,这种情况下并不会同步到第二个命名参数上,arguments 对象的长度是根据实际传入的参数个数变化的,跟定义了几个命名参数没有关系。
function add(a, b) {
arguments[1] = 2;
console.log(a + b);
}
add(1);//NaN
因为箭头函数没 arguments ,所以传给箭头函数的参数只能通过命名参数访问。
在定义函数的时候,给参数赋值,那么这个值就是参数的默认值。这个默认值可以使调用函数的返回值,这时只有在函数被调用时才会被求值。如下面的例子中,只有 add 函数被调用时才会调用 fn。
function add(a, b = 3) {
console.log(a + b);
}
add(1);//4
function fn() {
return 3;
}
function add1(a, b = fn()) {
console.log(a + b);
}
add1(1);//4
默认参数是按顺序初始化的,因此后边的参数的默认值可以使前边的参数。但是前面的参数默认值不能是后边的参数值。并且默认值不能是函数体内的变量值。
function add(a, b = a) {
console.log(a + b);
}
add(1);//4
函数参数可以使用扩展操作符,它将可迭代对象的每一个元素都作为一个参数传给函数。也可以使用扩展操作符将传入的多个独立参数组合为一个数组。
const arr = [1, 2]
function add() {
let sum = 0;
for (const item of arguments) {
sum += item;
}
console.log(sum);
}
add(...arr);//3
function add(...value) {
let sum = 0;
for (const item of value) {
sum += item;
}
console.log(sum);
}
add(1, 2);//3
谁调用就指向谁,全局函数调用就是向 window,对象调用函数,this就指向该对象实例,箭头函数的this指向定义时的上下文。
function test() {
console.log("函数声明this" + this);
}
const fn = () => {
console.log("箭头函数this" + this);
}
const obj = {
test,
fn
}
test();
obj.test();
fn();
obj.fn();

arguments 类数组对象还有一个 callee 属性,它指向 arguments 对象所在的函数。
const arr = [1, 2]
function add() {
console.log(arguments.callee);
}
add(...arr);//3

函数对象上也会有caller属性,这个属性指向调用当前函数的函数,严格模式下使用会报错。
function test() {
console.log(test.caller);
}
const fn = () => {
test();
}
fn();

由于 JS 中函数既可以作为普通函数被调用,也可以作为构造函数被调用,所以为了区分这两种情况,就有了 new.target 属性,如果是普通调用,那么它的值就是undefined,否则就为该构造函数。
function fn() {
if (new.target) {
console.log('构造函数');
} else {
console.log('函数调用');
}
}
fn();//函数调用
new fn();//构造函数
保存的是函数定的命名参数的个数,就是形参的个数。
function fn(a) { }
function add(a, b) { }
console.log(fn.length)//1
console.log(add.length)//2
属性保存引用类型所有实例方法的地方(原型对象)。
function fn(a) { }
console.log(fn.prototype)

ES6 新增了一项内存优化机制,来看下面的例子:
function fn(...value) {
add(value);
}
function add(value) {
let sum = 0;
for (const item of value) {
sum += item;
}
console.log(sum);
}
fn(1, 2);//3
ES6 之前执行逻辑:
在 ES6 做了尾调用优化之后执行逻辑:
由例子可以看到,未做优化前每调用一次嵌套函数执行栈中就会增加一个上函数下文,做了尾调用优化后,执行栈中就减少了上下文的数量。也就减少了内存的消耗。
因为尾调用优化本质上就是确定外部上下文没有必要存在了。所以需要满足如下条件:
function fn() {
let foo = 'bar';
function fn1() { return foo; }
return fn1();
}
JS 函数部分基础知识就这么多了,我是孤城浪人,一名正在前端路上摸爬滚打的菜鸟,欢迎你的关注。