关于Javascript面向对象的一些整理

红皮书(Javascript高级程序设计)的第6章是关于面向对象中的构造函数的内容,看了第二遍,对一些重点做下笔记。

构造器模式

function Person(name, age, job) {    
    this.name = name;    
    this.age = age;    
    this.job = job;    
    this.sayName = function() {        
        console.log(this.name);    
    }
}


var person1 = new Person("johnson", "22", "frontend engineer")

按照惯例,构造函数的函数名总是以大写字母开头,非构造函数的普通函数则是以小写字母开头,我想这也是为了做个明显的区分。

使用new 函数名()的形式的操作就是调用构造函数,而那些没有new 操作符调用的就是普通调用

函数在ECMAScript中就是对象, 每当一个函数被定义,其实就是一个对象被初始化。如下代码:

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function("alert(this.name)"); //逻辑上等于 function() { alert(this.name) }
}

原型模式

每个函数被创建时都会有个原型属性,这个原型其实也就是个包含属性和方法的对象,同时也能被特殊引用类型的实例给访问。这个对象(prototype)也就是原型在构造器(constructor)被调用时就被创建了。

function Person() {}

Person.prototype.name = "Johnson";
Person.prototype.age = "22";
Person.prototype.job = "frontend engineer";
Person.prototype.sayName = function() {
    console.log(this.name);
};

var person1 = new Person();
person1.sayName(); //"Johnson"

var person2 = new Person();
person2.sayName(); //"Johnson"


console.log(person1.sayName === person2.sayName); //true

所有的原型自动会得到一个叫构造器(constructor)的属性并且这个属性指向这个原型在的这个函数。Person.prototype.constructor指向Person。

当定义一个自定义的构造器,原型就默认获得了constructor属性。其他的方法比如函数的toString方法就从Object继承(又是原型链)。每当用new操作符定义个新的实例(比如new Person()), 这个实例有一个内部指针指向这个构造器的原型。在ECMA-262第五版,也叫作[[prototype]]。如下图:

QQ图片20161002095607.png

当一个属性在对象上能够被读取,有一种搜索规则来找到这个属性。这个搜索最开始从对象自身开始搜索,如果找到这个属性名这个属性值就被返回了并且不会继续往原型上找,当在对象自身不存在这个属性,则会到原型对象上去找这个属性,找到就返回。

如果一个属性被加到对象实例上(带new 操作符+函数名的实例)它会屏蔽原型上的同名属性,也意味着它锁住了访问原型同名属性的通道。即使用set方法把这个属性设置成Null也只是设置在对象本身的属性,并没有恢复访问原型同名属性的通道。除非用delete方法删除这个实例上的属性才能够重新访问到原型上这个同名属性的值。

function Person() {}

Person.prototype.name = "Johnson";
Person.prototype.age = "22";
Person.prototype.job = "frontend engineer";
Person.prototype.sayName = function() {
    console.log(this.name);
};

var person1 = new Person();
var person2 = new Person();

person1.name = "Jonas";
console.log(person1.name); //Jonas
console.log(person2.name); //Johnson

