最近自己在休假,打算闭门几天将《JavaScript高级程序设计》(第3版)这本良心教材再回顾一遍。目前自己进入前端领域两年多,现在重读并记录下这本教材的“硬”知识点 😊 。
函数没有重载
ECMAScript 函数不能像传统意义上那样实现重载。而在其他语言(如Java)中,可以为一个函数编写两个定义,只要这两个定义的签名(接受的参数类型和数量)不同即可[p66]。ECMAScript的类型是松散形的,没有签名,所以是没有重载的。
function load(num){
return num + 100;
}
function load(num,name){
return num + 200;
}
var result = load(100); // 300
# 后面的函数声明覆盖掉前面的函数声明
基本的数据类型
基本类型值指的是简单的数据段,而引用类型指那些可能由多个值构成的对象[p68]。这里指出来的基本的数据类型是说的es5的哈:Undefined
,Null
,Boolean
,Number
和String
。
传递参数
ECMAScript 中所有的函数的参数都是按值传递
的[p70]。也就是说,把函数外部的值复制给函数内部的参数,就是把值从一个变量复制到另一个变量一样。基本类型值的传递如同基本类型变量的复制一样,而引用类型值的传递,则如同引用类型变量的复制一样。下面分开例子介绍两种不同类型为什么是按值传递。
基本类型值
基本类型这个按值传递比较好理解,直接复制变量的值传递:
function addTen(num){
num += 10;
return num;
}
var count = 20;
var result = addTen(count);
console.log(result); // 30
console.log(count); // 20 ,没有变化哈
引用类型值
有些人认为引用类型的传参是按照引用来传的,那暂且认为他们的理解是正确的,那下面的示例结果怎么解析呢?
function setName(obj){
obj.name = '嘉明';
obj = new Object();
obj.name = '庞嘉明';
}
var person = new Object();
setName(person);
console.log(person.name); // '嘉明',为啥不是'庞嘉明'呢?
如果是按照引用传的话,那么新建的对象obj = new Object()
应该是指向堆内容的对象啊,那么改变它本有的name
属性值应该生效,然而并没有生效。所以它也是按值传递
滴。
函数声明与函数表达式
解析器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁[p111]。解析器会率先读取函数声明,并使其执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解析。
console.log(sum(10 , 10)); // 20
function sum(num1 , num2){
return num1 + num2;
}
console.log(sum(10 , 10)); //TypeError: sum is not a function
var sum = function(num1 , num2){
return num1 + num2;
}
apply和call
每个函数都包含两个非继承而来的方法:apply()和call()
。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值[116]。call和apply在对象中还是挺有用处的。
apply()方法和call()方法的作用是相同的,区别在于接收参数的方式不同。
apply
apply()方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组,这里的参数数组可以是Array的实例,也可以是arguments对象(类数组对象)。
function sum(num1 , num2){
return num1 + num2;
}
function callSum1(num1,num2){
return sum.apply(this,arguments); // 传入arguments类数组对象
}
function callSum2(num1,num2){
return sum.apply(this,[num1 , num2]); // 传入数组
}
console.log(callSum1(10 , 10)); // 20
console.log(callSum2(10 , 10)); // 20
call
call()方法接收的第一个参数和apply()方法接收的一样,变化的是其余的参数直接传递给函数。换句话说,在使用call()方法时,传递给函数的参数必须逐个列举出来。
function sum(num1 , num2){
return num1 + num2;
}
function callSum(num1 , num2){
return sum.call(this , sum1 , sum2);
}
console.log(callSum(10 , 10)); // 20
创建对象
虽然Object构造函数或者对象字面量都可以用来创建单个对象,但是这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。[p144]
工厂模式
工厂模式就是造一个模子产生一个个对象。
function createPerson(name , age ,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson('nicholas' , 29 , 'software engineer');
var person2 = createPerson('greg' , 27 , 'doctor');
工厂模式解决了创建多个相似对象的问题(解决创建对象时产生大量重复代码),但是没有解决对象识别的问题(即怎么知道一个对象的类型,是Person还是Animal啊)。
构造函数模式
下面使用构造函数创建特定类型的对象。这里是Person类型:
function Person(name , age , job){ // 注意构造函数的首字母为大写哦
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}
}
var person1 = new Person('nicholas' , 29 , 'software engineer');
var person2 = new Person('greg' , 27 , 'doctor');
alert(person1.constructor == Person); // true 可以理解为person1的创造者是Person,也就是对象的类型Person
在创建Person的新实例,必须使用new操作符。以这种方式调用构造函数实际上会经历以下4个步骤:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此this指向了这个新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
构造函数解决了重复实例话问题(也就是创建多个相似对象的问题)和解决了对象识别的问题。但是,像上面那样,person1和person2共有的方法,实例化的时候都创建了,这未免多余了。当然可以将共有的方法提取到外面,像这样:
function Person(name , age , job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
var person1 = new Person('nicholas' , 29 , 'software engineer');
var person2 = new Person('greg' , 27 , 'doctor');
将sayName提取出来,就成了全局的方法了,然而这里只有Person类创建对象的时候才使用到,这样就大才小用了吧,所以提取出来到全局方法这种操作不推荐。
原型模式
创建的每个函数都有一个prototype(原型)属性,这个属性就是一个指针,指向一个对象,而这个对象的用途就是包含可以由特定类型的所有实例共享的属性和方法。
function Person(){
}
Person.prototype.name = 'nicholas';
Person.prototype.age = 29;
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); // nicholas
var person2 = new Person();
person2.sayName(); // nicholas
console.log(person1.sayName == person2.sayName); // true
可以有关系图如下:
上面的Person.prototype不建议使用字面量来写Person.prototype={},虽让效果一样,但是这里重写了原本Person.prototype的对象,因此constructor属性会指向Ohject而不是Person。当然也是可以处理的啦,将指向指正确并指定'construtor'的枚举属性为enumerable: false
。
原型模式解决了函数共享的问题,但是也带了一个问题:实例化中对象的属性是独立的,而原型模式这里共享了。
组合使用构造函数模式和原型模式
创建自定义类型的最常见的方式,就是组合使用构造函数模式和原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性。
function Person(name , age ,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ['shelby' , 'court'];
}
Person.prototype.sayName = function(){
alert(this.name);
}
var person1 = new Person('nicholas' , 29 , 'software engineer');
var person2 = new Person('greg' , 27 , 'doctor');
person1.friends.push('van');
console.log(person1.friends); // 'shelby,court,van'
console.log(person2.friends); // 'shelby,court'
console.log(person1.friends === person2.friends); // false
console.log(person1.sayName === person2.sayName); // true
动态原型模式
其他的OO语言,比如java,创建对象的类中是包含了自身的属性、方法和共有的属性、方法,如下小狗的例子:
public class Dog{
int age;
public Dog(String name ){
this.age = age;
System.out.println('小狗的名字是: ' + name);
}
public void setAge(int age){
age = age;
}
public int getAge(){
System.out.println('小狗的年龄为: ' + age);
return age;
}
public static void main(String []args){
/* 创建对象 */
Dog dog = new Dog('tom');
/* 通过方法来设定age */
dog.setAge(2);
/* 调用另外一个方法获取age */
dog.getAge();
/* 也可以通过 对象.属性名 获取 */
System.out.println('变量值: ' + dog.age);
}
}
为了看起来是类那么一会事,动态原型模式把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。如下:
function Person(name , age ,job){
// 属性
this.name = name;
this.age = age;
this.job = job;
// 方法
if(typeof this.sayName != 'function'){
Person.prototype.sayName = function(){
alert(this.name);
}
}
}
var friend = new Person('nicholas' , 29 , 'software engineer');
friend.sayName();
寄生构造函数模式
在前面几种模式都不适应的情况下,可以用寄生构造函数模式(数据结构中就使用到哈),寄生构造函数模式可以看成是工厂模式和构造函数模式的结合体。其基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。
function Person(name , age , job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
}
return o;
}
var friend = new Person('nicholas', 29 , 'software engineer');
friend.sayName(); // nicholas
关于寄生构造函数模式,需要说明:返回的对象与构造函数或者与构造函数的原型属性直接没有什么关系;也就是说,构造函数返回的对象与构造函数外部创建的对象没有什么区别。为此,不能依赖instanceof操作符来确定对象类型。由于存在上面的问题,建议在可以使用其他模式的情况下,不要使用这种模式。