js之函数基础知识一

208 阅读14分钟

    计算机程序中通常包含由多条语句组成的逻辑单元,在JavaScript中这些单元被称为函数,函数其实就是一段可以重复调用的JS代码块,它只定义一次,但可以多次调用执行。JavaScript函数是参数化的,函数定义会包括实参和形参。形参相当于函数中定义的变量,实参是在运行函数调用时传入的参数。

一、函数定义

函数使用关键字function来定义,定义函数有如下几种方法:

1、函数声明

使用function关键字,后跟一组参数及函数体。

  • funcname是要声明的函数名称。函数名称是函数声明语句必需的部分。函数名是指向函数的指针。它们跟其它包含对象指针的变量具有相同行为。一个函数可以有多个名称。ECMAScript6的所有函数对象都会暴露name只读属性,它里面包含关于函数的信息,多数情况下这个属性中保存的就是一个函数标识符,或者是字符串化的变量名,即使函数没有名称也会如实显示成空字符串,如果是使用Function构造函数创建的,它会标识成‘amonymous’。使用不带括号的函数名可以访问到函数指针,但是不会执行函数。

  • 圆括号其中可以包含由0个或者是多个用逗号隔开的标识符,这些标识符是函数参数名称,它们就像是函数体中的局部变量一样。

  • 花括号,包偏一条或者是多条javascript语句。这些语句构成函数体,一旦调用函数,就会执行这些语句。

  • 变量的重复声明是无用的,但是函数重复声明会覆盖前面的声明(无论是变量还是函数声明)

  //语法:
function funcname(a,b) {
    return a + b;
} 
//注意:函数最后没有加分号

2、函数表达式

以表达式的方式来定义函数,函数名称是可选的。匿名函数是function关键字后面没有标识符的函数。通常而言,以表达式方式定义函数时都是不需要名称,这样做让代码更加简洁。特别适合用来定义那些只会使用一次的函数,末尾有分号。

let funcname(a,b){
    return a + b;
}

3、Function构造函数

Function构造函数接收任意数据参数,但最后一个参数始终都被看成是函数体,前面参数则枚举出了新函数的参数。Function构造函数无法指定函数名称,它创建了一个匿名函数。不推荐使用,因为它会导致两次代码解析,第一次将它当作常规代码,第二次是解释传给构造函数的字符串。

var functionName = new function(){
    statement;
}

4、箭头函数

ECMAScript6新增加了胖箭头(=>)语法定义函数表达式,任何可以使用函数表达式的地方都可以使用箭头函数。箭头函数语法简洁,如果只有一个参数,可以不用括号 。只有在没有参数或者是多个参数才需要使用括号,如下所示。

// 没有参数
let arrowFuncname = () => {
    statements;
}
// 只有一个参数
let arrowFuncname = a => {
    statements;
} 
//等价于下面这种
let arrowFuncname = (a) => {
    statements;
}
// 多个参数
let arrowFuncname = (a , b) =>{
    statements;
}
// 当箭头函数后面只有一行代码,可以不用大括号
let arrowFuncname = a => 3 * a;
// 当箭头函数后面只有一行代码,代码里面有return时依然需要大括号
let arrowFuncname = a => {return 3 * a}

需要注意箭头函数只能先定义再使用,它里面没有this对象,函数在哪里定义,this就指向谁(箭头函数会继承外层函数调用的this)。没有this也就不是构造函数,不能使用new去调用,箭头函数里没有arguments对象,此外箭头函数也没有prototype属性。

二、函数参数

1、理解参数

从函数外向函数内传入数据,在函数内可以接受到这些数据且能够使用它,这些数据就叫做参数。由于ECMAScript函数的参数在内部表现为一个数组。函数被调用时接收到的是一个数组,但函数并不关心这个数组中包含什么内容,是什么类型,如果有必要甚至可以不传参,它与大多数其它语言参数是不一样的。事实上,在使用function关键字定义非箭头函数时可以在函数内部访问arguments对象,从中取得传进来的每个参数值。

参数放在小括号内,可以放0个或者是多个,用逗号隔开,它分为形参和实参。形参是形式上的参数,在函数声明的小括号里,形参实际上是由实参来决定的。在非严格模式下,函数中可以出现同名的形参且只能访问最后出现的该名称形参。但在严格模式下会报错。

