js 的函数

js 高程中这样定义函数:函数是这样一段 JavaScript 代码,只定义一次,但可以被执行任意多次。JavaScript 的函数是参数化的:函数的定义会包括一个称为形参和标识符列表,这些参数在函数体中就像局部变量一样工作。函数的调用会为形参提供实参的值。函数使用它们实参的值来计算返回值,成为该函数调用表达式的值。出来实参之外,每次调用还会拥有另一个值-本次调用的上下文-这就是 this 关键字的值。

如果函数挂载在一个对象上,作为对象的一个方法调用,就称之为 对象的方法。当通过这个对象来调用函数时,该对象就是此次调用的上下文,也就是该函数的 this 的值。

用于初始化一个新建的对象的函数成为构造函数

在 JavaScript 里,函数即对象。可以把函数赋值给变量,或者作为参数传递给其他函数。

JavaScript 的函数可以嵌套在其他函数中定义,这样就可以访问它们被定义时所处的作用域中的任何变量。这意味着 JavaScript 函数构成了一个闭包。

函数的定义

函数定义方式一: 函数声明

1
2
3
function funcName(arg0, arg1, ...){
// 函数体
}

函数的组成:

  • function 关键字
  • 函数名标识符:是函数声明语句的必要组成
  • 一对圆括号:其中包含有 0 个或多个逗号分隔的标识符组成的列表,表示函数的参数
  • 一对花括号

函数的定义方式二: 函数表达式

1
2
3
4
5
6
7
var functionName = function(arg0, arg1, ...) {
//函数体
}
var functionName = function foo(arg0, arg1, ...) {
//函数体
}

以函数表达式定义的函数,函数名称是可选的。一条语句实际上声明了一个变量,并把一个函数对象赋值给它。函数表达式定义函数通常不加函数名,图特别适合仅调用一次的函数。

函数的嵌套

1
2
3
4
function hypotenuse(a, b) {
function square(x) { return x * x; }
return Math.sqrt(square(a) + square(b));
}

函数调用、this指向、返回值

一个函数最终产生什么样的结构,跟如何调用这个函数息息相关:函数的四种调用模式

函数的4种调用模式

  1. 第一种模式:函数调用模式,也就是写一个函数,然后调用一下
  2. 第二种模式:方法调用模式,也就是将函数成为对象的一个方法,然后通过对象来调用
  3. 第三种模式:构造函数调用模式,也就是将函数当成构造函数来调用
  4. 第四种调用模式:上下文调用模式,根据调用方式的不同可以产生不同的结果

第四种函数调用的实现方式

  • 实现方式:call/apply (apply 和 call 的唯一区别是第二个参数是数组,将实参值一一传到数组中。fn.call (函数内部的 this 的值,实参1,实参2…))

不同调用模式中的this的值

  1. 函数调用模式中 this 指向:window
  2. 方法调用模式中 this 指向:调用的对象
  3. 构造函数调用模式中 this 指向:构造函数的实例
  4. 上下文调用模式中 this 指向:
  • (1) 如果 call 方法的第一个参数是一个对象,则 fn 函数内部的 this 的值指向该对象
  • (2) 如果 call 方法的第一个参数是一个字符串、数字、布尔值,则 fn 函数内部的 this 的值会转换为该类型所对应的基本包装类型的对象
  • (3) 如果 call 方法的第一个参数是 null ,则 fn 函数内部的 this 的值是 window ——> 就相当于是一次函数调用模式

调用模式中的返回值

  1. 函数调用模式中返回值:由 return 语句决定
  2. 方法调用模式中返回值:由 return 语句决定
  3. 构造函数调用模式中的返回值:
  • (1). 如果构造函数没有手动设置返回值,那么会返回构造函数的实例
  • (2). 如果手动给构造函数添加了返回值,有以下2种情况:
    • (a). 返回值是值类型:最终的返回值还是构造函数的实例
    • (b). 返回值是引用类型(对象):最终的返回值就是该对象
  1. 上下文调用模式中的返回值:由 return 语句决定

