JS面向对象精要(一)_原始类型和引用类型
JS面向对象精要(二)_函数
JS面向对象精要(三)_理解对象
JS面向对象精要(四)_构造函数和原型对象
JS面向对象精要(五)_继承
函数
js 中函数其实就是对象,函数不同于其他对象的决定性特点就是:函数存在一个被称为[[Call]]
的内部属性
内部属性无法通过代码访问而是定义了代码执行时的行为,ECMAScript 为 JavaScript 的对象定义了多种内部属性,这些内部属性都用双重中括号来标注。
[[Call]]
属性是函数独有的,表明该对象可以被执行。由于仅函数拥有该属性,ECMAScript 定义 typeof 操作符对任何具有[[Call]]
属性的对象返回“function”。这在过去曾经导致一些问题,因为某些浏览器曾经在正则表达式中包含[[Call]]
属性,导致后者被错误鉴别为函数。现在,所有的浏览器行为都一致,typeof 不会再将正则表达式鉴别为函数了。
我们探讨在 JavaScript 中定义和执行函数的各种方法。由于函数是对象,它们的行为和其他语言中函数的行为不同,理解函数的行为是理解 JavaScript 的核心
2.1 声明还是表达式
// 函数声明
function add(num1, num2) {
return num1 + num2;
}
// 函数表达式,function关键字后面不需要加上函数的名字。这种函数被称为匿名函数
var add = function(num1, num2) {
return num1 + num2;
};
函数声明会被提升至上下文(要么是该函数被声明时所在的函数的范围,要么是全局范围)的顶部。这意味着你可以先使用函数后声明它们。
var result = add(5, 5);
function add(num1, num2) {
return num1 + num2;
}
// how the JavaScript engine interprets the code
function add(num1, num2) {
return num1 + num2;
}
var result = add(5, 5);
JavaScript 能对函数声明进行提升,这是因为引擎提前知道了函数的名字。而函数表达式仅能通过变量引用,因此无法提升。
2.1 函数就是对象
你可以像使用对象一样使用函数,也可以将它们赋给变量,在对象中添加它们,将它们当成参数传递给别的函数,或从别的函数中返
function sayHi() {
console.log("Hi!");
}
sayHi(); // outputs "Hi!"
var sayHi2 = sayHi;
sayHi2(); // outputs "Hi!"
上面代码首先有一个函数声明 sayHi。然后有一个变量 sayHi2 被创建并被赋予 sayHi 的值。sayHi 和 sayHi2 现在指向同一个函数,两者都可以被执行,并具有相同结果。为了更好地理解这点,让我们来看一下用 Function 构造函数重写的具有相同功能的代码。
Function 构造函数更加清楚地表明 sayHi 能够像其他对象一样被传来传去。只要你记住函数就是对象,很多行为就变得容易理解了。
var sayHi = new Function('console.log("Hi!");');
sayHi(); // outputs "Hi!"
var sayHi2 = sayHi;
sayHi2(); // outputs "Hi!"
比如:你可以将函数当成参数传递给其他的函数。JavaScript 数组的 sort()方法接受一个比较函数作为可选参数。每当数组中两个值需要进行比较时都会调用比较函数。如果第一个值小于第二个,比较函数返回一个负数。如果第一个值大于第二个,比较函数返回一个正数。如果两个值相等,函数返回 0。
var numbers = [1, 5, 8, 4, 7, 10, 2, 6];
numbers.sort(function(first, second) {
return first - second;
});
console.log(numbers); // "[1, 2, 4, 5, 6, 7, 8, 10]"
//不传递比较函数,此时默认的比较函数将所有值都转换成字符串进行比较。
numbers.sort();
console.log(numbers); // "[1, 10, 2, 4, 5, 6, 7, 8]"
2.3 参数
JavaScript 函数的另一个独特之处在于你可以给函数传递任意数量的参数却不造成错误。那是因为函数参数实际上被保存在一个被称为 arguments 的类似数组的对象中
arguments 对象不是一个数组的实例,其拥有的方法与数组不同,Array.isArray(arguments)永远返回 false
另一方面,JavaScript 也没有忽视那些命名参数。函数期望的参数个数保存在函数的 length 属性中。还记得吗?函数就是对象,所以它可以有属性。Length 属性表明了该函数的期望参数个数。了解函数的期望参数个数在 JavaScript 中是非常重要的,因为给它传递过多或过少的参数都不会抛出错误
// 具有单一命名参数的reflect()函数, 于只有一个命名参数,length属性为1
function reflect(value) {
return value;
}
console.log(reflect("Hi!")); // "Hi!"
console.log(reflect("Hi!", 25)); // "Hi!"
console.log(reflect.length); // 1
//无命名参数的函数,length属性为0
reflect = function() {
return arguments[0];
};
console.log(reflect("Hi!")); // "Hi!"
console.log(reflect("Hi!", 25)); // "Hi!"
console.log(reflect.length); // 0
如上代码,第二个使用 arguments 对象的版本有点让人莫名其妙,因为没有命名参数,你不得不浏览整个函数体来确定是否使用了参数。这就是为什么许多开发者尽可能避免使用 arguments 的原因。
不过,在某些情况下使用 arguments 比命名参数更有效。例如,假设你想创建一个函数接受任意数量的参数并返回它们的和。因为你不知道会有多少个参数,所以你无法使用命名参数。在这种情况下,使用 arguments 是最好的选择。
function sum() {
var result = 0,
i = 0,
len = arguments.length;
while (i < len) {
result += arguments[i];
i++;
}
return result;
}
console.log(sum(1, 2)); // 3
console.log(sum(3, 4, 5, 6)); // 18
console.log(sum(50)); // 50
console.log(sum()); // 0
2.4 重载
大多数面向对象语言支持函数重载,它能让一个函数具有多个签名。函数签名由函数的名字、参数的个数及其类型组成。因此,一个函数可以有一个接受一个字符串参数的签名和另一个接受两个数字参数的签名。JavaScript 语言根据实际传入的参数决定调用函数的哪个版本
上面已经提到过,JavaScript 函数可以接受任意数量的参数且参数类型完全没有限制。这说明 JavaScript 函数其实根本没有签名,因此也不存在重载。看看当你试图声明两个同名函数会发生什么。
function sayMessage(message) {
console.log(message);
}
function sayMessage() {
console.log("Default message");
}
sayMessage("Hello!"); // outputs "Default message"
如果这是其他的语言,sayMessage(“Hello!”)就会输出“Hello!”。然而在 JavaScript 里,当你试图定义多个同名的函数时,只有最后定义的有效,之前的函数声明被完全删除,只使用最后那个。下面,让我们用对象来帮助理解
var sayMessage = new Function("message", "console.log(message);");
sayMessage = new Function('console.log("Default message");');
sayMessage("Hello!"); // outputs "Default message"
JavaScript 函数没有签名这个事实不意味着你不能模仿函数重载。你可以用 arguments 对象获取传入的参数个数并决定怎么处理
function sayMessage(message) {
if (arguments.length === 0) {
message = "";
}
console.log(message);
}
sayMessage(); // // outputs "HDefault message"
sayMessage("hello"); // // outputs "Hello!"
2.5 对象方法
第 1 章中介绍了可以在任何时候给对象添加或删除属性。如果属性的值是函数,则该属性被称为方法,你可以像添加属性那样给对象添加方法。例如,在下面代码中,变量 person 被赋予了一个对象的字面形式,包含属性 name 和方法 sayName。
var person = {
name:"Nicholas",
sayName:function() {
console.log(person.name);
}
};
person.sayName(); // outputs "Nicholas"
2.5.1 this 对象
注意到前面例子中一些奇怪之处。sayName()方法直接引用了 person.name,在方法和对象间建立了紧耦合。如果你改变变量名,你也必须要改变方法中引用的名字。其次,这种紧耦合使得同一个方法很难被不同对象使用
JavaScript 所有的函数作用域内都有一个 this 对象代表调用该函数的对象。在全局作用域中,this 代表全局对象(浏览器里的 window)。当一个函数作为对象的方法被调用时,默认 this 的值等于那个对象。所以你应该在方法内引用 this 而不是直接引用一个对象
// this改写上述案例
var person = {
name:"Nicholas",
sayName:function() {
console.log(this.name);
}
};
person.sayName(); // outputs "Nicholas"
// 例如:我们可以轻易改变变量名,甚至是将该函数用在不同对象上
function sayNameForAll() {
console.log(this.name);
}
var person1 = {
name:"Nicholas",
sayName:sayNameForAll
};
var person2 = {
name:"Greg",
sayName:sayNameForAll
};
var name = "Michael";
person1.sayName(); // outputs "Nicholas"
person2.sayName(); // outputs "Greg"
sayNameForAll(); // outputs "Michael"
2.5.2 改变 this
在 JavaScript 中,使用和操作函数中 this 的能力是良好地面向对象编程的关键。函数会在各种不同上下文中被使用,它们必须到哪都能正常工作。一般 this 会被自动设置,但是你可以改变它的值来完成不同的目标。有 3 种函数方法允许你改变 this 的值。(记住函数是对象,而对象可以有方法,所以函数也有。)
- call 方法
以指定的 this 值和参数来执行函数。call()的第一个参数指定了函数执行时 this 的值,其后的所有参数都是需要被传入函数的参数
function sayNameForAll(label) {
console.log(label + ":" + this.name);
}
var person1 = {
name:"Nicholas"
};
var person2 = {
name:"Greg"
};
var name = "Michael";
sayNameForAll.call(this, "global"); // 全局this "global:Michael"
sayNameForAll.call(person1, "person1"); // "person1:Nicholas"
sayNameForAll.call(person2, "person2"); // "person2:Greg"
- apply 方法
接受两个参数:this 的值和一个数组或者类似数组的对象,内含需要被传入函数的参数(也就是说你可以把 arguments 对象作为 apply()的第二个参数)。你不需要像使用 call()那样一个个指定参数,而是可以轻松传递整个数组给 apply()
function sayNameForAll(label) {
console.log(label + ":" + this.name);
}
var person1 = {
name:"Nicholas"
};
var person2 = {
name:"Greg"
};
var name = "Michael";
sayNameForAll.apply(this, ["global"]); // "global:Michael"
sayNameForAll.apply(person1, ["person1"]); // "person1:Nicholas"
sayNameForAll.apply(person2, ["person2"]); // "person2:Greg"
- bind 方法
bind()的第一个参数是要传给新函数的 this 的值。其他所有参数代表需要被永久设置在新函数中的命名参数。你可以在之后继续设置任何非永久参数
function sayNameForAll(label) {
console.log(label + ":" + this.name);
}
var person1 = {
name:"Nicholas"
};
var person2 = {
name:"Greg"
};
// 为person1创建一个函数
var sayNameForPerson1 = sayNameForAll.bind(person1);
sayNameForPerson1("person1"); // "person1:Nicholas"
// 为person2创建一个函数
var sayNameForPerson2 = sayNameForAll.bind(person2, "person2");
sayNameForPerson2(); // "person2:Greg"
// 将方法附加到对象不会更改“this”
person2.sayName = sayNameForPerson1;
person2.sayName("person2"); // "person2:Nicholas"
如上代码中,将 sayNameForPerson1()设置为 person2 的 sayName 方法。由于其 this 的值已经绑定,所以虽然 sayNameForPerson1 现在是 person2 的方法,它仍然输出 person1.name 的值。
总结
- JavaScript 函数的独特之处在于它们同时也是对象,也就是说它们可以被访问、复制和覆盖,就像其他对象一样。JavaScript 中的函数和其他对象最大的区别在于它们有一个特殊的内部属性[[Call]],包含了该函数的执行指令。typeof 操作符会在对象内查找这个内部属性,如果找到,它返回“function”。
- 函数的字面形式有两种:声明和表达式。函数声明是 function 关键字右边跟着函数名。函数声明会被提升至上下文的顶部。函数表达式可被用于任何可以使用值的地方,例如赋值语句、函数参数或另一个函数的返回值。
- 函数是对象,所以存在一个 Function 构造函数。你可以用 Function 构造函数创建新的函数,不过没有人会建议你这么做,因为它会使你的代码难以理解和调试。但是有时你可能不得不使用这种用法,例如在函数的真实形式直到运行时才能确定的时候。
- 为了理解 JavaScript 的面向对象编程,你需要好好理解它的函数。因为 JavaScript 没有类的概念,能够帮助你实现聚合和继承的就只有函数和其他对象了。