资讯专栏INFORMATION COLUMN

JavaScript 原型链

verano / 1140人阅读

摘要:命令通过构造函数新建实例对象,实质就是将实例对象的原型,指向构造函数的属性,然后在实例对象上执行构造函数。

大部分面向对象的编程语言,都是以“类”(class)作为对象体系的语法基础。JavaScript语言中是没有class的概念的(ES6之前,ES6中虽然提供了class的写法,但实现原理并不是传统的“类”class概念,仅仅是一种写法), 但是它依旧可以实现面向对象的编程,这就是通过JavaScript中的“原型对象”(prototype)来实现的。

prototype 属性

请看这样一个例子:

function Person(name, gender) {
    this.name = name;
    this.gender = gender;
    this.sayHello = function() {
        console.log("Hello,I am", this.name, ". I"m a", this.gender);
    };
}

这样定义了一个构造函数,我们创建对象就可以使用这个构造函数作为模板来生成。不过以面向对象的思想来看,不难发现其中的一点问题:namegender属性是每个实例都各不相同,作为一个自身的属性没有问题,而sayHello方法,每个实例对象应该都有,而且都一样,给每个实例对象一个全新的、完全不同(虽然代码内容一样,但JavaScript中每个sayHello的值都在内存中多带带存在)的sayHello方法是没有必要的。

var zs = new Person("zhang san", "male"),
    xh = new Person("xiao hong", "female");

zs.sayHello(); // Hello,I am zhang san . I"m a male
xh.sayHello(); // Hello,I am xiao hong . I"m a female

zs.sayHello === xh.sayHello;  // false

上面代码中展示了zs.sayHellxh.sayHello这两个作用相同,而且看起来代码内容也是完全一样的对象,实际是两个独立的,互不相关的对象。

面向对象思想中,是将公共的、抽象的属性和方法提取出来,作为一个基类,子类继承这个基类,从而继承到这些属性和方法。而JavaScript中则可以通过prototype属性来实现类似的作用。以下是上面代码的改进示例:

function Person(name, gender) {
    this.name = name;
    this.gender = gender;
}
Person.prototype.sayHello = function() {
    console.log("Hello,I am", this.name, ". I"m a", this.gender);
};

var zs = new Person("zhang san", "male"),
    xh = new Person("xiao hong", "female");

zs.sayHello(); // Hello,I am zhang san . I"m a male
xh.sayHello(); // Hello,I am xiao hong . I"m a female

zs.sayHello === xh.sayHello;  // true

这时将sayHello方法定义到Person对象上的prototype属性上,取代了在构造函数中给每个实例对象添加sayHello方法。可以看到,其还能实现和之前相同的作用,而且zs.sayHellxh.sayHello是相同的内容,这样就很贴近面向对象的思想了。那么zsxh这两个对象,是怎么访问到这个sayHello方法的呢?

在浏览器控制台中打印出zs,将其展开,可以看到下面的结果:

zs;
/**
 * 
Person
    gender: "male"
    name: "zhang san"
    __proto__: Object
        constructor: function Person(name, gender) 
            arguments: null
            caller: null
            length: 2 
            name: "Person"
            prototype: Object
        sayHello:function()
            arguments:null
            caller:null
            length:0
            name:""
            prototype:Object
*/

zs这个对象只有两个自身的属性gendername,这和其构造函数Person的模板相同,并且可以在Person对象的__proto__属性下找到sayHello方法。那么这个__proto__是什么呢?它是浏览器环境下部署的一个对象,它指的是当前对象的原型对象,也就是构造函数的prototype属性。

现在就可以明白了,我们给构造函数Person对象的prototype属性添加了sayHello方法,zsxh这两个通过Person构造函数产生的对象,是可访问到Person对象的prototype属性的,所以我们定义在prototype下的sayHello方法,Person的实例对象都可以访问到。

关于构造函数的new命令原理是这样的:

创建一个空对象,作为将要返回的对象实例

将这个空对象的原型,指向构造函数的prototype属性

将这个空对象赋值给函数内部的this关键字

开始执行构造函数内部的代码

constructor 属性

prototype下有一个属性constructor,默认指向此prototype对象所在的构造函数。

如上例中的zs__proto__constructor值为function Person(name, gender)

由于此属性定义在prototype属性上,所以它可以在所有的实例对象中获取到。

zs.constructor;
// function Person(name, gender) {
//     this.name = name;
//     this.gender = gender;
// }

zs.hasOwnProperty("constructor"); // false
zs.constructor === Person; // true

zs.constructor === Function; // false
zs.constructor === Object; // false

constructor属性放在prototype属性中的一个作用是,可以通过这个属性来判断这个对象是由哪个构造函数产生的,上面代码中,zs是由Person构造函数产生的,而不是Function或者Object构造函数产生。

constructor属性的另一个作用就是:提供了一种继承的实现模式。

function Super() {
    // ...
}

function Sub() {
    Sub.superclass.constructor.call(this);
    // ...
}

Sub.superclass = new Super();

上面代码中,SuperSub都是构造函数,在Sub内部的this上调用Super,就会形成Sub继承Super的效果,miniui中是这样实现继承的:

mini.Control = function(el) {    
    mini.Control.superclass.constructor.apply(this, arguments);
    // ...
}
// 其中的superclass指代父类的prototype属性

我们自己写一个例子:

// 父类
function Animal(name) {
    this.name = name;
    this.introduce = function() {
        console.log("Hello , My name is", this.name);
    }
}
Animal.prototype.sayHello = function() {
    console.log("Hello, I am:", this.name);
}

// 子类
function Person(name, gender) {
    Person.superclass.constructor.apply(this, arguments);
    this.gender = gender;
}
Person.superclass = new Animal();

// 子类
function Dog(name) {
    Dog.superclass.constructor.apply(this, arguments);    
}
Dog.superclass = new Animal();

基本原理就是在子类中使用父类的构造函数。在PersonDog中均没有对name属性和introduce方法进行操作,只是使用了父类Animal的构造函数,就可以将name属性和introduce方法继承来,请看下面例子:

var zs = new Person("zhang san", "male");

zs; // Person {name: "zhang san", gender: "male"}
zs.sayHello(); // Uncaught TypeError: zs.sayHello is not a function(…)
zs.introduce(); // Hello , My name is zhang san

var wangCai = new Dog("旺财");

wangCai; // Dog {name: "旺财"}
wangCai.introduce(); // Hello , My name is 旺财

确实实现了我们需要的效果。可是我们发现在调用zs.sayHello()时报错了。为什么呢?

其实不难发现问题,我们的Person.superclassAnimal的一个实例,是有sayHello方法的,但是我们在Perosn构造函数的内部,只是使用了Person.superclass.constructor。而Person.superclass.constructor指的仅仅是Animal构造函数本身,并没有包括Animal.prototype,所以没有sayHello方法。

一种改进方法是:将自定义的superclass换为prototype,即:

function Person(name, gender) {
    Person.prototype.constructor.apply(this, arguments);
    this.gender = gender;
}
Person.prototype = Animal.prototype;

var zs = new Person("zhang san", "male");
zs.sayHello(); // Hello, I am: zhang san
zs.introduce() // Hello , My name is zhang san

这样就全部继承到了Animal.prototype下的方法。

但是一般不要这样做,上面写法中Person.prototype = Animal.prototype; 等号两端都是一个完整的对象,进行赋值时,Person.prototype的原对象完全被Animal.prototype替换,切断了和之前原型链的联系,而且此时Person.prototypeAnimal.prototype是相同的引用,给Person.prototype 添加的属性方法也将添加到Animal.prototype,反之亦然,这将引起逻辑混乱。

因此我们在原型上进行扩展是,通常是添加属性,而不是替换为一个新对象。

// 好的写法
Person.prototype.sayHello = function() {
    console.log("Hello,I am", this.name, ". I"m a", this.gender);
};
Person.prototype. // .. 其他属性 

// 不好的写法
Person.prototype = {
    sayHello:function(){
        console.log("Hello,I am", this.name, ". I"m a", this.gender);
    },
    // 其他属性方法 ...
}
JavaScript 原型链

JavaScript的所有对象都有构造函数,而所有构造函数都有prototype属性(其实是所有函数都有prototype属性),所以所有对象都有自己的原型对象。

对象的属性和方法,有可能是定义在自身,也有可能是定义在它的原型对象。由于原型本身也是对象,又有自己的原型,所以形成了一条原型链(prototype chain)。

zs.sayHello(); // Hello,I am zhang san . I"m a male

zs.toString(); // "[object Object]"

例如上面的zs对象,它的原型对象是Personprototype属性,而Personprototype本身也是一个对象,它的原型对象是Object.prototype

zs本身没有sayHello方法,JavaScript通过原型链向上继续寻找,在Person.prototype上找到了sayHello方法。toString方法在zs对象本身上没有,Person.prototype上也没有,因此继续沿原型链查找,最终可以在Object.prototype上找到了toString方法。

Object.prototype的原型指向null,由于null没有任何属性,因此原型链到Object.prototype终止,所以Object.prototype是原型链的最顶端。

“原型链”的作用是,读取对象的某个属性时,JavaScript引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype还是找不到,则返回undefined

如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overiding)。

JavaScript中通过原型链实现了类似面向对象编程语言中的继承,我们在复制一个对象时,只用复制其自身的属性即可,无需将整个原型链进行一次复制,Object.prototype下的hasOwnProperty方法可以判断一个属性是否是该对象自身的属性。

实例对象构造函数prototype之间的关系可用下图表示:

instranceof 运算符

instanceof运算符返回一个布尔值,表示指定对象是否为某个构造函数的实例。由于原型链的关系,所谓的实例并不一定是某个构造函数的直接实例,更准确的描述,应该是:返回一个后者的原型对象是否在前者的原型链上

zs instanceof Person; // true
zs instanceof Object ;// true 

var d = new Date();
d instanceof Date; // true
d instanceof Object; // true
原型链相关属性和方法 Object.prototype.hasOwnProperty()

hasOwnProperty()方法用来判断某个对象是否含有指定的自身属性。这个方法可以用来检测一个对象是否含有特定的自身属性,和 in 运算符不同,该方法会忽略掉那些从原型链上继承到的属性。

zs.hasOwnProperty("name"); // true
zs.hasOwnProperty("gender"); // true

zs.hasOwnProperty("sayHello"); // fasle
Person.prototype.hasOwnProperty("sayHello"); // true 

zs.hasOwnProperty("toString"); // fasle
Object.prototype.hasOwnProperty("toString"); // true
Object.prototype.isPrototypeOf()

对象实例的isPrototypeOf方法,用来判断一个对象是否是另一个对象的原型。

var o1 = {};
var o2 = Object.create(o1);
var o3 = Object.create(o2);

o2.isPrototypeOf(o3) // true
o1.isPrototypeOf(o3) // true

上面代码表明,只要某个对象处在原型链上,isProtypeOf都返回true

Object.prototype.isPrototypeOf({}) // true
Object.prototype.isPrototypeOf([]) // true
Object.prototype.isPrototypeOf(/xyz/) // true
Object.prototype.isPrototypeOf(Object.create(null)) // false

看起来这个方法和instanceof运算符作用类似,但实际使用是不一样的

例如:

zs instanceof Person ; // true;

Person.isPrototypeOf(zs);// false
Person.prototype.isPrototypeOf(zs); // true

zs instanceof Person可理解为判断Person.prototype在不在zs的原型链上。 而Person.isPrototypeOf(zs)指的就是Person本身在不在zs的原型链上,所以返回false,只有Person.prototype.isPrototypeOf(zs)才为 true

Object.getPrototypeOf()

ES5Object.getPrototypeOf方法返回一个对象的原型。这是获取原型对象的标准方法。

// 空对象的原型是Object.prototype
Object.getPrototypeOf({}) === Object.prototype
// true

// 函数的原型是Function.prototype
function f() {}
Object.getPrototypeOf(f) === Function.prototype
// true

// f 为 F 的实例对象,则 f 的原型是 F.prototype
var f = new F();
Object.getPrototypeOf(f) === F.prototype
// true

Object.getPrototypeOf("foo");
// TypeError: "foo" is not an object (ES5 code)
Object.getPrototypeOf("foo");
// String.prototype                  (ES6 code)

此方法是ES5方法,需要IE9+。在ES5中,参数只能是对象,否则将抛出异常,而在ES6中,此方法可正确识别原始类型。

Object.setPrototypeOf()

ES5Object.setPrototypeOf方法可以为现有对象设置原型,返回一个新对象。接受两个参数,第一个是现有对象,第二个是原型对象。

var a = {x: 1};
var b = Object.setPrototypeOf({}, a);
// 等同于
// var b = {__proto__: a};

b.x // 1

上面代码中,b对象是Object.setPrototypeOf方法返回的一个新对象。该对象本身为空、原型为a对象,所以b对象可以拿到a对象的所有属性和方法。b对象本身并没有x属性,但是JavaScript引擎找到它的原型对象a,然后读取ax属性。

new命令通过构造函数新建实例对象,实质就是将实例对象的原型,指向构造函数的prototype属性,然后在实例对象上执行构造函数。

var F = function () {
  this.foo = "bar";
};

// var f = new F();等同于下面代码
var f = Object.setPrototypeOf({}, F.prototype);
F.call(f);
Object.create()

ES5Object.create方法用于从原型对象生成新的实例对象,它接收两个参数:第一个为一个对象,新生成的对象完全继承前者的属性(即新生成的对象的原型此对象);第二个参数为一个属性描述对象,此对象的属性将会被添加到新对象。(关于属性描述对象可参考:MDN - Object.defineProperty())

上面代码举例:

var zs = new Person("zhang san", "male");

var zs_clone = Object.create(zs);

zs_clone; // {}
zs_clone.sayHello(); // Hello,I am zhang san . I"m a male
zs_clone.__proto__ === zs; // true
// Person
//     __proto__: Person
//         gender: "male"
//         name: "zhang san"
//         __proto__: Object

可以 看出 创建的新对象zs_clone的原型为zs,从而获得了zs的全部属性和方法。但是其自身属性为空,若需要为新对象添加自身属性,则使用第二个参数即可。

var zs_clone = Object.create(zs, {
    name: { value: "zhangsan"s clone" },
    gender: { value: "male" },
    age: { value: "25" }
});
zs_clone; // Person {name: "zhangsan"s clone", gender: "male", age: "25"}
参考链接

JS中的prototype - 轩脉刃

prototype 对象 - JavaScript标准参考教程

更多可见JavaScript 原型链

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

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

相关文章

  • 如何理解JavaScript原型原型

    摘要:之前有朋友问怎么去理解原型和原型链的问题。理解原型链的小技巧将箭头视作泛化子类到父类关系那么图中所有的虚线将构成一个继承层级,而实线表示属性引用。原型链是实现继承的重要方式,原型链的形成是真正是靠而非。 之前有朋友问怎么去理解原型和原型链的问题。这个问题,在面试中,很多同学经常都会遇到。这里给大家讲讲,方便大家记忆。 JavaScript的特点JavaScript是一门直译式脚本...

    xuexiangjys 评论0 收藏0
  • 如何理解JavaScript原型原型

    摘要:之前有朋友问怎么去理解原型和原型链的问题。理解原型链的小技巧将箭头视作泛化子类到父类关系那么图中所有的虚线将构成一个继承层级,而实线表示属性引用。原型链是实现继承的重要方式,原型链的形成是真正是靠而非。 之前有朋友问怎么去理解原型和原型链的问题。这个问题,在面试中,很多同学经常都会遇到。这里给大家讲讲,方便大家记忆。 JavaScript的特点JavaScript是一门直译式脚本...

    adie 评论0 收藏0
  • 我所认识的JavaScript作用域原型

    摘要:为了防止之后自己又开始模糊,所以自己来总结一下中关于作用域链和原型链的知识,并将二者相比较看待进一步加深理解。因此我们发现当多个作用域相互嵌套的时候,就形成了作用域链。原型链原型说完了作用域链,我们来讲讲原型链。   毕业也整整一年了,看着很多学弟都毕业了,忽然心中颇有感慨,时间一去不复还呀。记得从去年这个时候接触到JavaScript,从一开始就很喜欢这门语言,当时迷迷糊糊看完了《J...

    Bmob 评论0 收藏0
  • 细说 Javascript 对象篇(二) : 原型对象

    摘要:并没有类继承模型,而是使用原型对象进行原型式继承。我们举例说明原型链查找机制当访问一个对象的属性时,会从对象本身开始往上遍历整个原型链,直到找到对应属性为止。原始类型有以下五种型。此外,试图查找一个不存在属性时将会遍历整个原型链。 Javascript 并没有类继承模型,而是使用原型对象 prototype 进行原型式继承。 尽管人们经常将此看做是 Javascript 的一个缺点,然...

    lansheng228 评论0 收藏0
  • javascript高级程序设计》笔记:原型图解

    摘要:不理解没关系,下面会结合图例分析上一篇高级程序设计笔记创建对象下一篇高级程序设计笔记继承参考之原型链的解读三张图搞懂的原型对象与原型链继承与原型链 文章直接从原型图解开始的,如果对一些概念不太清除,可以结合后面几节查看 1. 图解原型链 1.1 铁三角关系(重点) function Person() {}; var p = new Person(); showImg(https://s...

    vspiders 评论0 收藏0
  • JavaScript原型以及Object,Function之间的关系

    摘要:由于一般所有的原型链最终都会指向顶端的,所以它们都是的。好了现在了,成了所有对象原型链的。 JavaScript里任何东西都是对象,任何一个对象内部都有另一个对象叫__proto__,即原型,它可以包含任何东西让对象继承。当然__proto__本身也是一个对象,它自己也有自己的__proto__,这样一级一级向上,就构成了一个__proto__链,即原型链。当然原型链不会无限向上,它有...

    zacklee 评论0 收藏0

发表评论

0条评论

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