继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

JavaScript 系列博客(三)

慕桂英546537
关注TA
已关注
手记 301
粉丝 31
获赞 200

前言

本篇介绍 JavaScript 中的函数知识。

函数的三种声明方法

function 命令

可以类比为 python 中的 def 关键词。

function 命令声明的代码区块,就是一个函数。命令后面是函数名,函数名后面的圆括号里面是要传入的形参名。函数体放在大括号里面。

function fn(name) {    console.log(name);
}

使用 function 命名了一个 fn 函数,以后可以通过调用 fn 来运行该函数。这叫做函数的声明(Function Declaration)。

函数表达式

除了使用 function 命令声明函数外,可以采用变量赋值的写法。(匿名函数)

var fn = function(name) {    console.log(name);
};

这种写法将一个匿名函数赋值给变量。这时,这个匿名函数又称之为函数表达式(Function Expression),因为赋值语句的等号右侧只能放表达式。

采用函数表达式声明函数时,function 命令后面不带有函数名。如果加上函数名,该函数名只能在函数体内访问,在函数体外部无效。

var fn = function x(name) {    console.log(typeof x);
};
x// ReferenceError: x is not definedfn();// function

声明函数时,在函数表达式后加了函数名 x,这个 x 只可以在函数内部使用,指代函数表达式本身。这种写法有两个用处:一可以在函数体内部调用自身;二方便debug(debug 显示函数调用栈时,会显示函数名)。需要注意的是,函数表达式需要在语句的结尾加上分号,表示语句结束。而函数的声明在结尾的大括号后面不用加分号。

Function 构造函数

第三种声明函数的方法是通过构造函数,可以理解为 python 中的函数类,通过传入参数并且返回结果就可以创建一个函数。

5acb3c8700013dc501600160.jpg

构造函数接收三个参数,最后一个为 add函数的‘’函数体‘’,其他参数为add 函数的参数。可以为构造函数传递任意数量的参数,不过只有最后一个参数被当做函数体,如果只有一个参数,该参数就是函数体。

Function 构造函数也可以不用 new 命令,结果一样。这种声明函数的方式不直观,使用概率很少。

函数的调用

和 python 一样,调用一个函数通过圆括号,圆括号中是要传入的实参。

函数体内部的 return 语句,表示返回。JavaScript 引擎遇到 return 时,就直接返回 return 后面表达式的值(和 python 一样),所以 return 后面的代码是无意义的,如果没有 return 那么就会返回 undefined(python 中返回 None)。

函数作用域

作用域的定义

作用域指的是变量存在的范围。在 ES5中,JavaScript 只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,任意位置可以访问到;另一种是函数作用域,也称之为局部作用域,变量只有在函数内部才能访问到。ES6新增了块级作用域,等价于局部作用域一样,就是新增了一种产生局部作用域的方式。通过大括号产生块级作用域。

在函数外部声明的变量就是全局变量,可以在任意位置读取。

在函数内部定义的变量,外部无法读取,只有在函数内部可以访问到。并且函数内部定义的同名变量,会在函数内覆盖全局变量。

注意:对于 var 命令来说,局部变量只可以在函数内部声明,在其他区块中声明,一律都是全局变量。ES6中声明变量的命令改为 let,在区块中声明变量产生块级作用域。

函数内部的变量提升

与全局作用域一样,函数作用域也会产生‘’变量提升‘’现象。var 命令生命的变量,不管在什么位置,变量声明都会被提升到函数体的头部。

function foo(x) {    if (x > 100) {        var tmp = x - 100;
    }
}// 等同于function foo(x) {    var tmp;    if (x > 100) {
        tmp = x - 100;
    }
}

函数本身的作用域

函数和其他值(数值、字符串、布尔值等)地位相同。凡是可以使用值得地方,就可以使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当做参数传入其他函数,或者作为函数的结果返回。函数是一个可以执行的值,此外没有特殊之处。

函数也有自己的作用域,函数的作用域称为局部作用域。与变量一样,就是其生命时所在的作用域,与其运行时所在的作用域无关(闭包、装饰器)。通俗地讲就是在定义函数的时候,作用域已经就确定好了,那么在访问变量的时候就开始从本作用域开始查找,而与函数的调用位置无关。