函数的实参和形参

函数的可选形参

当调用函数的时候传入的实参比函数声明时指定的形参个数要少,剩下的形参都将设置为 undefined 值。

1
2
3
4
5
6
7
8
9
function getPropertyName(o, /*可选的*/ a) {
if (a === undefined) a = []; // 如果未定义,赋值一个新数组
// 上面这句代码可以替换成 a = a || []; // 这才是习惯写法
for (var property in a) a.push(property);
return a;
}
var a = getPropertyName(o);
getPropertyNames(o, p); // 将 p 的属性追加到数组 a 中

注意: 需要定义可选的实参来实现函数时,需要将可选的实参放在实参列表的最后。

可变长的实参列表、实参对象

当调用函数时传入的实参个数超过函数定义时的形参个数时,没有办法直接获得未命名值的引用。函数的参数对象解决了这个问题。

在函数体内部, 标识符 arguments 指向实参对象的引用,实参对象包含一个 length 属性,是一个伪数组。、

实参的重要用途是可以操作任意数量的实参。

在非严格模式下,当一个函数包含若干个形参,实参对象的数组元素是函数形参所对应实参的别名,实参对象中以数字索引,并且形参名称可以认为是相同变量的不同命名。

严格模式下,arguments 对象变成了一个保留字,不能给其赋值,也不能使用 arguments 作为形参名或者局部变量名,也不能给 arguments 赋值。

callee 和 caller 属性

callee 指向当前正在执行的函数。

caller 是非标准的,但大多数函数实现了这个属性。指的是调用当前正在执行的函数的函数。

将对象的属性作为实参

JavaScript 中,可以通过 键/值 对的形式来传入参数,这样当一个函数中的参数有很多的时候,不需要记住传入的顺序。

这种风格调用的函数,传入的实参都写进一个单独的对象中,在调用的时候传入一个对象,对象中的 键/值 对是真正需要的实参数据。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 将原始数组中的 length 元素复制到目标数组中
function arraycopy(/* array */ from,
/* index */ form_start,
/* array */ to,
/* index */ to_start,
/* integer */ length){
// 代码段
};
// 这种方式效率较低,但不必记住参数顺序
function easycopy(args) {
arraycopy(args.from,
args.from_start || 0,
args.to,
args.to_start || 0,
args.length );
}
// 调用 easycopy 方法
var a = [1, 2, 3, 4], b = [];
easycopy({from: a, to: b, length: 4});

可以使用以上这种代码来适当使用文档说明自己的函数的参数

实参类型

JavaScript 方法的形参并未声明类型,在形参传入函数之前没有做任何类型检测。在定义函数的时候,需要添加类型判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function isArrayLike(o) {
if (o && typeof o === "object"
&& isFinite(o.length)
&& o.length >=0
&& o.length === Math.floor(o.length)
&& o.length < 4294967296)
return true;
else
return false;
}
function sum(a) {
var total = 0;
for(var i = 0; i < argument.length; i++) {
var element = arguments[i], n;
if (element == null) continue; // 忽略 null 和 undefined 实参
if (isArray(element)) // 如果实参是数组
n = sum.apply(this, element); // 递归地计算累加...
else if (typeof element === 'function') // 是函数
n = Number(element()); // 调用并做类型转换
else
n = Number(element); // 否则直接做类型转换
if (isNaN(n)) // 如果无法转换为数字,抛出异常
throw Error("sum(): can't convert " + element + " to number");
total += n;
}
return total;
}

作为值的函数

JavaScript 中的函数不仅仅是一种语法,也是值,可以将函数赋值给变量,也可以存储在对象的属性或数组的元素中,还可以作为参数传入另外一个函数等。

比如:

1
2
var a = [function(x) { return x*x;}, 20];
console.log(a[0](a[2])); // => 400

