资讯专栏INFORMATION COLUMN

JavaScript面向对象的程序设计——“创建对象”的注意要点

tracymac7 / 2455人阅读

摘要:所以,可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。如对于构造函数原型属性以及实例之间的关系,参见高级程序设计一书中第章节。稳妥构造函数模式稳妥对象,指的是没有公共属性,且其方法也不引用的对象如

创建对象

Object 构造函数或对象字面量都可以用来创建单个对象。但这个方法的缺点非常明显:同一个接口创建很可耐多对象会产生大量的重复代码。为了解决这个问题,人们开始使用工厂模式的一种变体。

工厂模式(摒弃,不推荐)

这个模式没有解决对象识别的问题(即怎样知道一个对象的类型)。如:

具体的创建单个对象:

var person = {};
person.name = "Oliver";
person.age = 18;
person.sayName = function(){
    return this.Name;
};

改变成工厂模式:

function createPerson(name,age){
    var obj = {};
    obj.name = name;
    obj.age = age;
    obj.sayName = function(){
        return this.name
    };
    return obj; //注意这里要返回obj 对象,这样才能把obj 对象传给createPerson 变量。
}

var newPerson = createPerson("Oliver",18);
构造函数模式

构造函数可以创建特定类型的对象。所以,可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。如:

function Person(name,age){ //注意大小写,构造函数应该把第一个字幕大写化
    this.name = name;
    this.age = age;
    this.sayName = function (){
        return this.name;
    };
}

var newPerson = new Person("Oliver",18);

document.write(newPerson.name); //Oliver
document.write(newPerson.age); //18
document.write(newPerson.sayName()); //Oliver

确实相当方便

这里一定要记住,构造函数都应该以一个大写字母开头,用来区分其他的函数

又如:

function Person(name,age){ //注意大小写,构造函数应该把第一个字幕大写化
    this.name = name;
    this.age = age;
    this.sayName = function (){
        return this.name;
    };
}

var person1 = new Person("Oliver",18);
var person2 = new Person("Troy",24);

document.write(person1.constructor == Person); //true
document.write(person2.constructor == Person); //true

这里的person1 和person2 分别保存着Person 的一个不同的实例。两个对象都有一个constructor(构造函数)属性,该属性指向Person。

在上面这个例子中,person1 和person2 即是Object 的实例,同时也是Person 的实例。可以通过instanceof 操作符来验证:

console.log((person1 instanceof Object) && (person2 instanceof Person)); //true

以这种方式创建构造函数是定义在Global 中的(window 对象)

将构造函数当做函数

任何函数,只要通过new 操作符来调用,那它就可以座位构造函数;而任何函数,如果不通过new 操作符来调用,那它就跟普通函数没区别。如下面这个构造函数:

function Car(name,color,sound){
    this.name = name;
    this.color = color;
    this.sound = function(){
        return sound;
    };
    console.log(this.name + " " + this.color + " " + this.sound());
}

如果当做构造函数来使用:

var benz = new Car("C200","White","Boom Boom"); //C200 White Boom Boom

如果作为普通函数来调用:

Car("Benz","White","Boom!"); //Benz White Boom!
console.log(window.name + window.color + window.sound()); //BenzWhiteBoom!

如果在另一个对象的作用域中调用:

var cars = {};
Car.call(cars,"Benz","White","Boom Boom!");
document.write(cars.sound()); //Boom Boom!
构造函数的问题

问题是每个方法都要在每个实例是重新创建一遍。可用通过把内部的函数转移到外部来解决这些问题。如:

function Car(name,color){
    this.name = name;
    this.color = color;
    this.show = show;
}
function show(){
    console.log(this.name + this.color);
}
var benz = new Car("Benz","white");
benz.show(); //Benzwhite

但这个问题是完全没有了封装性可言。不过可以通过原型模式来解决。

原型模式
function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.age = 18;
Person.prototype.sayName = function(){
    console.log(this.name);
};
var person1 = new Person();
person1.sayName(); //Oliver
var person2 = new Person();
person2.sayName(); //Oliver;
console.log(person1.sayName == person2.sayName); //true