var x = function () {    var a = 1;    console.log(a);
};function y() {    var a = 2;
    x();
}
y(); // 1

5acb3c8700013dc501600160.jpg

函数 x 是在函数 f 的外部生命的,所以它的作用域绑定外层,内部变量 a 不会到函数 f 体内取值,所以输出1,而不是2。

总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。

函数参数

调用函数时,有时候需要外部传入的实参,传入不同的实参会得到不同的结果,这种外部数据就叫参数。

参数的省略

在 JavaScript 中函数参数不是必需的,就算传入的参数和形参的个数不相等也不会报错。调用时无论提供多少个参数(或者不提供参数),JavaScript 都不会报错。省略的参数的值变为 undefined。需要注意的是,函数的 length 属性值与实际传入的参数个数无关,只反映函数预期传入的参数个数。

但是,JavaScript 中的参数都是位置参数,所以没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显示的传入 undefined。

传递方式

函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(pass by value)。这意味着,在函数体内修改参数值,不会影响到函数外部(局部变量的修改不会影响到全局变量:对于基本数据类型)。

但是,如果函数参数是复合类型的值(数组、对象、其他函数),因为传值方式为地址传递(pass by reference)。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。

注意:如果函数内部修改的不是参数对象的某个属性,而是直接替换掉整个参数,这时不会影响到原始值。

var obj = [1, 2, 3];function f(o) {
    o = [2, 3, 4];
}
f(obj);

obj // [1, 2, 3]

上面代码,在函数 f 内部,参数对象 obj 被整个替换成另一个值。这时不会影响到原始值。这是因为,形式参数(o)的值实际上是参数 obj 的地址,重新对o 赋值导致 o 指向另一个地址,保存在原地址上的数据不会被改变。

同名参数

如果有同名的参数,则取最后出现的那个值。

function f(a, a) {    console.log(a);
}

f(1, 2) // 2

上面代码中,函数 f 有两个参数,且参数名都是 a。取值的时候,以后面的 a 为准,即使后面的a 没有值或被省略,也是以其为准。

function f(a, a) {    console.log(a);
}
f(1) // undefined

调用函数 f 时,没有提供第二个参数,a 的取值就变成了 undefined。这时,如果要获得第一个 a 的值,可以使用 arguments 对象(类比linux 中的arg)。

function f(a, a) {    console.log(arguments[0]);
}

f(1) // 1

arguments 对象

定义

由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是 arguments 对象的由来。

arguments 对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,以此类推。注意:该对象只有在函数体内部才可以使用。

正常模式下,arguments 对象可以在运行时修改。

var f = function(a, b) {    arguments[0] = 3;    arguments[1] = 3;    return a + b;
}
f(1, 1) // 5

上面代码中,调用 f 时传入的参数,在函数体内被修改了,那么结果也会修改。

严格模式下,arguments 对象是一个只读对象,修改它是无效的,但不会报错。

var f = function(a, b) {    'use strict'; // 开启严格模式
    arguments[0] = 3; // 无效
    arguments[1] = 2; // 无效
    return a + b;
}

f(1, 1) // 2

开启严格模式后,虽然修改参数不报错,但是是无效的。

通过 arguments 对象的 length 属性,可以判断函数调用时到底带几个参数。

function f() {    return arguments.length;
}
f(1, 2, 3) // 3f(1) // 1

与数组的关系

需要注意的是,虽然 arguments 很像数组,但它是一个对象。数组专有的方法(比如 slice 和 forEach),不能再 arguments 对象上直接使用。

如果要让 arguments 对象使用数组方法,真正的解决方法是将 arguments 转为真正的数组。下面是两种常用的转换方法:slice 方法和逐一填入新数组。

var args = Array.prototype.slice.call(arguments);// var args = [];for (var i = 0; i < arguments.length; i++) {
    args.push(arguments[i]);
}

callee 属性

arguments 对象带有一个 callee 属性,返回它所对应的原函数。

var f = function() {    console.log(arguments.callee === f); 
}
f(); // true

