JavaScript面向对象知识点

文章目录

前言

最近都在恶补一些Javascript的基础知识,为转投Node.js作准备。一路以来,对Javascript这门语言都是停留在运用的层面上,或者零零散散地学习一些知识点,欠缺一个宏观的知识体系,导致虽然学了也用不了。希望后面的学习能够帮我重构自己的知识体系,建个索引,突破技术瓶颈!

这篇文章大部分参考了《Javascript高级程序设计》第3版第六章,也算是我自己的一个读书笔记。里面包含了我对书本上各个知识点的理解与分析过程。当然,我没有全部内容作阐述,如果你有兴趣,建议也购买这本书看看。

本文可能会有不正确的地方,还望各路大侠多多指正。

正文

Javascript没有类和接口,只有对象,而根据ECMAscript的定义,对象是

“无序属性的集合,其属性包含基本值,对象或者函数”。

1 理解对象

如前所说,对象是属性的集合,而属性包含了一系列内部特性,这些特性描述了属性的特征。由于这些特性是为了实现Javascript引擎用的,所以不能直接访问它们。特性用[[XXX]]表示,例如[[Enumerable]]。其中,有两种属性,数据属性与访问器属性。

  • 数据属性:[[Configurable]] [[Enumerable]] [[Writable]] [[Value]]
  • 访问器属性:[[Configurable]] [[Enumerable]] [[Get]] [[Set]]

我们可以通过Object的方法,对这些特性进行操作,包括定义一个属性Object.defineProperty(), 定义多个属性Object.defineProperties(), 读取属性特性Object.getOwnPropertyDescriptor()。

Ps

  1. 经测试,Node.js v0.10.12是支持这些方法的。如上文所说,这些特性是为了实现Javascript引擎用的,而这些特性支持Chrome,Chrome用的是V8,Node.js用的也是V8,那么Node.js就会支持的啦…
  2. 我认为这些属性特性丰富了属性的操作,权限的设置,setter/getter等,但系具体怎样去应用,有待研究。

2 创建对象

创建对象包括了几种经典的模式;工厂模式,构造函数模式,原型模式,混合构造函数/原型模式,动态原型模式,寄生构造函数模式等。

2.1 工厂模式

原理就是定义一个工厂函数,函数内部创建一个Object实例,然后向实例添加属性与方法,最后返回这个实例。这个模式解决了创建相似对象的问题,但没有对象识别的功能,创建的每个对象都只是Object。

2.2 构造函数模式

原理是像原生的构造函数Object, Array一样,创建自定义的构造函数,从而自定义对象的属性与方法。与工厂模式相比,构造函数模式,没有显示创建Object实例,而且可以标识对象类型。

function Person(name){
    this.name = name;
    this.sayName = function(){
        alert(this.name);
    }
}
var person1 = new Person("King");

定义构造函数注意的地方是: 函数名首字母大写; 实例化对象时要用new操作符;以这种方式调用构造函数,实际运行4个步骤:

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

问题:

1 将构造函数当作函数使用。

我们注意到,在构造函数中,我们会为this添加属性,因此如果我们将构造函数当作函数使用时,会有污染全局变量的风险!因为,this指向的是调用它的对象。在全局作用域下,这个对象是Global对象(浏览器的是window对象)。如上述例子,直接调用构造函数,则为全局对象添加了name和sayName方法这两个属性。

2 每个实例都会重复创建函数。

虽然每个对象拥有同名的方法,但其实每个对象实例的方法都是不同的Function实例。我们可以将this.sayName = function(){…}想象成this.sayName = new Function(“…”),这两个表达式逻辑上是等价的。

2.3 原型模式

原理是使用函数的prototype(原型)属性,这个属性是一个指向原型对象的指针,原型对象包含了特定类型的所有实例共享的属性和方法(定义在prototype上的属性和方法)。