实参是实际上的参数,放在了函数调用的小括号里,实参须与形参一一对应,在调用函数时,要把实参传够数量,如果有一个实参没有传,那它就是undefined。常常用逻辑或运算符给省略的参数设置一个合理默认值。

arguments对象它代表了所有实参集合是一个类数组。这个集合中的每个数据都有一个自己对应的下标,集合中还有一个length属性,代表了实参个数arugments对象的长度是根据传入的参数个数而非定义函数时给出的形参个数来确定的。它只能在函数内使用,如果函数外使用会报错。arguments对象中的值永远与对应命名参数的值保持同步,arguments对象与命名参数并不是访问相同内存空间,它们内存空间独立,但是值相等。

function fn(num1,num2){
    arguments[1] = 10;
    console.log(arguments[0] + num2); // 30
}
fn(20,40);

如果函数是使用箭头函数语法定义,那么传给函数参数不能使用arguments关键字访问,而只能通过定义的形参访问。可以通过外部的包装函数把它的arguments提供给箭头函数。

let arrowFn = () => {
    console.log(arguments[0]);
}
arrowFn(5); // Uncaught ReferenceError: arguments is not defined

function wrapperFn(){
    let arrowFn = () => {
        console.log(arguments[0]); //10
    }
    arrowFn();
}
wrapperFn(10);

ECMAScript中所有函数的参数都是按值传递。换句话说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。它不能按引用传递参数,如果把对象作为参数传递,那么传递的值就是这个对象的引用了。

//传递基本类型时,被传递的值会被赋给一个局部变量
function add(num){
    num += 10;
    return num;
}
let count = 10;
let re = add(count);
console.log(re,count);  // 20 10 

// 向参数传递引用类型时:
function fn(obj){
    obj.age = 20;
}
let person = new Object();
fn(person);
console.log(person.age);  // 20
//在函数外部创建了一个person对象,用fn进行调用,person的值复制一份后传递给参数obj,使得obj的引用与person指向同一对象

2、默认参数值

在ECMAScript5.1及之前我们可以检测某个参数是否等于undefined来实现默认参数,如果是则没有传递这个参数,可以为其赋值。

function fn(age){
    age = (typeof age !== 'undefined') ? age : 18;
    return `${age}`;
}
console.log(fn()); //18

但在ECMAScript6后可以显示的定义默认参数,如下所示:

function fn(age = 18){
    return `${age}`;
}
console.log(fn()); //18

函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值,并且函数默认值只有在调用函数但没有传入相应参数时才会被调用。 箭头函数同样也可以像上面那样使用默认参数,是不过在只有一个参数时,要使用括号,不能省略。

let fn = (age = 18) => `${age}`;
console.log(fn()); //18

在求值默认参数时可以定义对象,也可以动态调用函数,所以函数参数肯定是在某个作用域中求值的。当我们给多个参数定义默认值实际上跟使用let关键字顺序声明变量一样。参数是按顺序被始化,所以后定义默认值的参数可以引用前面定义的参数,但是前面定义的参数不能引用后面定义的。参数初始化顺序遵循暂时性死区规则。而且参数也存在于自己的作用域中,它们不能引用函数体的作用域。

    function fn(age = 18) {
        return `${age}`
    }
    console.log(fn()); //18

    function fn2() {
        let age = 18;
        return `${age}`
    }
    console.log(fn2()); //18

    function fn3(age1 = 18, secondAge = age1) {
        return 'age1:' + `${age1}` + 'age2' + `${secondAge}`;
    }
    console.log(fn3()); //age1:18age218

    // function fn4(age1 = secondAge, secondAge = 18) {
    //     return 'age1:' + `${age1}` + 'age2' + `${secondAge}`;
    // }
    // console.log(fn4()); //Uncaught ReferenceError: Cannot access 'secondAge' before initialization

    function fn5(name = defaultName) {
        let defaultName = 'lily';
        return `${defaultName} ${name}`;
    }
    console.log(fn5()); //Uncaught ReferenceError: defaultName is not defined、

三、函数内部

1、arguments

