专业编程基础技术教程

网站首页 > 基础教程 正文

第29节 原型prototype-Javascript-零点程序员-王唯

ccvgpt 2024-12-14 10:19:43 基础教程 7 ℃

本内容是《Web前端开发之Javascript视频》的课件,请配合大师哥《Javascript》视频课程学习。

原型prototype:

第29节 原型prototype-Javascript-零点程序员-王唯

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

每个对象都从原型继承属性,也就是说,如果一个函数是一个类的话,这个类的所有实例对象都是从同一个原型对象上继承成员;因此,原型是类的核心;

优点:可以让所有对象实例共享它所包含的属性和方法;如:

function Person(){}
Person.prototype.name = "wangwei";
Person.prototype.age = 18;
Person.prototype.job = "Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};
var p1 = new Person();
p1.sayName();
var p2 = new Person();
p2.sayName();
alert(p1.sayName == p2.sayName);

所有通过对象字面量创建的对象都具有同一个原型对象,并可以通过Object.prototype获取对原型对象的引用;

通过new构造函数创建的对象,其原型就是构造函数的prototype属性;

同样,通过new Array()创建的对象的原型就是array.prototype,通过new Date()创建的对象的原型就是Date.prototype;

并不是所有的对象都具有原型,比如:Object.prototype本身就是一个对象,它就没有原型,并且也不继承任何属性;

对于一个实例对象来说,该实例的内部将包含一个指针,指向构造函数的原型对象;ES把这个指针称为[[Prototype]],但在脚本中,没有标准的方式访问[[Prototype]],但在很多实现中,每个对象上都支持一个属性__proto__,其就指向原型对象,并且可以通过脚本访问到;

function Person(name,age){}
console.log(Person.prototype);
var p = new Person("wangwei",18);
console.log(p.__proto__);
console.log(Person.prototype === p.__proto__);

构造函数的prototype属性被用作新对象的原型;这意味着通过同一个构造函数创建的所有对象,都继承自一个相同的对象,因此它们都是同一个类的成员;

构造函数和类的标识:

原型对象是类的唯一标识,当且仅当两个对象继承自同一个原型对象时,它们才是属于同一个类的实例;

而初始化对象的状态的构造函数则不能作为类的标识,两个构造函数的prototype属性可能指向同一个原型对象,那么这两个构造函数创建的实例是属于同一个类的;

constructor属性:

任何Javascript函数都可以用作构造函数,并且调用构造函数是需要用到prototype属性的;

在默认情况下,所有原型对象都会自动获得一个constructor属性,该属性是一个指向prototype属性所在函数的指针,其也是prototype属性中的唯一不可枚举属性,它的值就是一个函数对象;如Person.prototype.constructor指向Person;如:

var F = function(){}; // F是函数对象
var p = F.prototype;  // 这是F相关联的原型对象
var c = p.constructor; // 这是与原型相关联的函数
console.log(c === F); //true

可以看到构造函数的原型中存在预先定义好的constructor属性,该属性指向对象的构造函数;由于构造函数是类的“公共标识”,因此constructor属性为对象提供了类;

var o = new F();
console.log(o.constructor === F); // true

创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性,至于其他方法,则都是从Object继承而来的;

上面的Range()构造函数,使用它自身的一个新对象重写预定义的Range.prtotype对象,这个新定义的原型对象不含有constructor属性,因此Range类的实例也不含有constructor属性,可以显式的为原型添加一个构造函数,如:

// 在Range.prototype中添加
    constructor: Range,  // 显式设置构造函数反向引用
// 设置了constructor,可以继续为原型对象添加其他属性和方法。
// 修改原例
Range.prototype.includes = function(x){return this.from <=x && x <= this.to;};
Range.prototype.foreach = function(f){
    for(var x = Math.ceil(this.from); x <= this.to; x++) f(x);
};
Range.prototype.toString = function(){return "(" + this.from + "..." + this.to + ")";}

isPrototypeOf()方法:虽然在所有实现中都无法访问到[[Prototype]],但可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系;从本质上讲,如果[[Prototype]]指向调用isPrototypeOf()方法的对象,那么这个方法就返回true,如:

var p = {x:1};
var o = Object.create(p);
console.log(p.isPrototypeOf(o));
console.log(Person.prototype.isPrototypeOf(p1));
 
Object.getPrototypeOf():该方法返回[[Prototype]]的值,即查询一个对象的原型,如:
alert(Object.getPrototypeOf(p1) == Person.prototype); // true
alert(Object.getPrototypeOf(p1).name); // wangwei
alert(Object.getPrototypeOf(p1));

