一、闭包
闭包是连接函数外与函数内的桥梁,它是通过在函数内部再定义子函数来实现的,用户可以在函数外通过闭包访问函数的局部变量。
// 外层函数
function outside() {
// 外层函数的局部变量
var a = 250;
// 内层函数
function inside() {
return a;
}
// 将内层函数作为返回值
return inside;
}
var fn = outside();
console.log(fn()); // 打印输出250
由上述代码所示,全局变量fn指向了outside()执行后返回的inside函数,而inside函数又依赖ouside函数的局部变量a,所以outside函数的局部变量将一直驻于内存中,用户随时可以在函数外通过闭包来进行访问,直到fn的引用解除后。
二、封装
封装是面向对象的一个重要特性,它指将对象内部的数据进行隐藏,只提供相应的接口供用户调用。
JS语言中没有类似private的访问限制符,只能通过闭包来实现封装。
/**
* 定义一个构造函数,内部实现闭包操作
*/
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
this.getName = function () {
return this.name;
};
this.getAge = function () {
return this.age;
};
this.getSex = function () {
return this.sex;
}
}
/**
* 实例两个Person类对象
*/
var obj1 = new Person("张三", 18, "男");
var obj2 = new Person("李四", 16, "女");
console.log(obj1.getName()); // 成功打印"张三"
console.log(obj2.getAge()); // 成功打印16
三、工厂模式
将对象创建的实现细节都封装在一个函数内,让用户通过该函数进行对象的生成。
/**
* 用于创建对象的工厂方法
*/
function createObj(name, age, sex) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.sex = sex;
return obj;
}
var person = createObj("小明", 18, "男"); // 利用工厂方法创建了一个对象,并赋值给了变量person
console.log(person); // 成功打印person对象的信息
四、原型模式
JS语言中没有类(Class)的概念,它是通过构造函数(constructor)以及原型链(prototype chain)来实现面向对象的。
构造函数中定义的属性和方法是无法被实例所共享的,每一个实例都会重新开辟内存,再将构造函数的属性和方法“复制”进去。
/**
* 定义一个构造函数,定义Person类拥有speak方法
*/
function Person() {
speak = function () {
console.log("我会说话");
};
}
/**
* 实例两个Person类对象
*/
var obj1 = new Person();
var obj2 = new Person();
obj1.speak(); // 成功输出
obj2.speak(); // 成功输出
console.log(obj1.speak === obj2.speak); // 输出false
上述代码中,虽然obj1和obj2的speak方法行为和特征完全相同,但并不是同一个方法,它们分别占用了不同的内存空间,彼此互不关联。
为了解决对象数据共享的问题,JS中每一个函数都有一个prototype属性,这个属性对应着一个原型对象;每一个对象实例都有一个__proto__属性,它指向了构造函数的prototype属性。所以,在同一构造函数下生成的实例都会共享原型对象上的属性和方法
function Person() {
}
/**
* 调用Person构造器的prototype属性,并向其添加一个speak方法
*/
Person.prototype.speak = function () {
console.log("我会说话");
};
var obj1 = new Person();
var obj2 = new Person();
obj1.speak(); // 成功输出
obj2.speak(); // 成功输出
console.log(obj1.speak === obj2.speak); // 输出true
因为obj1和obj2都直接引用了Person.prototype上的speak方法,所以两个speak方法是同一个对象,本质完全相同。
四、原型继承链
在JS中,每个对象都继承自另一个原型对象,而原型对象自身作为一个对象也继承了它的原型对象,以此不断传递下去,形成了原型链。
一切继承关系的顶端是Object.prototype(实际上Object也有自己的原型对象——null,但通常忽略不谈)
如果父对象与子对象有重复的属性或方法,将以子对象的为准(即覆写)
/**
* 定义父类Person的构造函数
*/
function Person(name) {
this.name = name;
this.objNum = 1;
}
/**
* 向Person的原型对象添加speak方法
*/
Person.prototype.speak = function () {
console.log(this.name + "在说话");
}
/**
* 定义子类Student的构造函数
*/
function Student(name, grade) {
this.name = name;
this.grade = grade;
this.objNum = 2;
}
/**
* 将Student的prototype属性引向Person的一个实例,此时该实例将成为原型对象
* 再向原型对象添加一个study方法
*/
Student.prototype = new Person(this.name);
Student.prototype.study = function () {
console.log(this.grade + "年级的" + this.name + "在学习");
}
/**
* 实例两个Student类对象
*/
var stu1 = new Student("小明", 1);
var stu2 = new Student("小红", 2);
stu1.study(); // 1年级的小明在学习
stu1.speak(); // 小明在说话
console.log(stu1.objNum); // 2
stu2.study(); // 2年级的小红在学习
stu2.speak(); // 小红在说话
console.log(stu2.objNum); // 2
上述代码中,Person设计为父对象,Student设计为子对象,为了完成继承关系,我们将Person的对象实例赋给了Student.prototype
如此一来,Student的所有实例在读取属性和方法时,会先查找自身,如果自身没有找到的话,会继续查找Student.prototype,即new Person()
如果在Person中也没有找到目标,会继续向上查找Person.prototype,以此不断向上搜寻,直到抵达Object.prototype,如果还是没找到则返回undefined。
父类Person的原型属性中定义了speak方法,而子类Student没有speak方法,stu1和stu2在自身和原型对象上找不到speak方法时,就会向上到父类中去读取。(属性继承)
父类Person和子类Student都定义了一个objNum的属性,但stu1和stu2都只识别子类Student的属性值。(属性覆写)
五、对象冒充
通过原型链实现的继承关系,子类可以共享其链条上端所有类的的属性和方法,包括prototype内的属性和方法。
对象冒充也是一种继承的实现,它将父类的构造器作为子类构造器的属性,然后在子类构造器内调用该属性进行实例化,子类就能冒充父类对象,以此获得父类的属性和方法。(也可以使用call和apply方法完成)
要注意的是:在对象冒充中,子类只能共享父类自有的属性和方法(简称特权属性),无法共享prototype域(简称公有属性)
/**
* 定义父类Person的构造函数,添加一个speak方法
*/
function Person() {
this.speak = function () {
console.log("我会说话");
};
}
/**
* 向Person的prototype属性添加一个eat方法
*/
Person.prototype.eat = function () {
console.log("我会吃饭");
};
/**
* 定义子类Student的构造函数
* 将Person构造器当作属性进行存储,调用以完成对象冒充操作
* 此处亦可用call或apply方法
*/
function Student() {
this.father = Person;
this.father();
}
var stu = new Student();
stu.speak(); // 成功运行
stu.eat(); // 无法运行,提示eat未定义