delete person1.name;
console.log(person1.name); //Johnson
2O(3YUA6CQ$L4HF1`BSJ`$B.png

遍历对象的属性

  • for in
  • Object.keys()
  • Object.getOwnPropertyNames()

三者区别:

  • for in能够输出自身以及原型链上可枚举的属性
var parent = Object.create(Object.prototype, {
    a: {
        value: 1,
        writable: true,
        enumerable: true,
        configurable: true            
    }
});

var child = Object.create(parent, {
    b: {
        value: 2,
        writable: true,
        enumerable: true,
        configurable: true
    },
    c: {
        value: 3,
        writable: true,
        enumerable: false,
        configurable: true
    }
});

for (var key in child) {
    console.log(key); // b a
}
  • Object.keys()主要用来获取对象自身可枚举的属性键,返回的是键名的数组。
function Person() {}

Person.prototype.name = 'Johnson';
Person.prototype.age = 29;
Person.prototype.job = "frontend engineer";
Person.prototype.sayName = function() {
    console.log(this.name);
}

var keys = Object.keys(Person.prototype); //此处遍历Person的原型对象

console.log(keys); //"name, age, job,sayName" 输出原型对象的key

var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1keys = Object.keys(p1);
console.log(p1keys);  //"name, age" 此处只输出自身可枚举属性,不输出原型的属性
  • Object.getOwnPropertyNames()用来获取对象自身的全部属性,无论枚举不可枚举。
var keys = Object.getOwnPropertyNames(person.prototype);
console.log(keys);  //"constructor(不可枚举), name, age, job, sayName"

交替的原型句法

function Person() {}
Person.prototype = {
    name: "Johnson",
    age: 29,
    job: "frontend engineer",
    sayName: function() {
        console.log(this.name);
    }
}

var friend = new Person();
console.log(friend instanceof Object); //true
console.log(friend instanceof Person); //true
console.log(friend.constructor == Person); //false
console.log(friend.constructor == Object); //true

以上的复写Person原型对象等同于创建了一个新的对象。但是构造器(constructor)属性不再指向Person。按照前面说的,当一个函数被创建时,它的原型对象会被创建并且构造器属性会被自动分配到原型对象中。但以上的写法完全复写了默认的原型对象,意味着构造器属性等同于在一个新的对象中,而不是函数的原型对象中。(翻译过来有点拗口,可以这么理解,原本这个构造函数是指向函数本身的,复写之后指向的是全局Object)。

function Person() {}
Person.prototype = {
    constructor: Person,
    name: "Johnson",
    age: 29,
    job: "frontend engineer",
    sayName: function() {
        console.log(this.name);
    }
}

但我们也能够让这个原型对象中的constructor重新指回Person。但是记住以这种方式回复构造器(constructor)属性的指向会使得constructor属性变为可枚举(Enumerable)。原生的constructor属性默认是不可枚举的,所以你最好重新把它定义成不可枚举,这里要用到Object.defineProperty()方法:

function Person() {}
Person.prototype = {
    constructor: Person,
    name: "Johnson",
    age: 29,
    job: "frontend engineer",
    sayName: function() {
        console.log(this.name);
    }
}

var p1 = new Person();
for(var i in p1) {
    console.log(i); // constructor, name, age, job, sayName
}
//ECMAScript 5 only - restore the constructor
Object.defineProperty(Person.prototype, 'constructor', {
    enumerable: false,
    value: Person
});

var p2 = new Person();
for(var j in p2) {
    console.log(j); //name, age, job, sayName
}

原型的动态特质

function Person() {}

var friend = new Person();

Person.prototype.sayHi = function() {
    console.log('hi');
}

friend.sayHi(); //hi

基于之前在原型上查找属性的惯例,在任何地方对原型做出修改都能直接反映到实例上,即使实例化操作在原型修改之前。也是因为实例和原型之间的连接只是个简单的指针而不是拷贝,当sayHi()被调用时,实例上是第一次搜索,没搜到,就继续到原型上去搜索直到搜索到方法并返回。

[[Prototype]]指针在构造器被调用时被分配到,所以修改原型变成一个不同的对象会切断构造器和原始的原型对象之间的联系。要记住一点:** 实例有一个只指向原型的指针而不是构造器 **,思考如下:

function Person() {}

var friend = new Person();

Person.prototype = {
    constructor: Person,
    name: 'Johnson',
    age: 29,
    job: 'Frontend engineer',
    sayName: function() {
        console.log(this.name);
    }
};

friend.sayName(); //error

当friend.sayName()被调用时,会发生错误,因为friend指向的原型没有这个属性。如下图:

![U%6}O8T5]W5SC{WQ9YJ_C53.png](http://upload-images.jianshu.io/upload_images/1572265-592c3bfcbd49fa44.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

个人理解,实例化在重写原型之前,切断了实例和之前那个原型的连接,但看图来说,我认为是实例还是指向之前的原型,只不过函数本身指向了新的原型对象。**注意:重写原型在实例化之前,friend.sayName()方法还是执行的 **

寄生构造器模式

function Person(name, age, job) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function() {
        console.log(this.name);
    };
    return o;
}

var friend = new Person('johnson', 29, 'Frontend engineer');
friend.sayName(); //johnson
console.log(friend.name); //johnson
console.log(friend instanceof  Person); //false

寄生模式下,friend能够访问到函数内部对象的属性。只是构造器和实例之间没有任何联系,所以instanceof不生效。

耐用模式

function Person(name, age, job) {
    var o = new Object();
    o.sayName = function() {
        console.log(name);
    }
    
    return o;
}

var friend = Person('johnson', 29, 'Frontend engineer');
friend.sayName(); //johnson

原型链

一个简单的例子:

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

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

function SubType() {
    this.subproperty = false;
}

//inherit from SuperType
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function() {
    return this.subproperty;
};

var instance = new SubType();
console.log(instance.getSuperValue());

NOBT{}0NA%CM8)AXLAS4%96.png

值得注意的是,getSuperValue()方法依旧在SuperType.prototype对象上而property在SubType.prototype对象上。SubType的原型现在是SuperType的实例,所以属性property存在SubType的原型上。同时也注意instance.constructor指向SuperType,因为在SubType.prototype上的构造器属性被复写了。

调用instance.getSuperValue()经过3个步骤的搜索:

  1. 实例instance
  2. SubType.prototype
  3. SuperType.prototype
function SuperType() {
    this.property = true;
}

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

function SubType() {
    this.subproperty = false;
}

//inherit from SuperType
SubType.prototype = new SuperType();

var SuperTp = new SuperType();

console.log(SuperTp.getSuperValue()); //true

SubType.prototype.getSubValue = function() {
    return this.subproperty;
};

//override existing method
SubType.prototype.getSuperValue = function() {
    return false;
}
var instance = new SubType();
console.log(instance.getSuperValue()); //false