当代码读取某个对象的某个属性时,会执行一次搜索,目标是具有给定名字的属性;

搜索首先从对象实例本身开始,再到原型对象;如果在实例中找到了具有给定名字的属性,则返回该属性值,如果没找到,则继续搜索指针指向的原型对象,如果找到,就返回该属性值;也就是说,前面调用p1.sayName()时,会先后执行两次搜索;其次,p2也是如此;正因为如此,多个对象实例才能共享原型中所保存的属性和方法。

虽然可以通过对象实例访问保存在原型中的值,但不能通过对象实例重写原型中的值;如果在实例中添加一个属性,而该属性与原型中的一个属性同名,那么就会在该实例中创建该属性,该属性会屏蔽原型中的同名属性;

// 在以上的示例中添加
var p1 = new Person();
var p2 = new Person();
p1.name = "wujing";
alert(p1.name);  // wujing 来自实例
alert(p2.name);  // wangwei  来自原型

如果想恢复访问原型中的属性,默认情况下是恢复不了的,即使将实例属性设置为null,也不会恢复其指向原型的连接,但可以使用delete完全删除实例属性,从而能够重新访问原型中的属性,如:

// p1.name = null;
delete p1.name;
alert(p1.name);  // wangwei  来自原型

重写整个原型:

为了减少不必要的输入,也为了从视觉上更好的封装原型的功能,常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象;

function Person(){}
Person.prototype = {
    name:"wangwei",
    age:18,
    sayName:function(){
        alert(this.name);
    }
};

此时,instanceof操作还能返回正确的结果,但通过constructor已经无法确定对象的类型了。

var p = new Person();
alert(p instanceof Person);  // true
alert(p instanceof Object);  // true
alert(p.constructor == Person);  // false
alert(p.constructor == Object); // true

如果需要constructor属性,可以在代码块中显式声明;

Person.prototype = {
    constructor:Person,
    name:"wangwei",
    age:18,
    sayName:function(){
        alert(this.name);
    }
};
var p = new Person();
alert(p.constructor);
alert(p.constructor == Person);  // true

但是,以这种方式重设constructor属性会导致它的[[Enumerable]]特性被设置为true;默认情况下,原生的constructor属性是不可枚举的;可以通过Object.defineProperty()方法重设constructor,如:

Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false,
    value: Person
});
var p = new Person();
alert(p.constructor);
alert(p.constructor == Person);  // true

Object.create()方法:

Object.create()方法规范了原型式继承,这个方法接收两个参数:一个用作新对象原型的对象和一个为新对象定义额外属性的对象(可选的);如:

var person = {
    name:"wangwei",
    friends:["wujing","lishi"]
};
var p1 = Object.create(person);  // person作为原型对象传入,p1继承了person属性
console.log(p1);
console.log(person);
p1.name = "wujing";
p1.friends.push("adu");
console.log(p1);
var p2 = Object.create(person);
console.log(p2);
p2.name = "juanzi";
p2.friends.push("van");
console.log(p1.friends); //wujing,lishi,adu,van
console.log(person.friends); //wujing,lishi,adu,van

如果传入参数null,就会创建一个没有原型的新对象,其也不会继承任何成员,可以对它直接使用in运算符,而无需使用hasOwnProperty()方法,如:

var o = Object.create(null);
console.log(o);  //  No properties

如果想创建一个普通的空对象,比如通过{ }或new Object()创建的对象,需要传入Object.prototype,如:

var o = Object.create(Object.prototype);
console.log(o);  // 与{}和new Object()一样

可以通过任意原型创建新对象,即可以使任意对象可继承,这是一个强大的特性;

Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同,每个属性都是通过自己的描述符定义的;以这种方式指定的任何属性都会覆盖原型对象上的同名属性;如:

var person = {
    name:"wangwei",
    friends:["wujing","lishi"]
};
var p1 = Object.create(person, {
    name:{
        value: "wujing"
    }
});
console.log(p1.name);

原型的动态性:

ES中基于原型的继承机制是动态的:对象从其原型继承属性,如果创建对象之后原型的属性发生改变,也会影响到继承这个原型的所有实例对象;这意味着可以通过给原型对象添加新方法来扩充ES类,即使先创建了实例后再修改原型也如此;如:

function Person(){}
Person.prototype = {
    name:"wangwei",
    age:18,
    sayName:function(){
        alert(this.name);
    }
};

尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果重写整个原型对象,本质上就不一样了;如:

var p = new Person();
alert(p instanceof Person);  // true
alert(p instanceof Object);  // true
alert(p.constructor == Person);  // false
alert(p.constructor == Object); // true

重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型;

原生对象的原型:

原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,也是采用这种模式创建的,所有原生引用类型(Array, Object, String等),都在其构造函数的原型上定义了方法,如:Array.prototype中可以找到sort方法,在String.prototye中找到substring() 方法;如:

Person.prototype = {
    constructor:Person,
    name:"wangwei",
    age:18,
    sayName:function(){
        alert(this.name);
    }
};
var p = new Person();
alert(p.constructor);
alert(p.constructor == Person);  // true

通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法,可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法;

Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false,
    value: Person
});
var p = new Person();
alert(p.constructor);
alert(p.constructor == Person);  // true

可以给Object.prototype添加方法,从而使所有的对象都可以调用这些方法;尽管可以这么做,但不建议修改原生对象的原型,因为当在另一个支持该方法的实现中运行代码时,就可能会导致命名冲突,另外,这样做也可能会无意地重写原生方法,如果有必要添加的话,最好使用Object.defineProperty()方法进行属性的特性的定义;

原型对象的问题:

原型缺点:首先其忽略了为构造函数传递初始化参数这一环节,所以在默认情况下所有实例将取得相同的属性值;其次,由于其属性是所有实例共享,对于基本类型的属性没有影响,毕竟通过在实例上添加一个同名属性,可以隐藏原型中的对应属性;但对引用类型的属性有影响;如:

function Person(){}
Person.prototype = {
    constructor:Person,
    name:"wangwei",
    age:18,
    friends: ["she","who"],
    sayName:function(){
        alert(this.name);
    }
};
var p1 = new Person();
var p2 = new Person();
p1.friends.push("van");  // 但是如果p1.friends = []就不一样了,相当于新建了数组
alert(p1.friends);  // she,who,van
alert(p2.friends);  // she,who,van
alert(p1.friends === p2.friends); // true

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

创建自定义类型的最常见的方式,就是组合使用构造函数模式和原型模式;即用构造函数定义对象的实例属性,而用原型方式定义对象的方法和共享的属性;另外,这种混合模式还支持向构造函数传递参数;如:

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["she","who"];
}
Person.prototype = {
    constructor: Person,
    sayName: function(){
        alert(this.name);
    }
};
var p1 = new Person("wangwei",18,"Engineer");
var p2 = new Person("wujing",28,"Doctor");
p1.friends.push("van");
alert(p1.friends);
alert(p2.friends);
alert(p1.friends === p2.friends);
alert(p1.sayName === p2.sayName);

总结:这种构造函数模式与原型模式的混合,是目前使用最广泛,认同度最高的一种创建自定义类型的方法;可以说,这是用来定义引用类型的一种默认模式。

动态原型模式:

原型模式还不是像其他语言一样把所有属性和方法都封装起来;

动态原型模式的基本思想是把所有信息封装到构造函数中,而通过构造函数初始化原型,又保持了同时使用构造函数和原型的优点;换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型;如:

function Person(name,age,job){
    // 属性
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["she","who"];
    // 方法
    if (typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            alert(this.name);
        };
    }
}
var p = new Person("wangwei",18,"Engineer");
p.sayName();

这段代码只会在初次调用构造函数时才会执行;此时,原型已经完成初始化,不需要再做什么修改了;注意:这里对原型的所做的修改,能够立即在所有实例中得到反映;因此,这种方式比较完美;其中,if语句检查的可以是初始化之后应该存在的任何属性或方法,不必用很多的if语句检查每个属性和方法,只要检查其中一个即可;对于采用这种模式创建的对象,还可以使用instanceof操作符确定它的类型。

function Car(sColor,iDoors,iMpg){
    this.color = sColor;
    this.doors = iDoors;
    this.mpg = iMpg;
    this.drivers = new Array("Mike","Sue");
    if(typeof Car._init == "undefined"){
        Car.prototype.showColor = function(){
            alert(this.color);
        };
        Car._init = true;
        alert("ready");
    }
}
var oCar1 = new Car("red",4,23);  // ready
var oCar2 = new Car("blue",3,24); // 无,只执行一次
alert(oCar1 instanceof Car);

寄生构造函数模式(混合工厂):

基本思想:创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面看,这个函数又很像是典型的构造函数,如:

