一、函数的基本概念
1.基本概念
函数实际上也是对象,每个函数都是Function类型的实例,而Function也有属性和方法,跟其他引用类型一样。
- 如果函数挂载在一个对象上,作为对象的属性,那么称它为对象的方法,当通过这个对象来调用函数时,该对象就是此次调用的执行上下文,也就是该函数的this的值
- 用于初始化一个新创建的对象的函数叫做构造函数(constructor)
- 函数即对象,因此可以把函数赋值给变量,或者作为参数传递给其他函数,因为函数就是对象,所以可以给它们设置属性和调用它们的方法
- 函数可以嵌套在其他函数中定义,从而可以访问它们被定义时所处的作用域中的任何变量。如此,函数构成了一个闭包。
2.函数的定义方式
函数声明语法
{
function sum(value1,value2){
return value1 + value2;
}
console.log(sum(100,20)); // 120
// 函数表达式
let min = function(value1,value2){
return value1 - value2;
};
console.log(min(10,6)); // 4
// 使用Function构造函数
let mul = new Function("num1","num2","return num1 + num2"); // 不推荐
}
箭头函数
ES6中新增了箭头函数( => )语法来定义函数表达式,很大程度上,箭头函数实例化的函数对象与正式的函数表达式创建的函数对象行为是相同的,箭头函数简洁的语法会更加方便实用。
{
// 大括号可以省略
let sum = (a,b) => a + b;
console.log(sum(1,2)) // 3
// 以下两种写法都有效
let double = (x) => { return 2 * x; };
let triple = x => { return 3 * x; };
// 没有参数需要括号
let getRandom = () => { return Math.random(); };
// 多个参数需要括号
let sum = (a, b) => { return a + b; };
// 无效的写法
let multiply = a, b => { return a * b; };
}
箭头函数也可以不用大括号,使用大括号就说明包含函数体,可以写多条语句,跟常规的函数一样。如果不使用大括号,那么箭头后面就只能有一行代码,省略大括号会隐式返回这行代码的值。
箭头函数虽然语法简洁,但也有很多场合不适用,比如箭头函数不能使用arguments也不能用作构造函数,并且箭头函数也没有prototype属性。
匿名函数与函数提升
{
say(); // 没有问题
function say(){
console.log("hello");
}
hello(); // 会报错,函数没有提升
// 匿名函数
let hello = function(){
console.log("Hello world");
}
console.log(hello instanceof Object); // true,函数也是对象
//
let newFun = hello;
newFun(); // 一样输出
}
可以看出使用函数表达式定义的函数不存在变量提升,因此也需要先定义后使用,也可以赋值给其他变量,这样就是同一个引用。
3.函数参数
- 特征
- ECMAScript函数既不关心传入的参数个数,也不关心这些参数的数据类型。也就是说定义函数时要接收两个参数,并不意味着调用时就传入两个参数
- 原因
- 因为ECMAScript函数的参数在内部表现为一个数组
- 函数被调用时总会接收一个数组,但函数并不关心这个数组中包含了什么
- 传进函数的每个参数值都被包含在arguments对象(类数组)中
{
function count(){
console.log(arguments.length)
}
count(); // 1
count('hello','tom'); // 2
}
{
// 箭头函数虽然不支持arguments对象,但支持剩余参数的定义方式,因此
// 也可以实现与使用arguments一样的逻辑
let getSum = (...value) => {return value.reduce((a,b) => a + b)}
console.log(getSum(1,2,3)); // 6
}
参数的默认值
在ES6以前,实现默认参数的一种常用方式就是检测某个参数是否等于undefined,如果是则意味着没有传入这个参数,那么就给它赋一个默认值。ES6就方便许多,它可以显式定义默认参数
{
// ES5当中参数默认值的实现
function fun1(name,age){
name = name ? name : "Tom";
// 也可以用短路运算
name = name || "Tom";
age = age ? age : 20;
console.log(name,age)
}
fun1(); // Tom 20
fun1("Jack"); // Jack 20
// ES6当中显式设置参数默认值
function fun2(name = "Tom",age = 18){
console.log(name,age);
}
fun2(); // Tom 18
fun2("Jack",24); // Jack 24
fun2(undefined,20); // Tom 20
}
参数传递undefined相当于没有传值,也会使用默认值
扩展参数
ES6新增了扩展操作符,使用它可以非常简洁地操作和组合集合类型数据,扩展运算符其中一个应用场景就是函数定义中的参数列表,扩展运算符既可以用于调用函数时传参,也可以用于定义函数参数。
{
// ES5
function sum(){
let res = 0;
for(let i = 0; i < arguments.length; i++){
res += arguments[i];
}
return res;
}
// ES6
function sum2(...args){
console.log(args); // [1,2,3,4,5]
return args.reduce((a,b) => a + b);
}
let arr = [1,2,3,4,5];
console.log(sum.apply(null,arr)); // 15,ES5解开数组
console.log(sum(...arr)); // 15,ES6解开数组
console.log(sum2(...arr)); // 15
}
这个函数希望把所有参数累加起来,通过迭代arguments对象,如果不使用扩展运算符,想把定义在这个函数的数组拆分,那么就需要apply方法,但是在ES6中可以通过扩展运算符极为简洁地实现这种操作。
剩余参数
剩余参数可以把后面多余的参数用扩展运算符接收起来,这样操作就方便了许多。
{
function sum1(name){
let res = 0;
for(let i = 1; i < arguments.length; i++){
res += arguments[i];
}
console.log(`${name}总分为:${res}`);
}
sum1("Tom",80,90,100) // Tom总分为:270
// ES6中的多余参数,用扩展运算符
function sum2(name,...scores){
let res = scores.reduce((a,b) => a + b);
console.log(`${name}总分为:${res}`);
}
sum2("Jack",100,95,90); // Jack总分为:285
}
剩余参数没有接收到实参值时,为空数组,且剩余参数必须位于参数列表的末尾
函数内部
函数内部存在两个特殊的对象,一个是arguments一个是this。arguments对象只有以function关键字定义的函数才会有,虽然主要用于包含函数参数,但arguments对象还有一个callee属性,是一个指向arguments对象所在函数的指针,比如一个经典的阶乘函数。
{
function factorial(num){
if(num <= 1){
return 1;
}else{
return num * factorial(num - 1);
}
}
console.log(factorial(4)) // 24
// 使用arguments.callee
function fac(num){
if(num <= 1){
return 1;
}else{
return num * arguments.callee(num - 1);
}
}
let newFac = fac;
fac = function(){
return 0;
}
console.log(fac(4)); // 0
console.log(newFac(4)); // 24
}
上面这样有一个问题就是如果函数名发生了改变那么就会出现问题,因此为了解决紧密耦合的问题,使用arguments.callee就可以让函数与函数名松耦合。
另一个特殊的对象是this,它在标准函数和箭头函数中有不同的行为。在标准函数中,this引用的是把函数当成方法调用的上下文对象,这时候通常称为this值(全局中调用函数时,this为window)
在箭头函数中,this引用的是定义箭头函数的上下文。
{
// 标准函数
window.color = "red";
let obj = {color:"blue"};
function show(){
console.log(this.color);
}
show(); // red
obj.show = show;
obj.show(); // blue
}
{
// 箭头函数
window.color = "red";
let obj = {color:"blue"};
let show = () => console.log(this.color);
show(); // red
obj.show = show;
obj.show(); // red
}
定义在全局上下文中的函数show引用了this对象,这个this具体引用哪个对象必须要函数被调用时才能确定,如果在全局中调用show方法结果会输出red,因为此时this指向window,而把show赋值给对象obj后再调用obj.show(),此时this就指向调用的这个对象,也就是obj,即this.color相当于obj.color所以输出blue。
而箭头函数中,this引用的都是window对象,因为箭头函数是在window上下文中定义的。
二、函数的使用
1.函数调用
构成函数主体的JavaScript代码在定义时并不会执行,只有调用该函数时,它才会执行
- 有4种方式来调用JavaScript函数:
- 作为函数
- 函数没有return也有返回值,为undefined
- 如果return里面没有值,也返回undefined
- 函数中的this默认是当前Window对象
- 作为方法
- 作为构造函数
- 通过它们的call()和apply()方法间接调用
- 作为函数
方法调用
-
一般情况下,与普通函数的使用方式一致,方法是属于某个特定对象才能调用的函数。
-
方法调用与函数调用有一个重要的区别就是调用上下文
- 属性访问表达式由两部分组成:一个对象(o)和属性名 (m)。在像这样的方法调用表达式里,对象o成为调用上下文,函数体可以使用关键字this引用该对象。
{
let calculator = {
oper1: 10,
oper2: 20,
add: function () {
this.result = this.oper1 + this.oper2;
},
};
calculator.add();
console.log(calculator.result); // 30
}
关键字this没有作用域的限制,嵌套的函数不会从调用它的函数中继承this,调用函数fn时里面嵌套的函数ff不会继承此时的this,因此需要定义一个变量来保存当前的执行上下文
{
let obj = {
x: 10,
fn: function () {
this.x++;
console.log(this.x); // 11
function ff() {
console.log(this.x); // undefined,此时的this是全局对象window
}
ff();
},
};
obj.fn();
}
{
// 改进后的
let obj = {
x: 10,
fn: function () {
this.x++;
let self = this; // 把当前对象赋给self,就保存了执行上下文
function ff() {
console.log(self.x);
}
ff();
},
};
obj.fn();
}
箭头函数带来的this变化
{
let person = {
name:"Tom",
age:20,
arr:[1,2,3,4,5];
show(){
console.log(this); // 此时的this就是当前对象
return this.arr.map((value) => {
console.log(this.name); // Tom
return value * 2;
})
}
}
console.log(person.show()); // [ 2, 4, 6, 8, 10 ]
}
箭头函数里的this会继承父级作用域的this,此时就可以直接调用。
构造函数调用
- 如果函数或方法调用之前带有关键字new,它就是构造函数调用。
- 构造函数调用和普通的函数调用以及方法调用在实参处理、调用上下文和返回值方面都有不同。
- 构造函数调用创建一个新的空对象,这个对象继承自构造函数的prototype属性。
- 构造函数试图初始化这个新创建的对象,并将这个对象用做其调用上下文,因此构造函数可以使用this关键字来引用这个新创建的对象。
- 构造函数通常不使用return关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会显式返回该对象。
{
// 构造函数里的this是在调用时自动产生的
function Car(name){
this.name = name;
console.log(name); // 小鹏
console.log(this,Object.getPrototypeOf(this));
}
var car = new Car('小鹏');
}
当前的this就是这个构造函数的实例对象,而它的原型则是这个构造函数。
间接调用
使用函数对象的call()和apply()方法可以间接调用函数,第一个参数指定调用上下文(也就是函数内部的this)第二个参数给函数传递参数。
{
// 间接调用函数
let obj1 = {
x : 100,
y : 200,
show(n = 1, m = 1){
return `(${this.x * n},${this.y * m})`;
},
concat : function(){
let r = [this.x,this.y];
// console.log(r);
console.log(this.x,this.y); // 111 222
for(let a of arguments){
r.push(a);
}
return r;
}
}
let obj2 = { x : 111 , y :222};
let r1 = obj1.show.call(obj2); // 指定obj2为obj1的this
let r2 = obj1.show.call(obj2,10,100); // 第二个参数就是函数的实参
console.log(r1,r2);
let r3 = obj1.concat.call(obj2,11,22,33);
let r4 = obj1.concat.apply(obj2,[2,3,4,5]); // 第二个参数为数组
console.log(r3,r4);
}
不同之处在于apply第二个参数传递的是个数组或者arguments对象,而call要传给被调用函数的参数则是逐个传递的,也就是必须要将参数一个一个列出来。
使用call或者apply的好处是可以将任意对象设置为任意函数的作用域,这样对象可以不用关心方法。
回调函数
回调函数是指作为参数传入另一个函数,并在该外部函数内被调用,用以完成某些任务的函数。
{
let arr = [22,11,6,88,7,5];
arr.sort((a,b) => a-b); // 箭头函数的方便
arr.sort(function(a,b){
return b - a;
})
console.log(arr); // [ 88, 22, 11, 7, 6, 5 ]
let data = {
x : 10,
y : 20,
// how就是回调函数
show : function (how){
how(this.x,this.y);
}
}
data.show(function(a,b){
console.log(`(${a},${b})`);
});
data.show((a,b) =>{
console.log(`(${a}->${b})`);
})
}
2.函数的属性与方法
函数的属性
- length属性
- 在函数体里,arguments.length表示实际传入函数的参数个数
- 函数本身的length属性是只读的,它代表函数声明的实际参数的数量
- prototype属性
- 每个函数都有一个prototype属性,这个属性是指向一个对象的引用,这个对象称作原型对象
- 每一个函数都包含不同的原型对象,当将函数用做构造函数的时候,新创建的对象会从原型对象上继承属性
- 自定义属性
- 函数是一种特殊的对象,因此可以拥有属性
{
function fn(a,b){
console.log(arguments.length); // 4
console.log(fn.length); // 2
console.log(arguments.callee.length); // 2,callee指向函数本身,因此参数也是2个
}
fn(1,2,3,4);
}
{
function fx(a,b){
// 函数扩展的属性是可读可写的,
if(fx.count) fx.count++;
else fx.count = 1;
return a + b;
}
fx(1,2);fx(2,3);fx(3,4);fx(4,5);fx(5,6)
console.log(fx.count); // 记录函数调用的次数,5
}
函数的方法
- call()和apply()方法
- 通过调用方法的形式来间接调用函数
- call和apply的第一个实参是调用上下文,也就是this,在函数体内通过this来获得对它的引用
- bind()方法
- 将函数绑定至某个对象
- 挡在函数上调用bind方法并传入一个对象作为参数,这个方法将返回一个新的函数
- 调用新的函数将会把原始的函数当做对象的方法来调用
- 传入新函数的任何实参都将传入原始函数
{
let obj = {color:"red"};
function show(){
console.log(this.color);
}
show.call(obj); // 会立即执行,red
let newShow = show.bind({color:"blue"}); // 不会立即执行会返回一个新函数
newShow(); // blue
}
{
// 重复指定次数
let obj = {
x : 10,
show : function(y){
let r = '';
for(let i = 0;i < y ;i++){
r += this.x + '';
}
console.log(r);
}
}
obj.show(4); // 10101010
let ss = obj.show.bind({x : 1000}); // 返回函数引用,这时的this就是参数里的对象
ss(3); // 100010001000
}
{
let sum = function (x, y) {
return x + y;
};
let succ = sum.bind(null, 1);
console.log(succ(2));
let obj = {};
obj.supAdd = sum.bind(obj, 10);
//偏函数,固定一个函数的一个或者多个参数,返回一个新的函数,这个函数用于接受剩余的参数。
console.log(obj.supAdd(20));
}
一个关于bind的小例子:随机颜色效果
{
function Color(element) {
this.element = element;
this.colors = ['#1abc9c', '#f1c40f', '#9b5ab6', '#f39c24'];
this.run = function () {
console.log(this);
setInterval(function () {
let i = Math.floor(Math.random() * this.colors.length);
this.element.style.backgroundColor = this.colors[i];
}.bind(this), 1000) // 通过bind将this也就是当前构造函数实例传递进来,就能使用this获取了。
}
}
let obj = new Color(document.body);
obj.run();
let obj2 = new Color(document.querySelector('h1'));
obj2.run();
}