与构造函数不同的是,新对象的这些属性和方法是由所有实例共享的。这里两个新的person 访问的都是同一组属性和同一个sayName() 函数。

理解原型对象

以上面的Person 为例,Person 构造函数里面存在一个prototype 属性,这个属性指向原型对象Person Prototype,该Person Prototype 里面包含了constructor 属性,该属性又指向构造函数Person。构造函数的实例包含了一个[[Prototype]]的内部属性,该内部属性则指向Person Prototype。如:

function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.age = 18;
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
person1.sayName(); //Oliver

var person2 = new Person();
person2.sayName(); //Oliver;

console.log(Person.prototype);
/*
age: 18
constructor: function Person() {}
name: "Oliver"
sayName: function () {
__proto__: Object
*/
console.log(Person.prototype.constructor);
//function Person() {}
console.log(Object.getPrototypeOf(person1));
/*
age: 18
constructor: function Person() {}
name: "Oliver"
sayName: function () {
__proto__: Object
*/

对于构造函数、原型属性以及实例之间的关系,参见《js高级程序设计》一书中第6.2.3 章节。

两个方法:isPrototypeOf()Object.getProtytypeOf()(ECMAScript 5)。前者是用来确定[[Prototype]];后者是用来返回[[Prototype]]值。如:

console.log(Person.prototype.isPrototypeOf(person1)); //true
console.log(Object.getPrototypeOf(person1).name); //Oliver

console.log(Object.getPrototypeOf(person1));
/*
age: 18
constructor: function Person() {}
name: "Oliver"
sayName: function () {
__proto__: Object
*/

为对象添加一个属性时,这个属性会屏蔽原型对象中的同名属性,但是并不会修改那个属性。如果使用delete 删除这个属性,就可以重新访问原型中的属性。如:

function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.age = 18;
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
person1.sayName(); //Oliver 原型中的Name

person1.name = "Troy";
person1.sayName(); //Troy 实例中的Name

delete person1.name;
person1.sayName(); //Oliver 原型中的Name

每次读取某个对象的某个属性,都会从实例本身开始搜索,如果没有找到给定名字的属性,则会在原型对象中再次搜索。

方法hasOwnProperty()检测属性如果在对象实例中时,返回true。如:

console.log(person1.hasOwnProperty("age")); //false age属性来自于原型
console.log(person1.hasOwnProperty("name")); //true name属性来自于实例
原型与in 操作符

两种方法使用in 操作符:多带带使用和for-in 循环中使用。

多带带使用时,in 返回true 说明该属性存在于实例或原型中。如:

function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.age = 18;
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
person1.name = "Troy";
person1.sayName(); //Troy 实例中的Name

console.log("name" in person1); //true name属性在实例或原型中
console.log(person1.hasOwnProperty("name")); //true name属性在实例中
//上面两个就说明name属性一定在实例中

又如:

function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.age = 18;
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
person1.name = "Troy";
person1.sayName(); //Troy 实例中的Name

var person2 = new Person();

console.log("name" in person1); //true name属性在实例或原型中
console.log(person1.hasOwnProperty("name")); //true name属性在实例中
//上面两个就说明name属性一定在实例中

console.log("name" in person2); //true
console.log(person2.hasOwnProperty("name")); //false
//上面两个说明name属性一定在原型中

自定义一个函数hasPrototypeProperty(object,name);即同时使用上面两个方法来确定属性到底是不是存在于实例中。如:

function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.age = 18;
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
person1.name = "Troy";
person1.sayName(); //Troy 实例中的Name

var person2 = new Person();

function hasPrototypeProperty(object,name){
    console.log((name in object) && !(object.hasOwnProperty(name)))
}

hasPrototypeProperty(person2,"name"); //true name属性是在原型中
hasPrototypeProperty(person1,"name"); //false name属性是在实例中

Object.defineProperty()方法定义的属性:

function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
Object.defineProperty(person1, "age", {
    value: 18
})

console.log(person1.hasOwnProperty("age")); //true age属性是实例属性