function Person(name,age,job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.jog = job;
    o.sayName = function(){
        alert(this.name);
    };
    return o;
}
var p = new Person("wangwei",18,"Engineer");
p.sayName();

这种模式可以在特殊的情况下用来为对象创建构造函数,如创建一个具有额外方法的特殊数组,由于不能直接修改Array构造函数,因此可以使用这个模式,如:

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

稳妥构造函数模式(混合工厂):

道格拉斯Douglas Crockford发明了JS中的稳妥对象(durable objects)这个概念;

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

这种模式最适合在一些安全的环境中(会禁止使用this和new),或者在防止数据被其他程序修改时使用;

稳妥构造函数模式遵循与寄生构造函数类似的模式,但有两点不同:一是新创建的对象的实例方法不引用this; 二是不使用new操作符调用构造函数;如:

function Person(name,age,jog){
    var o = new Object(); // 创建要返回的对象
    // 可以在这里定义私有变量和函数
    o.sayName = function(){
        alert(name);
    };
    return o;
}
var p = Person("wangwei",18,"Engineer");
p.sayName();

小示例:

字符串操作(提高其性能);

var str="hello";
str += "world";
var arr=new Array();
arr[0]="hello";
arr[1]="world";
var str=arr.join("");

可以把其封装,可复用:

function StringBuffer(){
    this._strings = new Array();
}
StringBuffer.prototype.append=function(str){
    this._strings.push(str);
}
StringBuffer.prototype.toString=function(){
    return this._strings.join("");
}
var buffer=new StringBuffer();
buffer.append("hello");
buffer.append("world");
var str=buffer.toString();
document.write(str);

测试性能:

function StringBuffer(){
    this._strings = new Array(); }
StringBuffer.prototype.append=function(str){
    this._strings.push(str); }
StringBuffer.prototype.toString=function(){
    return this._strings.join(""); }
var d1=new Date();
var str="";
for(var i=0; i<10000; i++){
    str += "text"; }
var d2=new Date();
document.write("页面执行时间为:" + (d2.getTime() - d1.getTime()) + "<br>");
d1=new Date();
var buffer=new StringBuffer();
for(var i=0; i<10000; i++)
    buffer.append("text"); 
str=buffer.toString();
d2=new Date();
document.write("页面执行时间为:" + (d2.getTime() - d1.getTime()) + "<br>");

利用prototype属性为所有对象自定义属性和方法;

//为Number对象添加输出16进制的方法;
Number.prototype.toHexString=function(){
    return this.toString(16);
}
var iNum=15;
console.log(iNum.toHexString());
//为数组添加队列方法;
Array.prototype.enqueue=function(vItem){
    this.push(vItem);
}
Array.prototype.dequeue=function(){
    return this.shift();
}
var arr=new Array("red","blue","white");
arr.enqueue("green");
console.log(arr);
arr.dequeue();
console.log(arr);
//如果想给所有内置对象添加新方法,必须在Object对象的prototype属性上定义;
Object.prototype.showValue=function(){
    console.log(this.valueOf());
};
var str="hello";
var iNum=23;
str.showValue();
iNum.showValue();
// 函数名只是指向函数的指针,因此可以使它指向其他函数
Function.prototype.toString=function(){
    return "函数内部代码隐藏";
}
function say(){
    console.log("hi");
}
console.log(say.toString());
//此方法会覆盖原始方法,所以可以在使用前存储它的指针,以便以后使用;
Function.prototype.oriToString=Function.prototype.toString;
Function.prototype.toString=function(){
    if(this.oriToString().length>100){
        return "内容过长,部分隐藏";
    }else{
        return this.oriToString();
    }
}

遍历和枚举属性:

in操作符:

in操作符会通过对象能够访问给定属性时返回true,无论属性存在于实例中还是原型中:

alert(p1.hasOwnProperty("name"));  // false
alert("name" in p1);  // true

除了in操作符,更为便捷的方式是使用“!==”判断一个属性是否是undefined,如:

var o = {x:1};
o.x !== undefined;  // true
o.y !== undefined;  // false
o.toString != undefined; // true,o继承了toString

然而有一种场景只能使用in而不能使用上述属性访问的方式,in可以区分该属性存在但值为undefined的情景,如:

var o = {x:undefined};
o.x !== undefined  //false,属性存在,但值为undefined
o.y !== undefined // false,属性不存在
"x" in o;  // true,属性存在
"y" in o; // flase,属性不存在
delete o.x;
"x" in o; // false