前面也说过arguments,它是一个类数组对象,包含调用函数时传入的所有参数,这个对象只有以function关键字定义函数时才会有,箭头函数是没有arguments对象的。arguments对象有一个callee属性,是一个指向arguments对象所在函数的指针。

2、this

this在函数中的指向一直是一个比较重要的问题。this是执行主体它与执行上下文有着本质区别。函数中this可以分为以下几咱情况:

2.1 事件绑定

给元素某个事件行为绑定方法,当事件行为触发,方法执行,方法中的this是当前元素本身。但在ie6-8中基于attachEvent方法实现的事件绑定,事件触发,方法中的this指向window。

<input type="button" value="click me" id='click'>
    <script>
        document.getElementById('click').onclick = function () {
            // 事件触发,方法执行,方法中的this是元素本身
            console.log(this.value); 
        }
    </script>

2.2 普通方法执行

普通方法执行只需要看函数执行时方法名前面是否有“点”,有“点”,点前面是谁this就是谁,没有点在非严格模式下this指向window,严格模式下指向undefined。

//普通方法执行(包括自执行函数,普通函数执行,对象成员访问)
//自执行函数,也是看函数执行时前面是否有点,函数中的this和函数在哪里定义,在哪里执行无关
(function () {
    console.log(this); //=>window
})();

let obj = {
    fn: (function () {
        console.log(this); //=>window
        return function () {}
    })() //把自执行函数执行的返回值赋值给obj.fn
};

2.3 构造函数

构造函数体中的this是当前类的实例。

function Func() {
   this.name = "F";
   //=>构造函数体中的this在“构造函数执行”模式下,是当前类的一个实例,并且this.XXX=XXX是给当前实例设置私有属性
   console.log(this);
}
Func.prototype.getNum = function getNum() {
   // 原型上的方法中的this不一定都是实例,主要看执行的时候,“点”前面的内容
   console.log(this);
};
let f = new Func;
f.getNum();
f.__proto__.getNum();
Func.prototype.getNum();

2.4 es6箭头函数

箭头函数中没有自己的this,它的this是继承上下文中的this。

let obj = {
    func: function () {
         console.log(this);
    },
    sum: () => {
     console.log(this);
    }
};
obj.func();    //this:obj
obj.sum();     //this是所在上下文中的this:window
obj.sum.call(obj); //箭头函数是没有this,哪怕强制修改也没用 this:window

2.5 call/apply/bind方式

Function.prototype内部有call/bind/apply三种方法手动改变函数中this的指向。

  • call: function.call(thisArg, arg1, arg2, ...)

function作为Function的一个实例,可以基于__proto__找到Function.prototype的call方法,并且把找到的call方法执行;

在call方法执行时,会把function执行,把函数中的this指向为thisArg,并且把arg1,arg2,...等参数值分别传递给函数。

  • apply:func.apply(thisArg, [argsArray]);

和call作用一样,只不过传递给函数的参数需要以数组形式进行传递。

  • bind:function.bind(thisArg[, arg1[, arg2[, ...]]])

语法上和call类似,但作用和call/apply都不太一样;call/apply都是把当前函数立即执行,并且改变函数中的this指向,而bind是一个预处理思想,基于bind只是预先把函数中的this指向thisArg,把arg1这些参数值预先存储起来,但是此时函数并没有被执行。

在下面代码中我们可以看到,call和apply的唯一区别在于传递参数的形式不一样,apply以数组形式传递参数。

call方法的第一个参数,如果不传或者是传递的为null/undefined,在非严格模式下,this指向window,严格模式下指向传递的值。

let obj = {
    name: 'obj'
};
function func(x, y) {
    console.log(this, x, y);
}

func.call(obj, 11, 12);//obj 11 12
func.apply(obj, [11, 12]); //obj 11 12
func.call();//window undefined undefined
func.call(null);//window undefined undefined
func.call(undefined);//window undefined undefined
func.call(11);//Number undefined unefined

3、new.target

ECMAScript中的函数有两重身份,它即可以作为构造函数实例化的一个新对象,也可以作为普通函数被调用。ECMAScript中新增了检测函数是否使用new关键字调用的new.target属性,如果函数是正常调用,则new.target值是undefined,如果是使用new关键字调用,则new.target将引用被调用的构造函数。