getSuperValue()方法被复写,原型上的的getSuperValue()方法会被遮蔽。当getSuperValue()在SubType实例上被调用,会调用复写的方法,但SuperType的实例还是会调用原型链上的方法。

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

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

function SubType() {
    this.subproperty = false;
}

//inherit from SuperType
SubType.prototype = new SuperType();

SubType.prototype = {
    getSubValue: function() {
        return this.subproperty;
    },
    someOtherMethod: function() {
        return false;
    }
}

var instance = new SubType();
console.log(instance.getSuperValue()); //error

SubType的原型被复写成一个新的对象实例,所以原型链断开,SubType和SuperType现在没任何联系。

构造器偷取

function SuperType() {
    this.colors = ['red', 'yellow'];
}
SuperType.prototype.sayColor = function() {
    console.log('1111');
};
function SubType() {
    SuperType.call(this);
}
var instance = new SubType();
instance.colors.push('black');
console.log(instance.colors);// ['red', 'yellow', 'black']
instance.sayColor() //error

var instance2 = new SubType();
console.log(instance2.colors); //['red', 'yellow']

** 记住函数只是在一个特殊上下文中执行代码的简单对象。**
apply和call方法能够被用作执行一个构造器在一个新创建的对象上。当构造器上的属性包含引用时不会造成共享,各个实例有自己的属性引用。好比以上的属性的数组。此模式还支持传递参数。

缺点:缺点在以上代码也很明显,就是被继承的函数上的原型方法不能够被继承的调用。

混合继承

混合继承结合了原型链和构造器偷取的优点。

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

SuperType.prototype.sayName = function() {
    console.log(this.name);
};

function SubType(name, age) {
    //inherit properties
    SuperType.call(this, name);
    
    this.age = age;
}

//inherit methods
SubType.prototype = new SuperType();

SubType.prototype.sayAge = function() {
    console.log(this.age);
}

var instance1 = new SubType('Nicholas', 29);
instance1.colors.push('black');
console.log(instance1.colors); //red, blue, green, black
instance1.sayName(); //Nicholas
instance1.sayAge(); //29

var instance2 = new SubType('Greg', 27);
console.log(instance2.colors); //red, blue, green
instance2.sayName(); //Greg
instance2.sayAge(); //27

这就解决了刚才构造器偷取不能访问被继承函数的原型方法的问题。并且最后的实例不会互相影响。

原型继承

var person = {
    name: 'Nicholas',
    friends: ['Shelby', 'Court', 'Van']
};
function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

var anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');

var yetAnotherPerson = object(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');

console.log(person.friends); //[ 'Shelby', 'Court', 'Van', 'Rob', 'Barbie' ]

新对象将person对象作为自己的原型。person.friends不仅被person共享而且也被anotherPerson和yetAnotherPerson共享。这个代码有两个person的克隆。

var person = {
    name: 'Nicholas',
    friends: ['Shelly', 'Court', 'Van']
};

var anotherPerson = Object.create(person, {
    name: {
        value: 'Greg'
    }
});
anotherPerson.friends.push('Rob');

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');

console.log(anotherPerson.name); //Greg
console.log(person.friends); //Shelly, Court, Van, Rob, Barbie

Object.create()方法 第一个参数作为原型对象,第二个参数则是附加的属性,最后返回一个新对象。记住按照这种模式属性中只要包含引用值,将会共享这个引用值,类似于原型方式。使用这种方法你不用去创建各种构造器。

寄生模式

function createAnother(original) {
    var clone = object(original);
    clone.sayHi = function() {
        console.log('hi);
    };
    return clone;
}

var person = {
    name: 'Nicholas',
    friends: ['Shelly', 'Court', 'Van']
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //hi

这种模式虽然能在无构造器和自定义类型下实现但也不是很高效。

最后我们来说一下:

寄生混合模式

此方法是对之前** 混合继承 **的一种改善,因为混合继承会调用2次superType的构造器。一次是创建subType.prototype,另一次是在subtype的构造器中。

C_5NSUK{{{YP6FF%U1U@(%R.png

当SubType构造器被调用,SuperType的构造器也被调用了。你会发现,有两个name和colors的集合属性,一个在instance实例上一个在SubType的原型上。
解决调用两次构造器的基本思想是代替调用supertype的构造器来分配给subType的原型。你只需要一个supetype的原型拷贝。

function inheritProtoype(subType, superType) {
    var prototype = Object(superType.prototype); //create object
    prototype.constructor = subType; //augment obect
    subType.prototype = prototype;   //assign object
}

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

SuperType.prototype.sayName = function() {
    console.log(this.name);
}

function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}

inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
    console.log(this.age);
}


var instance1 = new SubType('Nicholas', 29);
instance1.colors.push('black');
console.log(instance1.colors); //red, blue, green, black
instance1.sayName(); //Nicholas
instance1.sayAge(); //29

var instance2 = new SubType('Greg', 27);
console.log(instance2.colors); //red, blue, green
instance2.sayName(); //Greg
instance2.sayAge(); //27


寄生混合模式还是很高效的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容