可以通过 arguments.callee,达到调用自身的目的。这个属性在严格模式里面是禁用的,不建议使用。

函数闭包

闭包是所有编程语言的难点,在 python 中闭包的多应用于装饰器中。在 JavaScript 中闭包多用于创建作用域,或者解决变量污染的问题。

理解闭包,首先需要理解变量作用域。在 ES5中,JavaScript 只有两种作用域:全局作用于和函数作用域。函数内部可以直接读取全局变量。

var n = 999;function f1() {    console.log(n);
}
f1(); // 999,n是全局变量,可以被访问到

但是函数外部无法读物函数内部声明的变量。

function f1() {    var n = 999;
}console.log(n);// Uncaught ReferenceError: n is not defined

因为变量作用域的关系,在外部需要访问到局部变量在正常情况下是做不到的,这就可以通过闭包来实现。下来来看一个经典例子:循环绑定事件产生的变量污染

<div class="box">
    0000001</div><div class="box">
    0000002</div><div class="box">
    0000003</div><script>
    var divs = document.querySelectorAll(".box");    // 存在污染的写法
    for (var i =0; i < divs.length; i++) {
        divs.onclick = function () {            console.log('xxx', i)
        }
    }    // 运行结果显示4</script>

会产生变量污染的原因是作用域,因为 var 并不产生作用域,所以在 for循环中的变量就是全局变量,只要 for循环结束那么 i 的值就确定了,除非在极限情况下,你的手速比 cpu 还要快,那么可能会看到小于4的值。这样的问题可以通过函数的闭包来解决。产生新的作用域用来保存 i 的值。

for (var i = 0; i < divs.length; i++) {
    (function () {        var index = i;
        divs[index].onclick = function () {            console.log('xxx', index);
        }
    })()
}// 另一种版本for (var i = 0; i < divs.length; i++) {    function(i) {
        divs[i].onclick = function () {            console.log('yyy', i)
        }
    }(i)
}

利用闭包原理产生新的作用域用来保存变量 i 的值,这样就解决了变量污染的问题,还有利用ES6的声明变量关键词 let,也会产生新的作用域(块级作用域)也可以解决变量污染的问题。

在 JavaScript 中,嵌套函数中的子函数中可以访问到外部函数中的局部变量,但是外部函数访问不到子函数中的局部变量,这是 JavaScript 中特有的‘’链式作用域‘’结构(python 也一样),子对象会一级一级的向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。可以简单地把闭包理解为‘’定义在一个函数内部的函数‘’,闭包最大的特点就是它可以‘’记住‘’诞生的环境,在本质上闭包就是将函数内部和函数外连接起来的一座桥梁。

必报的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生的环境一直存在。下面的例子:

function createIncrementor(start) {    return function () {        return start++;
    };
}var inc = createIncrementor(5);

inc(); // 5inc(); // 6inc(): // 7

上面代码中,start 是函数 createIncrementor 的内部变量。通过闭包,start 的状态被保存,每一次调用都是在上一次调用的基础上进行计算。从中可以看出,闭包 inc 使得函数 createIncrementor 的内部环境一直存在。所以闭包可以看做是函数内部作用域的一个接口。为什么会这样呢?原因就在于 inc 始终在内存中,而 inc 的存在依赖于 createIncrementor,因此也一直存在于内存中,不会再外层函数调用结束后 start 变量被垃圾回收机制回收。

闭包的另外一个用处是封装对象的私有属性和私有方法。(这部分还不太懂,还需要琢磨)

function Person(name) {  var _age;  function setAge(n) {
    _age = n;
  }  function getAge() {    return _age;
  }  return {    name: name,    getAge: getAge,    setAge: setAge
  };
}var p1 = Person('张三');
p1.setAge(25);
p1.getAge() // 25

上面代码中,函数 Person 的内部变量_age,通过闭包 getAge 和 setAge,变成了返回对象p1的私有变量。

注意:外城函数每次运行,都会产生一个新的闭包,而这个闭包又会保留外城函数的内部变量,所以内存消耗很大。

原文出处:https://www.cnblogs.com/zuanzuan/p/10153168.html  

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP