[JS] 浅谈细说 JS 函数(call,apply,重载)

摘要

  • 什么是函数?
  • 函数的声明与调用
  • 函数的重载
  • 函数的独立性

什么是函数?

引用 W3School 的原话:

函数是一组可以随时随地运行的语句。

函数是 ECMAScript 的核心。

函数是由这样的方式进行声明的:关键字 function、函数名、一组参数,以及置于括号中的待执行代码。

函数的基本语法是这样的:
function functionName(arg0, arg1, ... argN) {
  statements
}

函数的声明与调用

//声明函数的4种方法

//方法一 直接声明
function speak(word){
    console.log(word)
}

//方法二 指定一个匿名函数 将其赋给一个变量,后面可以直接通过该变量调用该函数
var speak2 = function (word) {
    console.log(word);
};//定义匿名函数需要注意最后需要加分号

//方法三 使用 Function对象 生成一个函数实例 
var speak3 = new Function("word","console.log(word);");

//方法四 使用 Function函数 返回函数实例
var speak4 = Function("word","console.log(word);");

调用函数基本方法

speak("hello world for global !");

this.speak("hello world for this !");

window.speak("hello world for window !");

调用函数的高级方法

speak.call(null,"hello world with call !");//null 代表是用全局对象 window 调用

speak.apply(null,["hello world with apply !"]);

输出结果: 图片描述

方法一:

这里大家不言而喻,简单明了,直接就是声明了一个函数,需要指出的是,默认写的函数在不依附其他对象的情况下均为全局函数,即成为 window 对象的成员,可以直接使用 window(Window 对象的实例,Window 对象实现了核心 JavaScript 所定义的所有全局属性和方法)调用,或者通过 this 调用,在这里 JS 顶层代码中 this 指向的就是 window

每当我们使用方法一声明了一个函数的时候,实际是生成了一个 Function 对象的实例.即每个函数其实质都是一个 Function 对象实例.

注意: JS 解析器每解析到一个函数的时候,都会在堆内存内划分出一块空间来存储创建该 Function 实例

方法二:

首先 JS 解析器解析到一个函数的时候,在堆内存内划分出一块空间来存储创建该 Function 实例,接着在当前栈内存 创建一个叫 speak2 的变量,这个变量有个值,这个值是一个地址,指向的是堆内存中的那个 Function 实例.实际上这就是大名鼎鼎的引用.

方法三:

与方法一和方法二不同的是,前两个方法都是声明好让解析器去解析,让解析器生成 Function 实例(就是上面声明的函数,解析器调用 Function 构造器来生成实例,这些步骤是我们看不到的),方法三是我们手动调用 Function 构造器生成 Function 实例(步骤掌握在自己的手里)

方法四:

与方法三不同的地方就是没有 new,在这里 new 与没 new 的区别就是当有 new 的时候 Function 函数充当一个构造器,new 后返回的就是实例化后得到的对象(此时Function 内部的 this指向的就是当前生成的对象),不使用 new 的话就是把 Function 函数看做一个普通函数直接调用,直接调用 Function 函数让其在内部(我们看不到)new 一个实例返回,本质是一样的.

call 方法与 apply 方法

查看EcmaScript.js 图片描述

可以看到 call 方法与 apply 方法的区别:

  • 他们的第一个参数指的是调用该方法对象
  • call 方法的第二个参数是可变数组参数,即可以传入多个参数,非传入一个数组.传入的多个参数对应的是被调用方法的各参数.
  • apply 方法的第二个参数是一个数组对象,即可以直接传入一个数组对象,数组对象的每项对应的是被调用的方法的各参数.

函数的重载

在 JS 中,并不像其他强类型语言一样可以声明重载函数,若在原先声明的函数后再声明一个不同参数数量的函数(JS是弱语言, 声明的函数不需要指明参数类型),解析器会用后面声明的函数覆盖前面声明的函数.那我们该如何实现呢.

arguments 对象

在每个函数都有一个arguments 属性,同样查看 EcmaScript.js 图片描述

当生成一个函数实例后,解析器会赋给 arguments 属性一个 Arguments 对象实例,这个实例是什么,再看 EcmaScript.js 图片描述

可以得知其为一个对象同时为数组对象的子类,故可以将其当初数组对象使用. 函数实例中的 arguments 对象(可以算是一个数组)的数组项内容便是我们在调用函数时进行传递的参数.只要我们有传参,这个属性就有数组项,否则数组长度为0,故我们可以通过 arguments.length 来查看其得到的形参的数量.

有了上面的基础便可实现重载函数 这里引用 W3School 的例子

function doAdd() {
  if(arguments.length == 1) {
    alert(arguments[0] + 5);
  } else if(arguments.length == 2) {
    alert(arguments[0] + arguments[1]);
  }
}

doAdd(10);	    //输出 "15"
doAdd(40, 20);	//输出 "60"

函数的独立性

我们都知道,每当我们声明了一个函数,其实际为一个 Function 实例,那它独立在哪呢,如何体现呢 看一下代码:

function Dog(name,age){
    this.name = name;
    this.age = age;
    this.showName = function () {
        console.log(this.name);
    }
}

function Cat(name,age){
    this.name = name;
    this.age = age;
    this.showName = function () {
        console.log(this.name);
    }
}

var dog = new Dog("wangwang",2);
var cat = new Cat("miaomiao",3);

dog.showName();
cat.showName();
dog.showName.call(cat);

输出结果为: 图片描述

解释:

每当我们在函数内使用 this 的时候,无非这几种情况:

  1. 作为构造器生成的实例对象
  2. 作为调用当前方法的对象
  3. 在 JS 顶层代码可以使用 this 代表 window 调用全局函数等

故当我们使用 call 方法调用某个对象的方法时,虽然从代码语义上看,这个所属方法是属于该对象的(showName 属于 Dog 或 Cat),但是由于函数有用其特殊的独立性即有以上几个关于 this 的特点,导致最终的结果是不同的.

当我们直接调用 dog 的 showName 方法时,showName 方法内的 this 指向的是该dog 对象(Dog 实例).

我们知道调用 call 方法时需要传入的第一个参数即为调用当前函数亦或方法的对象,此时被调用的方法的 this 指向的实际为传入的第一个参数.即当我们通过 call 调通 dog 的 showName 方法时, 传入的第一个参数是 cat 对象,代表 dog 的 showName 方法的 this 此时指向的不是 dog 是 cat.最后输出的当然是 cat 的内容。

最后指出:在对函数进行传参时,若传的是 JS 的基本类型,则为值传递,否则为引用传递(传递的是参数的地址)。


tangzixiang