自定义函数的属性

JavaScript 中的函数并不是原始值,而是一个种特殊的对象,也就是说,函数可以拥有属性。当函数需要一个”静态” 变量来在调用时保持某个值不变,最方便的方式就是给函数定义属性,而不是定义全局变量。

1
2
3
4
uniquInteger.counter = 0;
function uniquInteger() {
return uniquInteger.counter++;
}

计算阶乘的函数:

1
2
3
4
5
6
7
8
9
function factorial(n) {
if (isFinite(n) && n > 0 && n == Math.round(n)) { // 有限的正整数
if (!(n in factorial))
factorial[n] = n * factorial(n-1);
return factorial[n];
}
else return NaN;
}
console.log(factorial[1]); // 初始化

作为命名空间的函数

命名空间内定义的变量不会污染全局变量,这就解决环境中变量冲突问题。

1
2
3
(function() {
// 模块代码段
})();

一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 定义一个用来将第二个以及后续参数复制至第一个参数
// 如果 o 的属性拥有了一个不可枚举的同名属性,则 for/in 循环
// 不会枚举对象 o 的可枚举属性
var extend = (function(){
// 在修复之前,先检测 bug 是否存在
for (var p in {toString: null}) {
return function extend(o) {
// 代码执行到这里,for/in 循环会正确工作并返回
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var prop in source) o[prop] = source[prop];
}
return o;
};
}
// 代码执行到这里,说明 for/in 不会枚举测试对象的 toString 属性
// 如果返回的另一个版本的 extend() 函数,这个函数是显式测试 Object.prototype中的不可枚举属性
return function patched_extend(o) {
for (var i =1; i < arguments.length; i++) {
var source = arguments[i];
for (var prop in source) o[prop] = source[prop];
for (var j = 0; j < protoprops.length; i++) {
prop = protoprops[j];
if (source.hasOwnProperty(prop)) o[prop] = source[prop];
}
}
return o;
};
// 列出了需要检测的特殊属性
var protoprops = ["toString", "valueOf", "constructor", "hasOwnProperty",
"isPrototypeOf", "propertyIsEnumerable", "toLocaleString"
];
}());

js 词法分析

程序执行过程

  1. 读取代码,主关注声明的部分:var
  2. 判断var后面的名字是否已经被标记,如果没有被标记过,就标记
  3. 读取完毕后,代码从上往下,从左往右依次执行

词法作用域(作用域:变量可以使用到不能使用的范围)

词法作用域就是描述变量的访问范围:

  1. 在代码中只有函数可以限定作用范围,允许函数访问外部的变量
  2. 在函数内优先访问内部声明的变量,如果没有才会访问外部的
  3. 所有变量的访问规则,按照预解析规则来访问

作用域链:

每一个函数具有独立作用域,由于函数内可以套函数,所以在函数内部访问变量的时候,需要一级一级的往上查找该变量,这样就好像构成了一个链式结构,把它称之为作用域链。

严格模式

开启严格模式:”use strict”;

  1. 严格模式中禁止给一个未声明的变量赋值:
  2. 严格模式中eval具有了独立作用域——>在eval中声明的变量和函数都是局部变量
  3. 严格模式中禁止使用arguments.callee进行递归调用

闭包

JavaScript 函数的执行依赖于变量作用域,这个作用域在函数定义时决定的,而不是函数调用的时候决定的。为了实现这种词法作用域, JavaScript 函数对象的内部状态不仅仅包含函数的代码逻辑,还必须引用当前的作用域, JavaScript 函数对象的内部状态不仅包含函数代码逻辑,还必须引用当前的作用域链。函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性称闭包。

