JavaScript 系列博客(三)

前言

本篇介绍 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 defined
fn();
// function

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

Function 构造函数

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

构造函数接收三个参数,最后一个为 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

函数 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) // 3
f(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(); // 5
inc(); // 6
inc(): // 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的私有变量。

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

JavaScript 系列博客(三)的更多相关文章

  1. JavaScript 系列博客(六)

    JavaScript 系列博客(六) 前言 本篇博客介绍 js 操作高级,通过 js 获取标签的全局属性.设置标签的全局属性,以及事件的绑定与取消.js 盒模型与 js 动画. 对象使用的高级 对象的 ...

  2. JavaScript 系列博客(五)

    JavaScript 系列博客(五) 前言 本篇博客学习 js 选择器来控制 css 和 html.使用事件(钩子函数)来处理事件完成后完成指定功能以及js 事件控制页面内容. js 选择器 在学习 ...

  3. JavaScript 系列博客(二)

    JavaScript 系列博客(二) 前言 本篇博客介绍 js 中的运算符.条件语句.循环语句以及数组. 运算符 算术运算符 // + | - | * | / | % | ++ | -- consol ...

  4. JavaScript 系列博客(一)

    JavaScript 系列博客(一) 前言 本系列博客为记录学习 JavaScript 的学习笔记,会从基础开始慢慢探索 js.今天的学习笔记主要为 js 引入.定义变量以及 JavaScript 中 ...

  5. JavaScript 系列博客(七)

    JavaScript 系列博客(七) 前言 本篇博客介绍页面节点概念.文档结构以及如何使用 js 操作文档节点还有事件 target 以及 BOM 操作. 节点 dom与dom属性 // DOM: 文 ...

  6. JavaScript 系列博客(四)

    JavaScript 系列博客之(四) 前言 本篇介绍 JavaScript 中的对象.在第一篇博客中已经说到 JavaScript 是一种''对象模型''语言.所以可以这样说,对象是 JavaScr ...

  7. JavaScript学习系列博客_1_JavaScript简介

    这个系列博客主要用来记录本人学习JavaScript的笔记,从0开始,即使有些知识我也是知道的.但是会经常忘记,干脆就写成博客,没事的时候翻来看一看,留下一点学习的痕迹也好.可能写博客的水平暂时不太好 ...

  8. Django 系列博客(三)

    Django 系列博客(三) 前言 本篇博客介绍 django 的前后端交互及如何处理 get 请求和 post 请求. get 请求 get请求是单纯的请求一个页面资源,一般不建议进行账号信息的传输 ...

  9. Django 系列博客(七)

    Django 系列博客(七) 前言 本篇博客介绍 Django 中的视图层中的相关参数,HttpRequest 对象.HttpResponse 对象.JsonResponse,以及视图层的两种响应方式 ...

随机推荐

  1. C#如何以管理员身份运行程序 转

    在使用winform程序获取调用cmd命令提示符时,如果是win7以上的操作系统,会需要必须以管理员身份运行才会执行成功,否则无效果或提示错误. 比如在通过winform程序执行cmd命令时,某些情况 ...

  2. CentOS MariaDB 安装和配置

    sudo vi /etc/yum.repos.d/mariadb.repo # MariaDB 10.1 CentOS repository list - created 2017-03-23 13: ...

  3. C# WebAPI系列(1)

    WebApi是微软在VS2012 MVC4版本中绑定发行的,WebApi是完全基于Restful标准的框架.RestFul: (英文:Representational State Transfer,简 ...

  4. IDEA的Database表的基本操作

    1.创建表 方法一:直接创建:右键-new-table 方法2: 参考别的表,直接用语句,右键-DDL and Sources- 然后直接在控制台修改 修改后直接运行,表就建好了 2.备份表 先用上面 ...

  5. openvpn搭建和使用

    一.openvpn原理 openvpn通过使用公开密钥(非对称密钥,加密解密使用不同的key,一个称为Publice key,另外一个是Private key)对数据进行加密的.这种方式称为TLS加密 ...

  6. 【腾讯Bugly干货分享】那些年,我们一起写过的“单例模式”

    题记 度娘上对设计模式(Design pattern)的定义是:"一套被反复使用.多数人知晓的.经过分类编目的.代码设计经验的总结."它由著名的"四人帮",又称 ...

  7. 【javascript】谈谈HTML5: Web-Worker、canvas、indexedDB、拖拽事件

    前言:作为一名Web开发者,可能你并没有对这个“H5”这个字眼投入太多的关注,但实际上它早已不知不觉进入到你的开发中,并且总有一天会让你不得不正视它,了解它并运用它   打个比方:<海贼王> ...

  8. IEDA的程序调试debug

    以前只是浅层面的使用dubug来查看程序运行顺序,排查一些异常的原因, 今天由于要学习一些源码,所以系统的记录一下(借鉴网上资料总结而来) 主要涉及到的功能区为如下: A::重启项目 快捷键 Ctrl ...

  9. 前端“黑话”polyfill

    前言 在Web前端开发这个日新月异的时代,总是需要阅读一些最新的英文技术博客来跟上技术的发展的潮流.而有时候会遇到一些比较高频的“黑话”,在社区里面可能已经是人人皆知的“共同语言”,而你接触的少就偏偏 ...

  10. centos7使用wordpress布署网站(2)

    1.接下来需要配置数据库,为使用wordpress做准备 修改认证方式: vim .../phpMyAdmin/config.inc.php [...] $cfg['Servers'][$i]['au ...