function Person(){}
Person.prototype.name = "";
Person.prototype.sayName = function(){
    alert(this.name);
}
var person1 = new Person();
person1.name = "King";
person1.sayName();

理解原型模式,首要是理解原型对象。 当创建一个函数的时候,就会根据特定的规则为该函数创建一个prototype的属性;该属性指向函数的原型对象,原型对象默认拥有属性constructor属性,指向原型对象所属的构造函数。每个对象实例,都会有个一个内部属性[[prototype]]指向对象原型。

对象与对象原型

因此,当实例添加了与原型的同名属性,则会覆盖原型中的属性,除非使用delete操作符删除该实例属性。我们可以用hasOwnProperty(),或者in操作符来确定属性的归属。 hasOwnProperty() 方法,可以知道属性是否属于对象实例;如: person1.hasOwnProperty(“name”); //true in 操作符,可以知道属性是否存在于对象,无论是存在于原型中还是实例中;如:"name" in person1; //true 结合这两个方法,知道属性是否属于原型对象;如:! person1.hasOwnProperty(“name”) && “name” in person1 //false

问题:

1 原型动态性。

每当我们调用对象的属性时,首先会在实例中搜索,如果没有,将在原型中搜索。因此,往原型对象上添加属性,也会即刻可以在实例中体验;即使先创建实例再修改原型,也能直接在实例调用新添加的属性。这一过程也被称为极晚绑定。

var person1 = new Person(); Person.prototype.sayHi = function(){alert(“Hi”);}; person1.sayHi(); //Hi

但是,如果我们重写原型对象,情况就不一样了。我们知道,每个实例会有一个内部属性[[prototype]]指向对象的原型,而将原型修改为另外一个对象,相当于切断了对象与最初原型的联系。

var person1 = new Person();
Person.prototype = {
    sayHi: function(){alert("Hi")};
} 
person1.sayHi(); //error

2 引用类型的原型属性。

原型具有共享属性的功能,而引用类型的赋值,这个值保存的是指向堆栈的指针。因此,当设置了原型属性为引用类型的值,这个值将共享于所有实例。

Person.prototype.friends = ["King1", "King2"];
var person1 = new Person();
var person2 = new Person();
person1.friends.push("King3");
console.log(person2.friends); //King1,King2,King3

3 不能为构造函数传递参数。

2.4 混合构造函数/原型模式

原理组合构造函数与原型模式,构造函数定义实例属性,原型定义方法和共享属性。

function Person(name){
    this.name = name;
    this.friends = ["King1", "King2"];
}
Person.prototype.sayName = function(){
    alert(this.name);
}
var person1 = new Person("King");

2.5 动态原型模式

原理是使用单例模式,把向原型添加方法的操作包装在构造函数内,又不重复定义。目的就是将对象的所有信息封装在构造函数中。

function Person(name){
    this.name = name;
    this.friends = ["King1", "King2"];
    if("function" != typeof "sayName")
    {
        Person.prototype.sayName = function(){
            alert(this.name);
        }
    }
}

问题:1 不能重写原型。

前面解释过,重写原型将会切断实例与新原型的联系。所以对象的第一个实例将是错误的。

 function Person(name){
    this._inited = false;
    this.name = name;
    this.friends = ["King1", "King2"];
    if( ! this._inited)
    {
        Person.prototype = {
            sayName: function(){alert(this.name);}
        }
    }
}

2.6 寄生构造函数模式

除了使用new操作符创建实例和包装函数称为构造函数外,与工厂模式一模一样。我们可以使用此模式在一些特殊的情况下为对象创建构造函数。

function SpecialArray(){
    var values = new Array();
    values.push.apply(this, arguments);
    values.toPipedString = function(){
        return this.join("|");
    }
    return values;
}
var colors = new SpecialArray("bule","red","yellow");
 colors.toPipedString(); //bule|red|yellow

问题: 1 不能依赖instanceof操作符来确定对象类型。

3 继承