函数定义时的定义作用域链到函数执行时依然有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 利用闭包实现的私有属性存取器方法
function addPrivateProperty (o, name, predicate) {
var value;
o["get" + name] = function() { return value; };
o["set" + name] = function(v) {
if (predicate && !predicate(v))
throw Error("set" + name + ": invalid value " + v);
else
value = v;
};
}
var o = {};
addPrivateProperty(o, "Name", function(x) { return typeof x == "string"});
o.setName("Hiraku"); // 设置属性值
console.log(o.getName); // 获取属性值
o.setName(0); // 试图设置一个错误的值

闭包的一个应用: 模块化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//面向对象——>模块化
var SongManager2=(function(){
function f1(){}
function f2(){}
function f3(){}
//构造函数
function SongManager(){}
SongManager.prototype={ //原型对象
init:function(){
f1();
f2();
f3();//这3个功能:如果业务逻辑非常复杂,需要把这些方法拆分掉,
// 并且这些方法不能让用户随便调用,用一些函数封装一下
},
//原型对象中的方法对于子对象是完全公开的,对象可以随意调用
init1:function(){},
init2:function(){}
};
return SongManager;
}());

闭包实现思路:外层函数,内层函数

  • 通常设置外层函数的返回值就是内层函数
  • 也可以让外层函数的返回值是一个对象(方法)
  • 如果需要保存一个数据(外层函数的同一个变量),让内层函数调用多次,该变量的值都是共享的
  • 如果需要保存多个数据(外层函数的同一个变量),让外层函数调用多次

函数的属性、方法和构造函数

JavaScript 中的函数是值, 使用 typeof 方法得到的结果是 “function”,但是函数是 JavaScript 中的特殊的对象,也可以拥有属性和方法。甚至可以用 Function()构造函数来创建新的函数对象。

length 属性

在函数体里, arguments.length 表示传入函数的实参的个数。而函数本事的 length 属性是只读的,它代表函数形参数量,即函数定义时时给出的实参个数,通常也是函数在调用时期望传入的个数。

1
2
3
4
5
6
7
8
9
10
11
12
// 判断所传入的实参个数是否正确
// 该函数使用 arguments.callee 不能在严格模式下调用
function check(args) {
var actual = args.length;
var expected = args.callee.length;
if (actural !== expected)
throw Error("Expected " + expected + "args; got " + actual);
}
function f(x, y, z) {
check(arguments);
return x + y + z;
}

prototype 属性

每一个函数都包含一个 prototype 属性,该属性指向一个对象的引用,这个对象称为原型对象。下一篇文章将深入分析。

call() 方法和 apply() 方法

call() 和 apply() 的第一个实参是要调用函数的母体对象,它是调用上下文,在函数体内通过 this 来获得对它的引用。

在严格模式中,call() 和 apply() 的第一个实参都会变成 this 的值,哪怕传入的是 null 或 undefined。在严格模式下,传入 null 或 undefined 的时候都会被全局对象代替。

call/apply区别

1.相同点:

  • (1) 都是Function.prototype对象中定义的方法
  • (2) 第一个参数都是表示函数内部的this的值
  1. 不同点:
  • 如果需要给函数传递参数的时候:
    • 利用call方法,将函数的参数从第二个参数开始依次排开
    • apply方法的第二个参数是一个数组对象,数组的第一个参数表示函数的第一个实参,依次以此类推

apply的一个漂亮的应用

1
2
3
4
var points = [
{ x: 110, y: 50}, { x: 130, y: 60 }, { x: 20, y: 70 }, { x: 60, y: 50 }
];
var maxX = Math.max.apply( null, points.map(function (v) { return v.x; }));

以上代码中借用Math对的max方法,利用arr.map()方法中返回的是数组这一特性得到了数组中对象的某个属性的最大值。

将当前函数的 arguments 数组直接传入 apply() 来调用另一个函数

1
2
3
4
5
6
7
8
9
10
11
// 将对象 o 中的方法替换为另一个方法
// 可以在调用原始方法之前和之后记录日志消息
function trace(o, m) {
var original = o[m]; // 在闭包中保存原有方法
o[m] = function () { // 定义新方法
console.log(new Date(), "Entering:", m); // 输出日志消息
var result = original.apply(this, arguments); // 调用原始函数
console.log(new Date(), "Exiting:", m); // 输出日志消息
return result; // 返回结果
};
}

