JavaScript 面向对象之二:原型链

构造函数的 prototype属性

JavaScript 的每个对象都继承另一个父级对象,父级对象称为 原型 (prototype)对象。

原型也是一个对象,原型对象上的所有属性和方法,都能被子对象 (派生对象) 共享
通过构造函数生成实例对象时,会自动为实例对象分配原型对象。
而每一个构造函数都有一个prototype属性,这个属性指向实例对象的原型对象

null没有自己的原型对象。

这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在构造函数的 prototype 属性上,

也就是实例对象的原型对象上。

function Cat(color) {
    this.color = color;
}

Cat.prototype.name = "猫";
Cat.prototype.sayhello = function(){
    console.log('hello'+this.name,this.color);
}
Cat.prototype.saycolor = function (){
    console.log('hello'+this.color);
}

var cat1 = new Cat('白色'); 
var cat2 = new Cat('黑色'); 
cat1.sayhello();
cat2.saycolor();

这时所有实例的 name 属性和 sayhello()saycolor 方法,
其实都是同一个内存地址,指向构造函数的 prototype 属性,因此就提高了运行效率节省了内存空间。

构造函数、实例、原型三者之间的关系

这里写图片描述

构造函数的prototyp属性,就是由这个构造函数new出来的所有实例对象的 原型对象

每个对象都有一个 constructor 属性,该属性指向创建该实例的构造函数

对象.__proto__ (两边都是两个下划线):获取对象的原型对象;

console.log(cat1.__proto__ == Cat.prototype); // true

注意:ES6标准规定,__proto__属性只有浏览器才需要部署,其他环境可以不部署,因此不建议使用

原型对象的获取及修改

上节可以看到,想要获取一个实例对象的原型对象,有两种方式:

1:通过实例对象的构造函数的prototype属性获取: 实例对象.constructor.prototype

2:通过实例对象的 __proto__ 属性获取: 实例对象.__proto__

而这两种方式,我们都不建议使用:

obj.constructor.prototype在手动改变原型对象时,可能会失效。

function P() {};
var p1 = new P();

function C() {};
// 修改构造函数的prototype属性的值为p1
C.prototype = p1; //也就是说,此后所有有C构造函数得到的对象的原型对象都是p1;

var c1 = new C();

console.log(c1.constructor.prototype === p1) // false

推举设置获取实例对象的原型的方式

Object.getPrototypeOf(实例对象) 方法返回一个对象的原型对象。

这是获取原型对象的标准方法。

function Cat(name, color) {
    this.name = name;
}
var cat1 = new Cat('猫'); //获取cat1对象的原型对象
var s = Object.getPrototypeOf(cat1); 
console.log(s);

Object.setPrototypeOf(实例对象,原型对象) 为现有对象设置原型对象
第一个是实例对象,第二个是要设置成为实例对象的原型对象的对象
这是设置原型对象的标准方法。

function Cat(name) {
    this.name = name;
}
var ob = {p:'波斯'};
var cat1 = new Cat('猫'); 
//设置cat1的原型对象为ob 
Object.setPrototypeOf(cat1,ob); 
console.log(cat1.p);//cat1的原型对象中有p属性 
console.log(Object.getPrototypeOf(cat1));
console.log(cat1.__proto__);
//注意:如果对象的原型被改变,不会影响构造函数获取的原型的结果
console.log(Cat.prototype == cat1.__proto__); //false

以上的两中方法,都是在ES6新标准中添加的;

原型及原型链

所有对象都有原型对象;

function Cat(name, color) {
    this.name = name;
}

var cat1 = new Cat('猫');
console.log(cat1.__proto__.__proto__.__proto__);

而原型对象中的属性和方法,都可以被实例对象直接使用;

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

  • 搜索首先从对象实例本身开始

  • 如果在实例中找到了具有给定名字的属性,则返回该属性的值

  • 如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性

  • 如果在原型对象中找到了这个属性,则返回该属性的值

  • 如果还是找不到,就到原型的原型去找,依次类推。

  • 如果直到最顶层的Object.prototype还是找不到,则返回undefined。

而这正是多个对象实例共享原型所保存的属性和方法的基本原理。

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

注意,不在要原型上形成多层链式查找,非常浪费资源

这里写图片描述

更简单的原型语法

我们注意到,前面例子中每添加一个属性和方法就要敲一遍 构造函数.prototype
为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:

function Person (name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype = {
    type: 'human',
    sayHello: function () {
        console.log('我叫' + this.name + ',我今年' + this.age + '岁了');
    }
}

在该示例中,我们将 Person.prototype 重置到了一个新的对象。
这样做的好处就是为 Person.prototype 添加成员简单了,但是也会带来一个问题,那就是原型对象丢失了 constructor 成员(构造函数)。

所以,我们为了保持 constructor 的指向正确,建议的写法是:

function Person (name, age) {
    this.name = name
    this.age = age
}

Person.prototype = {
    // 将这个对象的构造函数指向Person
    //constructor: Person, // => 手动将 constructor 指向正确的构造函数
    type: 'human',
    sayHello: function () {
        console.log('我叫' + this.name + ',我今年' + this.age + '岁了');
    }
}

var p = new Person();

原生对象的原型

所有构造函数都有prototype属性;

  • Object.prototype
  • Function.prototype
  • Array.prototype
  • String.prototype
  • Number.prototype
  • Date.prototype

为内置对象扩展原型方法:

例:

var ar = [1,5,23,15,5];

function f(){
    var minarr = [];
    this.forEach(function(v,k){
        if(v<10){
            minarr.push(v);
        }
    })
    return minarr;
}

Object.getPrototypeOf(ar).min10 = f;
console.log(ar.min10());//[1, 5, 5]

// 其他数组对象也具有相应的方法
var a = [1,2,34,7];
console.log(a.min10()); //[1, 2, 7]

这种技术被称为猴子补丁,并且会破坏封装。尽管一些流行的框架(如 Prototype.js)在使用该技术,但仍然没有足够好的理由使用附加的非标准方法来混入内置原型。

原型对象的问题及使用建议

性能问题:

在原型链上查找属性时是比较消耗资源的,对性能有副作用,这在性能要求苛刻的情况下很重要。

另外,试图访问不存在的属性时会遍历整个原型链

//声明构造函数Man
function Man(name){
    this.name = name;
    this.p = function(){
        console.log(this.name+'跑');
    }
}

var m = new Man('张三');

console.log(m.hasOwnProperty('name')); // true
console.log(m.hasOwnProperty('age')); //false

对象实例的hasOwnProperty方法返回一个布尔值,用于判断某个属性定义在对象自身,还是定义在原型链上。

hasOwnProperty 是 判断对象自身有没有某个属性;

注意:检查属性是否undefined还不够。该属性可能存在,但其值恰好设置为undefined

//声明构造函数Man
function Man(name){
    this.name = name;
    this.n = undefined;
    this.p = function(){
        console.log(this.name+'跑');
    }
}

var m = new Man('张三');

if(m.n == undefined){
    console.log('没有n属性')
}

console.log(m.hasOwnProperty('name')); // true
console.log(m.hasOwnProperty('name')); // true
console.log(m.hasOwnProperty('n')); //true

猜你喜欢

转载自blog.csdn.net/github_27314097/article/details/81699071