前端基础知识整理(四)

本文详细探讨了JavaScript的面向对象编程,包括理解JS对象、创建对象的各种模式(工厂模式、构造函数模式、原型模式等)以及继承的方式(原型链、借用构造函数、组合继承等)。此外,还详细阐述了对象属性的特性以及如何使用Object.defineProperty()定义属性。
摘要由CSDN通过智能技术生成

js面向对象的程序设计

js对象

理解js的对象

面向对象(Object-Oriented,OO)的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。但是,ECMAScript 中没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”

js对象的属性

ECMAScript 中有两种属性:数据属性和访问器属性。
数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4 个描述其行为的特性。

  • [[Configurable]]:表示能否通过delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为true。
  • [[Enumerable]]:表示能否通过for-in
    循环返回属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为true。
  • [[Writable]]:表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为true。
  • [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为undefined。

访问器属性不包含数据值;它们包含一对儿getter 和setter 函数(不过,这两个函数都不是必需的)。

  • [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性的默认值为true。

  • [[Enumerable]]:表示能否通过for-in 循环返回属性。对于直接在对象上定义的属性,这个特性的默认值为true。

  • [[Get]]:在读取属性时调用的函数。默认值为undefined。

  • [[Set]]:在写入属性时调用的函数。默认值为undefined。

访问器属性不能直接定义,必须使用Object.defineProperty();

对于对象属性的定义,使用Object.defineProperty(),Object.defineProperties()(定义多个属性)

针对object属性的详细解读,可以参考https://msdn.microsoft.com/zh-cn/library/dn342818(v=vs.94).aspx
里面有自动翻译的版本,英语好的同学可以自行切换成原文。

创建js对象

工厂模式

首先,不要用java或C++的工厂设计模式来和这个混淆,这里的工厂只是使用了工厂设计模式的理念——统一管理对象的创建过程。
如下面的例子:

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");

但是这个模式有个缺点,就是无法解决对象识别的问题

构造函数模式

例子如下:
首先回顾一下js的constructor属性。每个函数都有一个constructor属性,这个属性指向创建自己的函数(构造函数)。看如下的例子。

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");

这个例子中如果在控制台打印person1.constructor,返回值是什么?返回的就是Person函数。

要创建Person 的新实例,必须使用new 操作符。以这种方式调用构造函数实际上会经历以下4个步骤:

  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(因此this 就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象。

任何函数,只要通过new 操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new 操作符来调用,那它跟普通函数也不会有什么两样。

例如,前面例子中定义的Person()函数可以通过下列任何一种方式来调用。

// 当作构造函数使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); //"Nicholas"
// 作为普通函数调用
Person("Greg", 27, "Doctor"); // 添加到window
window.sayName(); //"Greg"
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。
构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。

原型模式

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

首先要理解prototype。这里推荐大家看看这篇文章
http://blog.csdn.net/zhaozheng7758/article/details/7760433?utm_source=tuicool

理解过后直接来看例子:

function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true

这就是原生模式。
原生模式的问题在于,对于引用数据类型而言,一个类的改变会影响到其他类(因为都是共享的)

组合使用构造函数模式和原型模式

顾名思义,集合了两者的优点,避免了两者的缺点

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
}
Person.prototype = {
    constructor : Person,
    sayName : function(){
        alert(this.name);
    }
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true

这也是目前使用最广泛,认同度最高的写法。

动态原型模式

对于java和C++程序员来说,混合模式还是很别扭,毕竟要把对象的属性声明放到外面。而动态原型模式就把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。

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 SpecialArray(){
    //创建数组
    var values = new Array();
    //添加值
    values.push.apply(values, arguments);
    //添加方法
    values.toPipedString = function(){
        return this.join("|");
    };
    //返回数组
    return values;
}
var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); //"red|blue|green"

其实除了使用了new 以为,和工厂模式是一样的,这个函数的作用就是新建一个特殊的数组,而又没有影响原来的数组。
但是返回的对象与构造函数或者与构造函数的原型属性之间没有关系。因此不能依赖instanceof 操作符来确定对象类型。

稳妥构造函数模式

。所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this 和new),或者在防止数据被其他应用程序(如Mashup程序)改动时使用。

function Person(name, age, job){
    //创建要返回的对象
    var o = new Object();
    //可以在这里定义私有变量和函数
    //添加方法
    o.sayName = function(){
        alert(name);
    };
    //返回对象
    return o;
}

注意,在以这种模式创建的对象中,除了使用sayName()方法之外,没有其他办法访问name 的值。
可以像下面使用稳妥的Person 构造函数。

var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"

关于安全性方面的简介,大家可以参考一下http://www.cnblogs.com/justinw/archive/2010/07/07/1772836.html这篇博客

继承

说到面向对象,那么就不得不提到继承,首先js是没有java和C++那种接口机制的,所以无法使用接口继承,只能使用实现继承。

原型链

大致实现方式如下

function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};
function SubType(){
    this.subproperty = false;
}
//继承了SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
    return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true

原型链方法也存在很多问题,比如如果在定义了父类的方法后,子类定义同名方法会覆盖,使用字面量重新定义会破坏原型链,也存在着原型模式中的引用类型全局影响的问题。

借用构造函数

通过使用apply()和call()方法在(将来)新创建的对象上执行构造函数。

function SuperType(name){
    this.name = name;
}
function SubType(){
    //继承了SuperType,同时还传递了参数
    SuperType.call(this, "Nicholas");
    //实例属性
    this.age = 29;
}
var instance = new SubType();
alert(instance.name); //"Nicholas";
alert(instance.age); //29

如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的。

组合继承

指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
};
function SubType(name, age){
    //继承属性
    SuperType.call(this, name);
    this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript 中最常用的继承模式。

原型式继承
var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"

如果只是一些简单的场景,可以考虑使用原型式继承,但是问题从例子中也是显而易见的。

寄生式继承
function createAnother(original){
    var clone = object(original); //通过调用函数创建一个新对象
    clone.sayHi = function(){ //以某种方式来增强这个对象
        alert("hi");
    };
    return clone; //返回这个对象
}
var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"

这个例子中的代码基于person 返回了一个新对象——anotherPerson。新对象不仅具有person的所有属性和方法,而且还有自己的sayHi()方法。
使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。

寄生组合式继承

前面说过,组合继承是JavaScript 最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

寄生组合式继承的基本模式如下所示:

function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype); //创建对象
    prototype.constructor = subType; //增强对象
    subType.prototype = prototype; //指定对象
}
function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
};
function SubType(name, age){
    SuperType.call(this, name);
    this.age = age;
}
inheritPrototype(SubType, SuperType);//替换原型赋值语句
SubType.prototype.sayAge = function(){
    alert(this.age);
};

开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值