trace 方法接收两个参数,一个对象和一个方法名,它将制定的方法替换为一个新的方法。

bind() 方法

bind() 方法在 Function 的原型对象上

bind 方法是 ECMAScript 5 的新方法,用途是将函数绑定到某个对象。当函数 f() 上调用 bind 方法并传入一个对象 o 作为参数,这个方法返回一个新的对象。

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

bind() 简单绑定

1
2
3
4
5
6
7
// 返回一个函数,通过调用它来调用 o 中的方法 f(), 传递它所有的实参
function bind(f, o) {
if (f.bind) return f.bind(o);
else return function() {
return f.apply(o, arguments);
}
}

ECMAScript 5 的 bind() 方法不仅仅是将函数绑定到一个对象,它还可以附带一些其它应用:除了第一个参数外,传入 bind() 的实参也会绑定到 this。

1
2
3
4
5
6
7
8
var sum = function(x, y) { return x + y; };
// 创建一个类似 sum 的函数,但 this 的值绑定到 null
// 并且第一个参数绑定到 1,这个新的函数期望只传入一个实参
var succ = sum.bind(null, 1);
console.log(succ(2)); // 3: x 绑定到 1, 并传入 2 作为实参 y
function f(y,z) { return this.x + y + z };
var g = f.bind({x:1}, 2);
console.log(g(3)); // 6: this.x 绑定到 1,y 绑定到 2, z 绑定到 3

bind() 的兼容方法

1
2
3
4
5
6
7
8
9
10
11
12
if (!Function.prototype.bind) {
Function.prototype.bind = function (o /*, args*/) {
// 将 this 和 arguments 的值保存到变量中
var self = this, boundArgs = arguments;
return function() {
var args = [], i;
for (i = 1; i < boundArgs.length; i++) args.push(boundArgs[i]);
for (i = 1; i < arguments.length; i++) args.push(arguments[i]);
return self.apply(o, args);
};
};
}

ECMAScript 5 的 bind() 方法返回的函数不包含 prototype 属性,并且将这些绑定的函数用作构造函数时所创建的对象从原始的未绑定的构造函数中继承 prototype 。

toString() 方法

函数也有 toString() 方法,大多数函数的 toString() 方法返回包含函数体本身的字符串。

静态属性和实例属性

  1. 给函数添加一个属性(静态属性——>函数对象自身的属性)
  2. 给某个构造函数的实例添加的属性:实例属性

所有的函数对象都共有的一些静态属性

  1. name:获取函数的名称
  2. length:表示函数形参的个数
  3. caller:表示当前函数调用是在哪个函数内

Function() 构造函数

Function() 构造函数可以传入任意数量的字符串实参,最后一个实参所表示的文本就是函数体,它可以包含任意的 JavaScript 语句,每两条语句之间用分号分隔。传入构造函数的其它所有字符串是指定函数的形参名字的字符串。如果定义的函数不包含任何参数,只需要给构造函数简单的传入一个字符串,即函数体。

注: Function() 构造函数并不需要通过传入实参以指定函数名。就像函数的直接量一样,Function() 构造函数创建一个匿名函数。

  • Function() 构造函数允许 JavaScript 在运行时动态地创建并编译函数
  • 每次调用 Function() 构造函数都会解析函数体,并创建函数对象
  • Function() 构造函数创建的函数并不使用词法作用域,函数体代码的编译总是会在顶层函数执行。
1
2
3
4
5
6
7
var scope = "global";
function constructFunction() {
var scope = "local";
return new Function("return scope"); // 这里无法使用局部作用域的变量
}
// 通过 Function() 构造函数定义的函数使用的不是局部作用域
console.log(constructFunction()()); // "golobal"