关于for-in、[[enumerable]]、defineProperty、hasOwnProperty 的例子:

var person1 = {
    age: 18
};
Object.defineProperty(person1, "name", {
    value: "Oliver",
    enumerable: true
})
for(var x in person1){
    console.log(x);
}
console.log(person1.hasOwnProperty("name")); //true

又如:

function Person(age){
    this.age = age;
}
var person1 = new Person(18);
Object.defineProperty(person1, "name", {
    value: "Oliver",
    enumerable: false
})
for(var x in person1){
    console.log(x); //用defineProperty 定义的name 属性是实例属性,这里不会枚举到
}
console.log(person1.hasOwnProperty("name")); //true

又如:

function Person(){};
Person.prototype.age = 18;
var person1 = new Person();
Object.defineProperty(person1, "name", {
    value: "Oliver",
    enumerable: false
})
for(x in person1){
    console.log(x); //这里仍然不回枚举到自定义的name 实例属性
}

但是:

function Person(){};
Person.prototype.age = 18;
Person.prototype.name = "Oliver";
var person1 = new Person();
Object.defineProperty(person1, "name", {
    enumerable: false
})
for(x in person1){
    console.log(x); //这里则返回枚举到自定义的name 原型属性
}

原型属性的[[enumerable]]设置为false,调用for-in 仍然可以被枚举到。

另外,Object.keys()方法可以返回所有可枚举属性的字符串数组:

function Person(){};
Person.prototype.age = 18;
Person.prototype.name = "Oliver";

var person1 = new Person();
Object.defineProperty(person1, "sound", {
    value: "miao~",
    enumerable: true //可枚举
});
Object.defineProperty(person1, "sound2", {
    value: "wang~",
    enumerable: false //不可枚举
});

console.log(Object.keys(Person.prototype)); //["age", "name"]
console.log(Object.keys(person1)); //["sound"]

Object.getOwnPropertyName()方法,则可以返回无论可否枚举的所有实例属性:

function Person(){};
Person.prototype.age = 18;
Person.prototype.name = "Oliver";

var person1 = new Person();
Object.defineProperty(person1, "sound", {
    value: "miao~",
    enumerable: true //可枚举
});
Object.defineProperty(person1, "sound2", {
    value: "wang~",
    enumerable: false //不可枚举
});

console.log(Object.keys(Person.prototype)); //["age", "name"]
console.log(Object.keys(person1)); //["sound"]
console.log(Object.getOwnPropertyNames(Person.prototype)); //["constructor", "age", "name"]
console.log(Object.getOwnPropertyNames(person1)); //["sound","sound2"]
更简单的原型语法

即字面量方法:

function Person(){};
Person.prototype = {
    name: "Oliver",
    age: 18,
    sayName: function(){
        console.log(this.name);
    }
};

var person1 = new Person();
console.log(Person.prototype.constructor); //不再指向Person()构造函数

function People(){};
People.prototype.name = "Troy";
People.prototype.age = 26;
People.prototype.sayName = function(){
    console.log(this.name);
};

var people1 = new People();
console.log(People.prototype.constructor); //这里则指向People()构造函数

上面第一种就是字面量方法。但是由此带来的问题是,他的原型对象中的constructor 属性将不再指向上个例子中的Person() 构造函数了。(其实我们本质上是重写了prototype对象)

如果constructor 值真的非常重要,则只需要把它设置回适当的值就可以了:

function Person(){};
Person.prototype = {
    constructor: Person,
    name: "Oliver",
    age: 18,
    sayName: function(){
        console.log(this.name);
    }
};

var person1 = new Person();
console.log(Person.prototype.constructor); //重新指向Person()构造函数

function People(){};
People.prototype.name = "Troy";
People.prototype.age = 26;
People.prototype.sayName = function(){
    console.log(this.name);
};

var people1 = new People();
console.log(People.prototype.constructor); //这里则指向People()构造函数

然而用字面量的方法导致的问题仍然没有结束,以上面这种方式重设constructor 属性会导致[[Enumerable]]特性被设置为true。因此在支持ECMAScript 5 的js 引擎中可以用Object.defineProperty()方法把它修改为false:

function Person(){};
Person.prototype = {
    constructor: Person,
    name: "Oliver",
    age: 18,
    sayName: function(){
        console.log(this.name);
    }
};

var person1 = new Person();
console.log(Person.prototype.constructor);
for (var x in person1){
    console.log(x); //这里会出现constructor,但是我们实际上不应该让他能够被枚举出
}

Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false
});

for (var x in person1){
    console.log(x); //这里就不会出现constructor 了,但是这种方法只支持ECMAScript 5的js 引擎
}

/*
[Log] function Person() {} (repetition.html, line 130)
[Log] constructor (repetition.html, line 132)
[Log] name (repetition.html, line 132)
[Log] age (repetition.html, line 132)
[Log] sayName (repetition.html, line 132)
[Log] name (repetition.html, line 140)
[Log] age (repetition.html, line 140)
[Log] sayName (repetition.html, line 140)
*/
原型的动态性

我们对原型对象所做的任何修改都能立即从实例上反应出来。因为实例与原型之间的链接只不过是一个指针而不是副本:

function Person(){};
var person = new Person(); //person在Person()构造函数修改之前创建的
Person.prototype.name = "Oliver";
console.log(person.name); //仍然会出现实时的变化

但是如果重写了prototype 则就不同了,因为实例的[[Prototype]]会指向原型对象,如果修改了原来的原型对象,则就是切断了构造函数与最初原型之间的联系:

function Person(){};
var person = new Person();

Person.prototype = { //这里重写了Person.prototype,属于新的Person.prototype
    constructor: Person,
    name: "Oliver"
}

console.log(person.name); //原型对象被修改了,指针仍然指向旧的Person.prototype

从这里就可以很明显看出,Person.prototype={},实际上字面量方法就是重写了原型对象。如果是Person.prototype.name="Oliver",则并不是重写而是修改,不会创建“新的原型对象。”

《js高级程序设计》一书中6.2.3 中的图6-3 很清楚的描述了该原理

原生对象的原型

所有原生的引用类型(Object、Array、String等等)都在其构造函数的原型上定义了方法。同时,我们也可以给原生对象自定义方法:

var array = new Array();
Array.prototype.name = function(){
    console.log("Array")
};

array.push("hello ","there");
console.log(array);

array.name();

也可以修改:

var array = new Array();
Array.prototype.toString = function(){
    return("Array")
};

array.push("hello ","there");
console.log(array.toString());
//这样就抹去了toString()方法

强烈不推荐修改和重写原生对象的原型

原型对象的问题

就是包含引用类型值的属性来说,问题比较严重。具体体现在原型中的属性被实例共享:

function Person(){};
Person.prototype = {
    constructor: Person,
    name: "Oliver",
    age: 18,
    friends: ["Troy","Alice"]
}

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

person1.friends.push("Ellen");
console.log(person1.friends); //["Troy", "Alice", "Ellen"]
console.log(person2.friends); //["Troy", "Alice", "Ellen"]
//两者完全相同,因为原型中的该属性被实例所共享,push()方法只是修改了person1.friends,没有重写

person1.friends = ["Troy", "Alice"];
console.log(person1.friends); //["Troy", "Alice"]
console.log(person2.friends); //["Troy", "Alice", "Ellen"]
//虽然可以通过重写和覆盖来解决该问题,但是仍然非常麻烦

console.log(person1.hasOwnProperty("friends")); //true;
console.log(person2.hasOwnProperty("friends")); //false;
//这里就可以看到,重写能解决问题是因为重写导致它创建了实例属性"friends"

这里可以看出,如果我们的初衷像这样只是想创建一个共享的数组,那么当然不会有什么问题;但是实例一般都会有自己的属性,所以不应该多带带使用原型模式。而是组合使用构造函数模式和原型模式。

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

这是一种用来定义引用类型的一种默认模式:

function Person(name,age){
    this.name = name;
    this.age = age;
    this.friends = [];
} //独享的部分
Person.prototype = {
    constructor: Person,
    sayName: function(){
        return this.name;
    }
} //共享的部分
var person1 = new Person("Oliver",18);
var person2 = new Person("Troy",24);
person1.friends.push("Alice","Mark");
person2.friends.push("Mac");
console.log(person1.friends.toString());
console.log(person2.friends.toString());
/*
[Log] Alice,Mark (repetition.html, line 228)
[Log] Mac (repetition.html, line 229)
*/
动态原型模式

可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型:

function Person(name,age){
    this.name = name;
    this.age = age;
    if (typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            return (this.name);
        };
    }
}
var person = new Person("Oliver",18);
console.log(person.sayName()); //Oliver

实际上就是把下面代码封装在了构造函数中:

function Person(name,age){
    this.name = name;
    this.age = age;
}
Person.prototype.sayName = function(){
    return(this.name);
};
var person = new Person("Troy",24);
console.log(person.sayName()); //Troy
寄生构造函数模式

世纪撒好难过跟工厂模式一样。建议在可以使用其他模式的情况下,不要使用该模式。

稳妥构造函数模式

稳妥对象,指的是没有公共属性,且其方法也不引用this 的对象如:

function Person(name,age){
    var obj = new Object();
    obj.sayName = function(){
        console.log(name);
    };
    return obj;
}
var person1 = Person("Oliver",18);
person1.sayName(); //Oliver

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/86248.html

相关文章

  • JavaScript面向对象程序设计——“对象继承”注意要点

    摘要:如继承了这里就不必写该方法的主要优势就是可以在子类型构造函数中向超类型构造函数传递参数。以上原型式继承通常只是想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。 继承 继承分为接口继承和实现继承;接口继承只继承方法签名,实现继承则继承实际的方法;由于函数没有签名,所以ECMAScript 中没有接口继承,只能依靠原型链来实现实现继承。 原型链 基本思想是利用原型链让...

    zhongmeizhi 评论0 收藏0
  • JavaScript面向对象程序设计——“对象基本概念”注意要点

    摘要:描述符对象就是上面提到的个描述其行为的特性和。真是奇怪读取属性的特征使用的方法,可以取得给定属性的描述符。接收两个参数所在的对象和要读取其描述符的属性名称。 对象的基本概念 面向对象(Object-Oriented,OO),的语言最大的特征就是它们都有类的概念,通过类可以创建任意多个具有相同属性和方法的对象。 创建自定义对象最简单的方式就是创建一个Object 的实例,然后再给他添加属...

    HmyBmny 评论0 收藏0
  • JavaScript 工厂函数 vs 构造函数

    摘要:当谈到语言与其他编程语言相比时,你可能会听到一些令人困惑东西,其中之一是工厂函数和构造函数。好的,让我们用构造函数做同样的实验。当我们使用工厂函数创建对象时,它的指向,而当从构造函数创建对象时,它指向它的构造函数原型对象。 showImg(https://segmentfault.com/img/bVbr58T?w=1600&h=900); 当谈到JavaScript语言与其他编程语言...

    RayKr 评论0 收藏0
  • JavaScript要点(不含有语言基础语法)

    摘要:被覆盖级事件处理事件名,事件处理函数,事件捕获事件冒泡清除事件处理要使用级事件处理程序不会被覆盖而是会一步一步的解析执行。 一,变量1.可以用new Array(1,2);来定义数组。2.可以通过为变量赋值为null来清除变量,如: //首先定义一个变量 var i1=10; i1=null; //此时的i1就被清除了 在函数里面这样定义变量的时候要注意 funtion demo()...

    OpenDigg 评论0 收藏0
  • JavaScript面向对象

    摘要:构造函数的两个特征函数内部使用了,指向所要生成的对象实例。将一个空对象的指向构造函数的属性,这个对象就是要返回的实例对象。用面向对象开发时,把要生成的实例对象的特有属性放到构造函数内,把共有的方法放到构造函数的里面。 JS中面向对象的概念 面向对象OOP是一种组织代码结构、实现功能过程的思维方式。它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的...

    Eirunye 评论0 收藏0

发表评论

0条评论

tracymac7

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<