相对于创建对象,继承对象也有一系列方法。

3.1 原型链

原理是利用原型让一个引用类型继承另一个引用类型的属性和方法。我们知道,每个实例都会有一个指向原型对象的内部指针,假如我们将一个引用类型的原型等于另一个引用类型的实例,此时该引用类型将包含一个指向另一个引用类型原型对象的内部指针,从而可以访问另一个对象的以及其原型对象的属性与方法。

function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperTypeValue = function(){
    return this.property;
}

function SubType(){
    this.property = false;
}
SubType.prototype = new SuperType();
SubType.prototype.getSubTypeValue = function(){
    return this.properrty;
}

问题: 1 超类型构造函数定义的引用类型属性会成为子类型的引用类型原型属性。

function SuperType(){
    this.colors = ["blue", "red", "yellow"];
}
...
SubType.prototype = new SuperType();

var s1 = new SubType();
var s2 = new SubType();
s1.colors.push("green");
s2.colors; //blue,red,yellow,green

2 传递参数。 创建子类型实例时无法向超类型构造函数传递参数。

3.2 对象冒充

原理是在子类型构造函数内部以子类型上下文调用超类型构造函数,从而解决了原型链方法的两个问题。

 function SuperType(){
    this.property = true;
}
...
function SubType(){
    SuperType.apply(this, arguments); //也可以用call方法
}

#####问题: #####1 超类原型定义的方法不可见。 由于只是借调构造函数,而不是使用new操作符创建超类型的实例,所以就没有超类型原型对象的关联。

3.3 组合继承

原理是同时使用原型链与对象冒充方法,以避免单一使用这两种方法时出现的缺陷。

#####问题: #####1 调用了两次超类型的构造函数。 #####2 子类型的原型对象包含了多余的超类型实例属性。

3.4 寄生组合式继承

原理是子类型构造函数使用对象冒充,从而获得超类型的构造函数的初始化属性;对于超类型的原型,子类型只需其原型副本,而不需再次执行构造函数。

function SuperType(){
    this.property = true;
}
SuperType.prototype.getProperty = function(){
    return this.property;
}

function SubType(){
    SuperType.apply(this, arguments);
}

//复制超类型prototype副本
SubType.prototype = inheritPrototype(SuperType.prototype);

function inheritPrototype(obj){
    function F(){};
    F.prototype = obj;
    return new F;
}

这里可能有点绕,为什么要用一个object函数来复制超类型的副本呢?不能够直接

SubType.prototype = SuperType.prototype

吗?absolutely no ! 试想一下,如果直接将超类型直接赋予子类型的prototype,那么子类型的prototype即也指向超类型的原型对象;如果修改子类型原型,那么超类型都会受到影响(修改的是同一个原型对象)。所以,我们要想一个方法只获得超类型的副本。

object函数其实也是一种继承方法,被称为原型式继承。它的原理是借助原型基于已有的对象创建一个新对象。我们尝试分析上述例子的关系。

寄生组合继承对象关系

如图所示,SuperType对象包含一个指向其原型对象的指针;在执行inheritPrototype函数时,我们创建了一个名为F的构造函数;在第二行代码,我们将函数传入的参数赋予F的原型(此时参数为SuperType的原型对象);最后返回一个F的实例,而这个实例包含一个指向SuperType原型对象的内部指针;我们将返回的F实例赋予SubType的原型,因此SubType将继承了SuperType的原型属性与方法。而且,当我们修改SubType的原型时,实际上是在F的实例上添加属性和方法,对于SuperType来说是没有影响的。

####最后再梳理一下各个知识点的脉络

#####对象

  • 属性
  • 数据特性
  • 访问器特性

#####创建对象

  • 工厂模式
  • 构造函数模式
  • 原型模式
  • 组合模式

#####对象继承

  • 原型链继承
  • 对象冒充
  • 寄生组合继承

 

评论已关闭。