可以认为 Function() 构造函数是在全局作用域中执行的 eval(); eval() 可以在自己的私有作用域内定义新变量和函数。 Function() 很少用到。

可调用的对象

类似于 所有的”伪数组”,对于函数也存在类似的情况。”可调用对象” 是一个对象,可以在函数调用表达式中调用这个对象。所有的函数都是可调用的,但并非所有的可调用对象都是函数。

可调用对象在两个 JavaScript 实现中不能算作函数。首先,IE Web 浏览器实现了客户端方法,比如 Window.alert(), Document.getElementById(), 使用了可调用的宿主对象,而不是内置函数对象。

另一个可调用对象是 RegExp 对象,可以直接调用 RegExp 对象,这比调用它的 exec() 方法更快一些。这是 JavaScript 中的一个非标准特性,使用typeof 运算的结果并不统一。

函数式编程

使用函数处理数组

ECMAScript 3 中没有数组的 map() 和 reduce() 函数,封装兼容的 map() 和 reduce()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 对于数组的每个元素调用函数,返回一个数组
var map = Array.prototype.map
? function(a, callback) { return a.map(callback); }
: function(a, callback) {
var results = [];
for (var i = 0, len = a.length; i < len; i++) {
if (i in a) results[i] = callback.call(null, a[i], i, a);
}
return results;
};
// 如果函数 callback 和可选的初始值将数组 a 减至一个值
var reduce = Array.prototype.reduce
? function(a, callback, initial) {
if (arguments.length > 2)
return a.reduce(callback, initial); // 如果传入了一个初始值
else return a.reduce(callback); // 否则没有初始值
}
: funciton(a, callback, initial) {
var i = 0, len = a.length, accumulator;
if (arguments.length > 2) accumulator = initial;
else { // 找到数组中已定义的索引
if (len == 0) throw TypeError();
while(i < len) {
if (i in a) {
accumulator = a[i++];
break;
}
else i++;
}
if (i == len) throw TypeError();
}
// 对数组剩余的元素依次调用 callback
while (i < len) {
if (i in a)
accumulator = callback.call(undefined, accumulator, a[i], i, a);
i++;
}
return accumulator;
};

递归:函数自己调用自己

计算斐波那契数列第n项的值:1,1,2,3,5,8,13…

1
2
3
4
5
function fibonacci(n){
if(n==1 || n==2) return 1;
return fibonacci(n-1)+fibonacci(n-2);
}
for (var i = 0; i < 10; i++) { console.log(fibonacci(i+1)); }

递归计算阶乘

1
2
3
4
5
6
function factorial(n){
if(n<0) return 0; //为了防止报错
if(n==0) return 1; //递归的结束条件:0的阶乘为1
return factorial(n-1)*n;
}
for (var i = 0; i < 10; i++) { console.log("数字:"+i); console.log(factorial(i));}

m的n次方

1
2
3
4
5
6
function pow(n, m) {
if (m === 0) return 1;
if (m < 0) return 1 / (pow(n, -(m + 1)) * n);
else if (m > 0) return pow(n, m - 1) * n;
}
for (var i = -2; i <= 0; i++) { console.log(pow(2, i)); }

递归查找父元素

需求:要判断一个div是否在另一个div的下面

1
2
3
4
5
6
7
8
9
10
11
12
function find(child,parent){
//实现思路:由子元素一级一级的查找父元素
//递归的结束条件:查到了文档的根节点、找到了父元素
if(child.parentNode===parent) return true; //说明已经找到了符合条件的父元素
if(child.parentNode===null) return false; //说明已经查找到了文档的根节点
return find(child.parentNode,parent);
//第1次执行find——>child.parentNode===parent
//第2次执行find——>child.parentNode.parentNode===parent
//第3次执行find——>child.parentNode.parentNode.parentNode===parent
}
console.log(find(d3,d10));//false
console.log(find(d3,d1));//true
感谢您的支持!