<input type="button" value="click me" id='click'>
<script>
    function Fn() {
        if (!new.target) {
            throw 'fn must be instantiated using "new"';
        }
        console.log('fn instantiated using "new"');
    }
    new Fn(); //fn instantiated using "new"
    Fn(); //Uncaught fn must be instantiated using "new"
</script>

四、函数调用

只有函数被调用时才会执行。一般来说,函数调用分为以下4种:

1、函数调用模式

调用方法:函数名(参数)。对于普通函数调用来说,函数的返回值就是调用表达式的值。

使用函数调用模式调用函数时,在非严格模式下,this指向window,在严格模式下this指向undefined。

let add = function (a, b) {
    'use strict';
    console.log(this); //undefined
    // alert(this);//Window
    return a + b;
}
let sum = add(3, 4);
console.log(sum);//7;

2、方法调用模式

调用方法:对象.方法(参数)。当函数被保存为对象的一个属性时,我们称它为方法。当方法被调用时,this被绑定到该对象。如果调用表达式包含一个提取属性的动作,那么它就是被当做一个方法来调用。(简而言之方法调用就是用对象的方法调用,所以说方法对象一定要有宿主对象)。

方法可以使用this访问自己所属的对象,所以它能从对象中取值或对对象进行修改,this到对象的绑定发生在调用时。通过this可取得它们所属对象上下文的方法称之为公共方法。

任何函数只要作为方法调用都会传入一个隐匿的实参,这个实参是一个对象,方法调用的母体就是这个对象。

和变量不同,关键字this没有作用域限制,嵌套函数不会从调用它的函数中继承this,如果嵌套函数作为方法调用,其this的指向是调用它的对象,如果嵌套函数作为函数调用,其this的指向就是全局对象(严格模式下就是undefined)。

所以总结来看:

  • 方法调用模式不是独立的,需要宿主,而函数调用模式是独立的
  • 方法调用模式方法:obj.fn(),函数调用模式方法:fn();
  • 方法调用模式中,this指宿主,而函数调用模式中this指全局对象
let myObject = {
    value: 0,
    fn1: function () {
        console.log(this);//myObject
        return this;
    },
    fn2: function () {
        console.log(this);//myObject
        this.value = 1;
    },
    fn3: function () {
        function n() {
            console.log(this);//window
            return this;
        }
        return n();
    }
}
console.log(myObject.fn1().value);//0
myObject.fn2();
console.log(myObject.fn1().value);//1
console.log(myObject.fn3());//window

3、构造器调用模式

如果函数或者方法调用之前带有关键字new,那么它就构成构造函数调用。new是一个运算符,专门用来申请创建对象,创建出来的对象传递给构造函数的this,然后利用构造函数对其初始化。

执行步骤:var p = new Person();

如果构造函数调用在圆括号内包含一组实参列表,先计算这些实参表达式,然后传入函数内。如果构造函数没有形参,js构造函数调用的语法是允许省略实参列表和圆括号的。凡是没有形参的构造函数调用都可以省略圆括号。

构造函数通常不使用return关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会显示返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值。

let o = {
    m: function () {
        return this;
    }
}
let obj = new o.m();
console.log(obj, obj === o);//{} false
console.log(obj.constructor === o.m);//true

4、间接调用模式

js中函数也是对象,函数对象也可以包含方法,call(),apply()和bind()方法可以用来间接调用函数。

let obj = {};//定义一个空对象
function fn(x, y) {
    console.log(this);//obj
    console.log(x, y);//1,2
}
fn.apply(obj, [1, 2]);
fn.call(obj, 1, 2);//直接调用
let b = fn.bind(obj);//bind()不能调用函数
b();//此时才调用

五、return返回值

函数中的return语句用来返回函数调用后的返回值,阻止函数继续运行。它经常作为函数的最后一条出现,当return被执行时,函数立即返回不再执行余下语句。如果函数里没有return,那这个函数的返回值结果就是undefined。它只能出现在函数体内,如是不是那就会报错。一个函数中可以有多个return语句。

function fn(a, b) {
    return a + b;
    alert(1); //不会执行,因为放在了return后面
}
console.log(fn(2, 3));  //5
function fn1(a, b) {
    let c = a + b;
}
console.log(fn1(4, 5));//undefined 函数里没有return