前台入门12-JavaScript语法之函数

作者 : 开心源码 本文共10282个字,预计阅读时间需要26分钟 发布时间: 2022-05-12 共105人阅读

公告

本系列文章内容一律梳理自以下几个来源:

  • 《JavaScript权威指南》
  • MDN web docs
  • Github:smyhvae/web
  • Github:goddyZhao/Translation/JavaScript

作为一个前台小白,入门跟着这几个来源学习,感谢作者的分享,在其基础上,通过自己的了解,梳理出的知识点,或者许有遗漏,或者许有些了解是错误的,如有发现,欢迎指点下。

PS:梳理的内容以《JavaScript权威指南》这本书中的内容为主,因而接下去跟 JavaScript 语法相关的系列文章基本只详情 ES5 标准规范的内容、ES6 等这系列梳理完再单独来讲讲。

正文-函数

在 JavaScript 里用 function 公告的就是函数,函数本质上也是一个对象,不同的函数调用方式有着不同的用途,下面就来讲讲函数。

函数有少量相关术语: function 关键字、函数名、函数体、形参、实参、构造函数;

其中,大部分的术语用 Java 的基础来了解就可,就构造函数需要注意一下,跟 Java 里不大一样。在 JavaScript 中,所有的函数,只需它和 new 关键字一起使用的,此时,即可称这个函数为构造函数。

由于,为了能够在程序中辨别普通函数和构造函数,书中建议需要有一种良好的编程规范,比方构造函数首字母都用大写,普通函数或者方法的首字母小写,以人为的手段来良好的区分它们。这是由于,通常用来当做构造函数就很少会再以普通函数形式使用它。

函数定义

函数的定义大体上包含以下几部分:function 关键字、函数对象的变量标识符、形参列表、函数体、返回语句。

假如函数没有 return 语句,则函数返回的是 undefined。

函数定义有三种方式:

函数公告式

add(1,2); //因为函数公告被提前了,不会出错function add(x, y) {    //函数体}

add 是函数名,因为 JavaScript 有公告提前的解决,以这种方式定义的函数,可以在它之前调用。

函数定义表达式