在使用”!==”时,要注意其与“!=”不同点,“!==”可以区分undefined和null,如:

var o={x:2};
// 如果o中存在属性x,且x的值不是null或undefined,则o.x乘以2
if(o.x != null) o.x *= 2;
// 如果o中存在属性x,且x的值不能转换为false,o.x乘以2
// 如果x是undefined、null、false、“ ”、0或NaN,则它保持不变
if(o.x) o.x *=2;

同时使用hasOwnProperty()方法和in操作符,可以确定该属性是存在于对象还是原型中;

// 判断是否为原型
var p1 = new Person();
// p1.name = "wujing";  // 添加此名就会返回false
function hasPrototypeProperty(object, name){
    return !object.hasOwnProperty(name) && (name in object);
}
alert(hasPrototypeProperty(p1,"name"));  // true
// 或者:
var obj = {
    name:"wang",
    age:100,
    sex:'male',
    __proto__:{
        lastName:"wei"
    }
}
Object.prototype.height = '178CM';
for(var p in obj){
    if(obj.hasOwnProperty(p)){
        console.log(obj[p]);
    }
}

除了检测对象的属性是否存在,还会经常遍历对象的属性;通常使用for/in循环遍历,但ES还提供了两个更好的方案;

for-in:可以遍历所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在原型中的属性;

var o = {x:1,y:2,z:3};
Object.defineProperty(o,"z",{value:4,enumerable:false});
console.log(o.z);
for(p in o)
    console.log(p);
    
function Person(){this.name="wangwei";this.age=10;}
Person.prototype.toString = function(){return "wangwei";}
var p1 = new Person();
Object.defineProperty(p1,"age",{value:18,enumerable:false});
for(prop in p1)
    console.log(prop + ":" + p1[prop]);

有时需要过滤遍历,如:

var obj={a:1,b:2};
var o = Object.create(obj);
o.x=1,o.y=2,o.z=3;
o.sayName = function(){}
for(p in o){
    if(!o.hasOwnProperty(p)) continue;  // 跳过继承的属性
    console.log(p);
}
for(p in o){
    if(typeof o[p] === "function") continue;
    console.log(p);  // 跳过方法
}

用来枚举属性的工具函数:

// 把p中的可枚举属性复制到o中,并返回o,如果o和p中含有同名属性,则覆盖
function extend(o,p){
    for(prop in p)
        o[prop] = p[prop];
    return o;
}
 
// 如果o和p中有同名属性,则o中的属性不受影响
function merge(o,p){
    for(prop in p){
        if(o.hasOwnProperty[prop]) continue;
        o[prop] = p[prop];
    }
    return o;
}
 
// 如果o和p中没有同名属性,则从o中删除这个属性
function restrict(o,p){
    for(prop in o){
        if(!(prop in p)) delete o[prop];
    }
    return o;
}
// 如果o和p中有同名属性,则从o中删除这个属性
function substract(o,p){
    for(prop in p){
        delete o[prop]; // 删除一个不存在的属性也不会报错
    }
    return o;
}
// 返回一个新对象,这个对象同时拥有o和p的属性,如果o和p有重名属性,则用p的属性
function union(o,p){
    return extend(extend({},o), p);
}
// 返回一个新对象,这个对象同时拥有o和p的属性,交集,但p中属性的值被忽略
function intersection(o,p){
    return restrict(extend({},o), p);
}
// 返回一个数组,这个数组包含的是o中可枚举的自有属性的名字
function keys(o){
    if(typeof o !== "object") throw TypeError;  // o必须为对象
    var result = [];
    for(var prop in o){  // 所有可枚举的属性
        if(o.hasOwnProperty(prop))  // 判断是否是自有属性
            result.push(prop);
    }
    return result;
}

Object.keys()方法:取得对象上所有可枚举的实例属性,该方法接受一个对象作为参数,返回一个包含所有可枚举属性的字符串数组,如:

var keys = Object.keys(Person.prototype);
alert(keys);  // name,age,job,sayName,toString
var p1 = new Person();
p1.name = "wujing";
p1.age = 28;
keys = Object.keys(p1);
alert(keys);  // name,age

Object.getOwnPropertyNames()方法:与Object.keys()类型,但获得是所有实例属性,无论它是否可枚举如:

var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys);  // constructor,name,age,job,sayName,toString

分析说明:结果中包含了不可枚举的constructor属性;

注:Object.keys()和Object.getOwnPropertyNames()方法都可以用来替代for-in循环。


最近发表
标签列表