var add = function (x, y) {    //函数体}

这种方式其实是定义了匿名函数,而后将函数对象赋值给 add 变量,JavaScript 的公告提前解决只将 add 变量的公告提前,赋值操作仍在原位置,因而这种方式的公告,函数的调用需要在公告之后才不会报错。

注意,即便 function 后跟随了一个函数名,不使用匿名函数方式,但在外部依旧只能使用 add 来调用函数,无法通过函数名,这是因为 JavaScript 中作用域机制原理导致,在后续讲作用域时会来讲讲。

Function

var add = new Function("x", "y", "return x*y;");//基本等价于var add = function (x, y) {    return x*y;}

Function 构造函数接收不定数量的参数,最后一个参数表示函数体,前面的都作为函数参数解决。

注意:以这种方式公告的函数作用域是全局作用域,即便这句代码是放在某个函数内部,相当于全局作用域下执行 eval(),而且对性能有所影响,不建议使用这种方式。

函数调用

跟 Java 不一样的地方,在 JavaScript 中函数也是对象,既然是对象,那么对于函数对象这个变量是可以随便使用的,比方作为赋值语句的右值,作为参数等。

当被作为函数对象看待时,函数体的语句代码并不会被执行,只有明确是函数调用时,才会触发函数体内的语句代码的执行。

例如:

var a = function () {    return 2;}var b = a;    //将函数对象a的引用赋值给bvar c = a();  //调用a函数,并将返回值赋值给c

函数的调用可分为四种场景:

  • 作为普通函数被调用
  • 作为对象的方法被调用
  • 作为构造函数被调用
  • 通过 call() 或者 apply() 间接的调用

不同场景的调用所造成的区别就是,函数调用时的上下文(this)区别、作用域链的区别;

作为普通函数被调用

通常来说,直接使用函数名+() 的形式调用,即可以认为这是作为函数被调用。假如有借助 bind() 时会是个例外的场景,但一般都可以这么了解。

假如只是单纯作为函数被调用,那么通常是不用去考虑它的上下文、它的this值,由于这个时候,函数的用途倾向于解决少量通用的工作,而不是特定对象的特定行为,所以需要使用 this 的场景不多。

普通函数被调用时的作用域链的影响因素取决于这个函数被定义的位置,作用域链是给变量的作用域使用的,变量的作用域分两种:全局变量、函数内变量,作用域链决定着函数内的变量取值来源于哪里;

普通函数被调用时的上下文在非严格模式下,一直都是全局对象,不论这个函数是在嵌套函数内被调用或者定义还是在全局内被定义或者调用。但在严格模式下,上下文是 undefined。

作为对象的方法被调用

普通的函数假如挂载在某个对象内,作为对象的属性存在时,此时可从对象角度称这个函数为对象的方法,而通过对象的引用访问这个函数类型的属性并调用它时,此时称为方法调用。

方法调用的上下文(this)会指向挂载的这个对象,作用域链依旧是按照函数定义的位置生成。

var a = {    b: 1,    c: function () {        return this.b;    }}a.c();  //输出1,a.c() 称为对象的方法调用a["c"](); //对象的属性也可通过[]访问,此种写法也是调用对象a的c方法

只有明确通过对象的引用访问函数类型的属性并调用它的行为才称为对象的方法调用,并不是函数挂载在对象上,它的调用就是方法调用,需要注意下这点,看个例子:

var d = a.c;d();  //将对象的c函数引用赋值给d,调用d,此时d()是普调的函数调用,上下文在非严格模式下是全局对象,不是对象a

下面通过一个例子来说明普通函数调用和对象的方法调用:

var a = 0;var o = {    a:1,    m: function () {        console.log(this.a);         f();  //f() 是函数调用        function f() {            console.log(this.a);        }    }}o.m(); //输出 1 0,由于0.m()是方法调用,m中的this指向对象o,所以输出

输出1 0,由于 o.m() 是方法调用,m 中的 this 指向对象 o,所以输出的 a 是对象 o 中 a 属性的值 1;

而 m 中尽管内嵌了一个函数 f,它并不挂载在哪个对象像,f() 是对函数 f 的调用,那么它的上下文 this 指向的是全局对象。

所以,对于函数的不同场景的调用,重要的区别就是上下文。

作为构造函数被调用

普通函数挂载在对象中,通过对象来调用称方法;而当普通函数结合 new 关键字一起使用时,被称为构造函数。

构造函数的场景跟其余场景比较不同,区别也比较大少量,除了调用上下文的区别外,在实参解决、返回值方面都有不同。

假如不需要给构造函数传入参数,是可以省略圆括号的,如:

var o = new Object();var o = new Object;

对于方法调用或者函数调用圆括号是不能省略的,一旦省略,就只会将它们当做对象解决,并不会调用函数。

构造函数调用时,是会创立一个新的空对象,继承自构造函数的 prototype 属性,并且这个新创立的空对象会作为构造函数的上下文,如:

var o = {    a:1,    f:function () {        console.log(this.a);    }}o.f();  //输出1new o.f();  //输出undefined

假如是 o.f() 时,此时是方法调用,输出 1;

而假如是 new o.f() 时,此时 f 被当做构造函数解决,this 指向的是新创立的空对象,空对象没有 a 这个属性,所以输出 undefined。

构造函数通常不使用 return 语句,默认会创立继承自构造函数 prototype 的新对象返回。但假如硬要使用 return 语句时,假如 return 的是个对象类型,那么会覆盖掉构造函数创立的新对象返回,假如 return 的是原始值时,return 语句无效。

var o = {    f:function () {        return [];    }}var b = new o.f();  //b是[] 空数组对象,而不是f

间接调用

call()apply() 是 Function.prototype 提供的函数,所有的函数对象都继承自 Function.prototype,所有都可以使用这两个函数。它们的作用是可以间接的调用此函数。

什么意思,也就是说,任何函数可以作为任何对象的方法来调用,即便这个函数并不是那个对象的方法。

var o = {    a:1,    f:function () {        console.log(this.a);    }}o.f(); //输出1var o1 = {    a:2}o.f.call(o1); //输出2

函数 f 本来是对象 o 的方法,但可以通过 call 来间接让函数 f 作为其余对象如 o1 的方法调用。

所以间接调用本质上也还是对象的方法调用。应用场景可以是子类用来调用父类的方法。

那么函数的调用其实按场景来分可以分为三类:作为普通函数被调用,作为对象方法被调用,作为构造函数被调用。

普通函数和对象方法这两种区别在于上下文不一样,而构造函数与前两者区别更多,在参数解决、上下文、返回值上都有所区别。

假如硬要类比于 Java 的函数方面,我觉得可以这么类比:

  •      普通函数的调用 VS 公开权限的静态方法
  •      对象方法的调用 VS 对象的公开权限的方法
  •      构造函数的调用 VS 构造函数的调用

左边 JavaScript,右边 Java,具体实现细节很多不一样,但大体上可以这么类比了解。

函数参数

参数分形参和实参两个概念,形参是定义时指定的参数列表,期望调用时函数所需传入的参数,实参是实际调用时传入的参数列表。

在 JavaScript 中,不存在 Java 里方法重载的场景,由于 JavaScript 不限制参数的个数,假如实参比形参多,多的省略,假如实参比形参少,少的参数值就是 undefined。

这种特性让函数的用法变得很灵活,调用过程中,根据需要传入所需的参数个数。但同样的,也带来少量问题,比方调用时没有按照形参规定的参数列表来传入,那么函数体内部就要自己做相对应的解决,防止程序因参数问题而异常。

同样需要解决的还有参数的类型,由于 JavaScript 是弱类型语言,函数定义时无需指定参数类型,但在函数体内部解决时,假如所期望的参数类型与传入的不一致,比方希望数组,传入的是字符串,这种类型不一致的场景JavaScript尽管会自动根据类型转换规则进行转换,但有时转换结果也不是我们所期望的。

所以,有些时候,函数体内部除了要解决形参个数和实参个数不匹配的场景外,最好也需要解决参数的类型检查,来避免因类型错误而导致的程序异常。

arguments

函数也是个对象,当定义了一个函数后,它继承自 Function.prototype 原型,在这个原型中定义了所有函数共有的基础方法和属性,其中一个属性就是 arguments。

这个属性是一个类数组对象,按数组序号顺序存储着实参列表,所以在函数内使用参数时,除了可以使用形参定义的变量,也可以使用 arguments。

var a = function (x, y) {    //x 和 arguments[0]等效    console.log(x);    console.log(arguments[0]);    console.log(arguments[1]);    console.log(arguments[2]);}a(5); //输出 5 5 undefined undefineda(5, 4, 3); //输出 5 5 4 3

所以,尽管函数定义时公告了三个参数,但使用的时候,并不肯定需要传入三个,当传入的实参个数少于定义的形参个数时,相应形参变量对应的值为 undefined;

相反,当传入实参个数超过形参个数时,可用 arguments 来获得这些参数使用。

参数解决

由于函数不对参数个数、类型做限制,使用时可以传入任意数量的任意类型的实参,所以在函数内部通常需要做少量解决,大体上从三个方面进行考虑:

  • 形参个数与实参个数不符时解决
  • 参数默认值解决
  • 参数类型解决

下面分别来讲讲:

形参个数与实参个数不符时解决

通过 argument.length 可以获取实参的个数,通过函数属性 length 可以获取到形参个数,知道形参个数和实参个数即可以做少量解决。如:

var a = function (x) {    if (arguments.length !== arguments.callee.length) {        throw Error("...");    }}

上述代码表示当传入的实参个数不等于形参个数时,抛异常。

形参个数用:arguments.callee.length 获取,callee 是一个指向函数本身对象的引用。这里不能直接用 length 或者 this.length,由于在函数调用一节说过,当以不同场景使用函数时,上下文 this 的值是不同的,不肯定指向函数对象本身。

在函数体内部要获取一个指向函数本身对象的引用有三种方式:

  • 函数名
  • arguments.callee
  • 作用域下的一个指向该函数的变量名
参数默认值解决

通常是由于实参个数少于形参的个数,导致某些参数并没有被定义,函数内使用这些参数时,参数值将会是 undefined,为了避免会造成少量逻辑异常,可以做少量默认值解决。

var a = function (x) {    //根据形参实参个数做解决    if (arguments.length !== arguments.callee.length) {        throw Error("...");    }    //解决参数默认值    x = x || "default"; // 等效于 if(x === undefined) x = "default";}
参数类型解决
var a = function (x) {    //根据形参实参个数做解决    if (arguments.length !== arguments.callee.length) {        throw Error("...");    }    //解决参数默认值    x = x || "default"; // 等效于 if(x === undefined) x = "default";    //参数类型解决    if (Array.isArray(x)) {        //...       }    if (x instanceof Function) {        //...       }     //...}

参数类型的解决可能比较常见,通过各种辅助手段,确认所需的参数类型到底是不是期望的类型。

多个参数时将其封装在对象内

当函数的形参个数比较多的时候,对于这个函数的调用是比较令人头疼的,由于必需要记住这么多参数,每个位置应该传哪个。这个时候,即可以通过将这些参数都封装到对象上,函数调用传参时,就不必关心各个参数的顺序,都增加到对象的属性中就可。

//函数用于复制原始数组指定起点位置开始的n个元素到目标数组指定的开始位置function arrayCopy(fromArray, fromStart, toArray, toStart, length) {    //...}//外部调用时,传入对象内只需有这5个属性就可,不必考虑参数顺序,同时这种方式也可以实现给参数设置默认值function arrayCopyWrapper(args) {    arrayCopy(args.fromArray,                args.fromStart || 0,                 args.toArray,                args.toStart || 0,                args.length);}arrayCopyWrapper({fromArray:[1,2,3], fromStart:0, toArray:a, length:3});

第二种方式相比第一种方式会更方便使用。

函数特性

函数既是函数,也是对象。它拥有相似其余语言中函数的角色功能,同时,它本身也属于一个对象,同样拥有对象的相关功能。

当作为函数来对待时,它的主要特性也就是函数的定义和调用:如何定义、如何调用、不同定义方式有和区别、不同调用方式适用哪些场景等等。

而当作为对象来看待时,对象上的特性此时也就适用于这个函数对象,如:动态为其增加或者删除属性、方法,作为值被传递使用等。

所以,函数的参数类型也可以是函数,函数对象也可以拥有类型为函数的属性,此时称它为这个对象的方法。

假如某些场景下,函数的每次调用时,函数体内部都需要一个唯一变量,此时通过给函数增加属性的方式,可以避免在全局作用域内定义全局变量,这是 Java 这类语言做不到的地方。

相似需要跟踪函数每次的调用这种场景,就都可以通过对函数增加少量属性来实现。

function uniqueCounter() {    return uniqueCounter.counter++;}uniqueCounter.counter = 0;var a = uniqueCounter();  //a = 0;var b = uniqueCounter();  //b = 1;var c = uniqueCounter();  //c = 2;

尽管定义全局变量的方式也可以实现,但容易污染全局空间的变量。

函数属性

除了可动态对函数增加属性外,因为函数都是继承自 Function.prototype 原型,因而每个函数其实已经自带了少量属性,包括常用的方法和变量,比方上述详情过的 arguments。

这里就来学下,一个函数本身自带了哪些属性,不过函数比较特别,下面详情的少量属性并没有被归入标准规范中,但各大浏览器却都有实现,不过使用这类属性还是要注意下:

arguments

上述详情过,这个属性是个类数组对象,用于存储函数调用时传入的实参列表。

但有一点需要注意,在严格模式下,不允许使用这个属性了,这个变量被作为一个保留字了。

length

上述也提过,这个属性表示函数公告时的形参个数,也可以说是函数期望的参数个数。

有一点也需要注意,在函数体内不能直接通过 length 或者 this.length 来访问这个属性,由于函数会跟随着不同的调用方式有不同的上下文 this,并不肯定都指向函数对象本身。

而 arguments 对象中还有一个属性 callee,它指向当前正在执行的函数,在函数体内部可以通过 arguments.callee 来获取函数对象本身,而后访问它的 length 属性。

在函数外部,即可以直接通过访问函数对象的属性方式直接获取 length。如:

var a = function (x, y) {    console.log(arguments.length);    console.log(arguments.callee.length);}a(1); // 输出 1 2,实参个数1个,形参个数2个a.length;  //2

但需要注意一点,在严格模式下,函数体内部就不能通过 arguments.callee.length 来使用了。

caller

caller 属性表示指向当前正在执行的函数的函数,也就是当前在执行的函数是在哪个函数内执行的。这个是非标准的,但大多浏览器都有实现。

在严格模式下,不能使用。

还有一点需要注意的是,有的书里是说这个 caller 属性是函数的参数对象 arguments 里的一个属性,但某些浏览器中,caller 是直接作为函数对象的属性。

总之,arguments,caller,callee 这三个属性假如要使用的话,需要注意一下。

name

返回函数名,这个属性是 ES6 新添加的属性,但某些浏览器在 ES6 出来前也实现了这个属性。即便不通过这个属性,也可以通过函数的 toSring() 来获取函数名。

bind()

用于将当前函数绑定至指定对象,也就是作为指定对象的方法存在。同时,这个函数会返回一个函数类型的返回值,所以通过 bind() 方式,可以实现以函数调用的方式来调用对象的方法。

function f(y) {    return this.x + y;}var o = {x:1}var g = f.bind(o);g(2);  //输出 3

此时 g 尽管是个函数,但它表示的是对象 o 的方法 f,所以 g() 这种形式尽管是函数调用,但实际上却是调用 o 对象的方法 f,所以方法 f 函数体中的 this 才会指向对象 o。

另外,假如调用 bind() 时传入了多个参数,第一个参数表示需要到的对象,剩余参数会被使用到当前函数的参数列表。

prototype

该属性名直译就是原型,当函数被当做构造函数使用时才有它的意义,用于当某个对象是从构造函数实例化出来的,那么这个对象会继承自这个构造函数的 prototype 所指向的对象。

尽管这个属性的中文直译就是原型,但我不喜欢这么称呼它,由于原型应该是指从子对象的角度来看,它们继承的那个对象,称作它们的原型,由于原型就是相似于 Java 里父类的概念。

尽管,子对象的原型的确由构造函数的 prototype 决定,但假如将这个词直接翻译成原型的话,那先来看下这样的一句表述:通过构造函数创立的新对象继承自构造函数的原型。

没觉得这句话会有一点儿歧义吗?构造函数本质上也是一个对象,它也有继承结构,它也有它继承的原型,那么上面那句表述到底是指新对象继承自构造函数的原型,还是构造函数的 prototype 属性值所指向的那个对象?

所以,你可以看看,在我写的这系列文章中,凡是出现需要形容新对象的原型来源,我都是说,新对象继承自构造函数的 prototype 所指向的那个对象,我不对这个属性名进行直译,由于我觉得它会混淆我的了解。

另外,在 prototype 指向的原型对象中增加的属性,会被所有从它关联的构造函数创立出来的对象所继承。所有,数组内置提供的少量属性方法、函数内置提供的相关属性方法,实际上都是在 Array.prototype 或者 Function.prototype 中定义的。

call() 和 apply()

这两个方法在函数调用一小节中详情过了,由于在 JavaScript 中的函数的动态的,任意函数都可以作为任意对象的方法被调用,即便这个函数公告在其余对象中。此时,就需要通过间接调用实现,也就是通过 call()apply()

一种很常见的应用场景,就是用于调用原型中的方法,相似于 Java 中的 super 调用父类的方法。由于子类可能重写了父类的方法,但有时又需要调用父类的方法,那么可通过这个实现。

toString()

Function.prototype 重写了 Object.prototype 中提供的 toString 方法,自己设置的函数会通常会返回函数的完整源码,而内置的函数通常返回 [native code] 字符串。

借助这个可以获取到自己设置的函数名。

嵌套函数

嵌套函数就是在函数体中继续定义函数,需要跟函数的方法定义区别开来。

函数的方法定义,是将函数看成对象,定义它的属性,类型为函数,这个函数只是该函数对象的方法,并不是它的嵌套函数。

而嵌套函数需要在函数体部分再用 function 定义的函数,这些函数称为嵌套函数。

var x = 0;var a = function () {    var x = 1;    function b() {        console.log(x);    }    var c = function () {        console.log(x);    }    b();  //输出:1    c();  //输出:1    a.d();//输出:0}a.d = function () {    console.log(x);}

函数 b 和 c 是嵌套在函数 a 中的函数,称它们为嵌套函数。其实本质就是函数体内部的局部变量。

函数 d 是函数 a 的方法。

嵌套函数有些相似于 Java 中的非静态内部类,它们都可以访问外部的变量,Java 的内部类本质上是隐式的持有外部类的引用,而 JavaScript 的嵌套函数,其实是因为作用域链的生成规则形成了一个闭包,以此才能嵌套函数内部可以直接访问外部函数的变量。

闭包涉及到了作用域链,而继承涉及到了原型链,这些概念后面会专门来讲述。

这里略微提下,闭包浅显点了解也就是函数将其外部的词法作用域包起来,以便函数内部能够访问外部的相关变量。

通常有大括号出现都会有闭包,所以函数都会对应着一个闭包。

高级应用场景

利用函数的特性、闭包特性、继承等,能够将函数应用到各种场景。

使用函数作为临时命名空间

JavaScript 中的变量作用域大概就两种:全局作用域和函数内作用域,函数内定义的变量只能内部访问,外部无法访问。函数外定义的变量,任何地方均能访问。

基于这点,为了保护全局命名空间不被污染,常常利用函数来实现一个临时的命名空间,两种写法:

var a;(function () {    var a = 1;    console.log(a);  //输出1})();console.log(a);  //输出undefined

简单说就是定义一个函数,定义的同时末尾加上 () 顺便调用执行函数体内容,那么这个函数的作用其实也就是创立一个临时的命名空间,在函数体内部定义的变量不用担心与其余人起冲突。

(function () {   //...}());

外层括号不能漏掉,末尾函数调用的括号也不能漏掉,这样即可以了,至于末尾的括号是放在外层括号内,还是外都可以。

使用函数封装内部信息

闭包的特性,让 JavaScript 尽管没有相似 Java 的权限控制机制,但也能近似的模拟实现。

由于函数内的变量外部访问不到,而函数又有闭包的特性,嵌套函数可以包裹外部函数的局部变量,那么外部函数的这些局部变量,只有在嵌套函数内可以访问,这样即可以实现对外隐藏内部少量实现细节。

var a = function () {    var b = 1;    return {        getB: function () {            return b;        }    }}console.log(c.b); //输出 undefinedvar c = a();   //输出 1

大家好,我是 dasu,欢迎关注我的公众号(dasuAndroidTv),公众号中有我的联络方式,欢迎有事没事来唠嗑一下,假如你觉得本篇内容有帮助到你,可以转载但记得要关注,要标明原文哦,谢谢支持~

dasuAndroidTv2.png

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 前台入门12-JavaScript语法之函数

发表回复