Skip to content

原型与继承

原型对象

每个对象都有一个原型prototype对象,通过函数创建的对象也将拥有这个原型对象。原型是一个指向对象的指针。

  • 原型包含 constructor 属性,指向构造函数
  • 对象包含 __proto__ 指向他的原型对象

默认情况下创建的对象都有原型

javascript
let hd = { name: "love" };
console.log(hd);

以下 x、y 的原型都为元对象 Object,即 JS 中的根对象

javascript
let x = {};
let y = {};
console.log(Object.getPrototypeOf(x) == Object.getPrototypeOf(y)); //true

极简对象(纯数据字典对象)没有原型(原型为 null)

js
let xj = Object.create(null, {
    name: {
        value: "love"
    }
})
console.log(xj) //{name: 'love'}
console.log(xj.hasOwnProperty("name")) //Error

关系图

proto-link

下面使用 setPrototypeOfgetPrototypeOf 获取与设置原型

javascript
let hd = {};
let parent = { name: "parent" };
Object.setPrototypeOf(hd, parent);
console.log(hd);
console.log(Object.getPrototypeOf(hd));

constructor 存在于 prototype 原型中,用于指向构建函数的引用。

javascript
function hd() {
  this.show = function() {
    return "show method";
  };
}
const obj = new hd(); //true
console.log(obj instanceof hd);

const obj2 = new obj.constructor();
console.dir(obj2.show()); //show method

使用对象的 constructor 创建对象,即构造函数的的原型可以通过实例对象的反向找到。函数默认prototype 指包含一个属性 constructor 的对象,constructor 指向当前构造函数

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

function createByObject(obj, ...args) {
  const constructor = Object.getPrototypeOf(obj).constructor;
  return new constructor(...args);
}

let hd = new User("zhangsan");
let xj = createByObject(hd, "lisi", 12);
console.log(xj);

综上:

js
function User(name) {
  this.name = name;
}
let xj = new User("zhangsan");
console.log(xj);
console.log(User.prototype.constructor == User); //true 构造函数原型的constructor就是构造函数本身
console.log(xj.__proto__ == User.prototype); //true

let lisi = new xj.constructor("李四");
console.log(lisi.__proto__ == xj.__proto__); //true

原型检测

instanceof 检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

使用isPrototypeOf检测一个对象是否是另一个对象的原型链中

javascript
const a = {};
const b = {};
const c = {};

Object.setPrototypeOf(a, b);
Object.setPrototypeOf(b, c);

console.log(b.isPrototypeOf(a)); //true
console.log(c.isPrototypeOf(a)); //true
console.log(c.isPrototypeOf(b)); //true

属性遍历

使用in 检测原型链上是否存在属性,使用 hasOwnProperty 只检测当前对象。

使用 for/in 遍历时同时会遍历原型上的属性如下例

hasOwnProperty 方法判断对象是否存在属性,而不会查找原型。

借用原型

使用 callapply 可以借用其他原型方法完成功能。

this

this 不受原型继承影响,this 指向调用属性时使用的对象。

原型总结

prototype

prototype是函数对象独有的属性,它指向了一个原型对象。每个函数对象都有一个prototype属性,但它只有在这个函数作为构造函数使用时才有意义。

js
function Person() {}

console.log(Person.prototype); // 输出:Person {}

prototype属性的主要作用是在构造函数模式下,用于构建实例对象的原型链。当我们使用构造函数来创建一个实例对象时,它的__proto__属性会指向构造函数的prototype属性。

js
let person = new Person();
console.log(person.__proto__ === Person.prototype); // 输出:true

上面的代码中,我们使用了Person构造函数来创建了一个对象person。结果表明,person.__proto__指向了Person.prototype

Object.create

使用Object.create创建一个新对象时使用现有对象做为新对象的原型对象,第二个参数设置新对象的属性

javascript
let user = {
  show() {
    return this.name;
  }
};
let hd = Object.create(user, {
  name: {
    value: "you"
  }
});
hd.name = "love";
console.log(hd.show());

proto

__proto__是对象的一个内置属性,它用于指向该对象的原型。每个对象都有一个__proto__属性,包括自定义对象、内置对象和函数对象。通过__proto__属性,我们可以访问和操作对象的原型链。

js
let obj = {};
console.log(obj.__proto__); // 输出:Object {}

let arr = [];
console.log(arr.__proto__); // 输出:Array []

function func() {}
console.log(func.__proto__); // 输出:[Function]

上面的代码中,我们创建了一个空对象obj,并访问了它的__proto__属性。可以看到,obj.__proto__指向了一个Object{}对象。同样,我们还创建了一个空数组arr,并访问了它的__proto__属性,结果是arr.__proto__指向了一个Array []对象。而对于函数对象func来说,它的__proto__指向的是一个[Function]对象。

总结起来,__proto__属性用于指向对象的原型,我们可以通过它来访问和操作原型链。

在实例化对象上存在 proto 记录了原型,所以可以通过对象访问到原型的属性或方法。

  • __proto__ 不是对象属性,理解为prototypegetter/setter 实现,他是一个非标准定义
  • __proto__ 内部使用getter/setter 控制值,所以只允许对象或 null
  • 建议使用 Object.setPrototypeOfObject.getProttoeypOf 替代 __proto__

下面修改对象的 __proto__ 是不会成功的,因为_proto__ 内部使用getter/setter 控制值,所以只允许对象或 null

javascript
let xj = {};
xj.__proto__ = "love";
console.log(xj);

下面定义的__proto__ 就会成功,因为这是一个极简对象,没有原型对象所以不会影响__proto__赋值。

javascript
let hd = Object.create(null);
hd.__proto__ = "love";
console.log(hd); //{__proto__: "love"}

__proto__和prototype都与对象的原型相关,它们之间的联系和区别如下:

  1. __proto__是实例对象的属性,用于指向该对象的原型;而prototype是构造函数的属性,用于指向构造函数的原型对象。

  2. __proto__是读取并访问对象的原型链的属性,可以在实例对象上直接访问;而prototype是构造函数的属性,只能在构造函数内部访问。

  3. __proto__是非标准的属性,只有部分浏览器支持;而prototype是标准属性,所有的对象和函数都有。

  4. __proto__是获取对象原型的,prototype是为对象添加原型的属性和方法的。

实例与原型的关系‌:当一个对象被创建时,它的__proto__属性会指向其构造函数的prototype属性。这意味着每个对象的__proto__属性实际上就是其构造函数的prototype属性‌。例如,如果有一个构造函数Person,创建了一个实例person1,那么person1.__proto__ === Person.prototype将会返回true

原型中保存引用类型会造成对象共享属性,所以一般只会在原型中定义方法。

javascript
function User() {}
User.prototype = {
  lessons: ["JS", "VUE"]
};
const lisi = new User();
const wangwu = new User();

lisi.lessons.push("CSS");

console.log(lisi.lessons); //["JS", "VUE", "CSS"]
console.log(wangwu.lessons); //["JS", "VUE", "CSS"]

为 Object 原型对象添加方法,将影响所有函数

html
<body>
  <button onclick="this.hide()">love</button>
</body>
<script>
  Object.prototype.hide = function() {
    this.style.display = "none";
  };
</script>

对象设置属性,只是修改对象属性并不会修改原型属性,使用hasOwnProperty 判断对象本身是否含有属性并不会检测原型。

javascript
function User() {}
const lisi = new User();
const wangwu = new User();

lisi.name = "小明";
console.log(lisi.name);
console.log(lisi.hasOwnProperty("name"));

//修改原型属性后
lisi.__proto__.name = "张三";
console.log(wangwu.name);

//删除对象属性后
delete lisi.name;
console.log(lisi.hasOwnProperty("name"));
console.log(lisi.name);

使用 in 会检测原型与对象,而 hasOwnProperty 只检测对象,所以结合后可判断属性是否在原型中

javascript
function User() {
}
User.prototype.name = "后盾人";
const lisi = new User();
//in会在原型中检测
console.log("name" in lisi);
//hasOwnProperty 检测对象属性
console.log(lisi.hasOwnProperty("name"));

使用建议

通过前介绍我们知道可以使用多种方式设置原型,下面是按时间顺序的排列

  1. prototype 构造函数的原型属性
  2. Object.create 创建对象时指定原型
  3. __proto__ 声明自定义的非标准属性设置原型,解决之前通过 Object.create 定义原型,而没提供获取方法
  4. Object.setPrototypeOf 设置对象原型

这几种方式都可以管理原型,一般来讲使用 prototype 更改构造函数原型,使用 Object.setPrototypeOfObject.getPrototypeOf 获取或设置原型。

构造函数

使用优化

使用构造函数会产生函数复制造成内存占用,及函数不能共享的问题。

javascript
function User(name) {
  this.name = name;
  this.get = function() {
    return this.name;
  };
}
let lisi = new User("小明");
let wangwu = new User("王五");
console.log(lisi.get == wangwu.get); //false


//优化后
function User(name) {
    this.name = name
}
User.prototype.get = function () {
    return this.name
}
let lisi = new User("小明")
let wangwu = new User("王五")
console.log(lisi.get == wangwu.get) //true


//通过修改原型方法会影响所有对象调用,因为方法是共用的
lisi.__proto__.get = function () {
    return "love" + this.name
}
console.log(lisi.get())
console.log(wangwu.get());

使用Object.assign一次设置原型方法来复用,后面会使用这个功能实现 Mixin 模式

javascript
function User(name, age) {
  this.name = name;
  this.age = age;
}
Object.assign(User.prototype, {
  getName() {
      return this.name;
  },
  getAge() {
      return this.age;
  }
});
let lisi = new User('李四', 12);
let xiaoming = new User('小明', 32);
console.log(lisi.getName()); //李四
console.log(lisi.__proto__)

我们希望调用父类构造函数完成对象的属性初始化,但像下面这样使用是不会成功的。因为此时 this 指向了 window,无法为当前对象声明属性。

javascript
function User(name) {
  this.name = name;
  console.log(this);// Window
}
User.prototype.getUserName = function() {
  return this.name;
};

function Admin(name) {
  User(name);
}
Admin.prototype = Object.create(User.prototype);
Admin.prototype.role = function() {};

let xj = new Admin("love");
console.log(xj.getUserName()); //undefined

解决上面的问题是使用 call/apply 为每个生成的对象设置属性

javascript
function User(name) {
  this.name = name;
  console.log(this); // Admin
}
User.prototype.getUserName = function() {
  return this.name;
};

function Admin(name) {
  User.call(this, name);
}
Admin.prototype = Object.create(User.prototype);

let xj = new Admin("love");
console.log(xj.getUserName()); //love

Mixin 模式

JS不能实现多继承,如果要使用多个类的方法时可以使用mixin混合模式来完成。

下面分拆功能使用 Mixin 实现多继承,使用代码结构更清晰。只让 Admin 继承 User 原型

javascript
function extend(sub, sup) {
  sub.prototype = Object.create(sup.prototype);
  sub.prototype.constructor = sub;
}
function User(name, age) {
  this.name = name;
  this.age = age;
}
User.prototype.show = function() {
  console.log(this.name, this.age);
};
const Credit = {
  total() {
    console.log("统计积分");
  }
};
const Request = {
  ajax() {
    console.log("请求后台");
  }
};

function Admin(...args) {
  User.apply(this, args);
}
extend(Admin, User);
Object.assign(Admin.prototype, Request, Credit);
let hd = new Admin("向军", 19);
hd.show();
hd.total(); //统计积分
hd.ajax(); //请求后台

mixin 类也可以继承其他类,比如下面的 Create 类获取积分要请求后台,就需要继承 Request 来完成。

  • super 是在 mixin 类的原型中查找,而不是在 User 原型中
javascript
function extend(sub, sup) {
  sub.prototype = Object.create(sup.prototype);
  sub.prototype.constructor = sub;
}
function User(name, age) {
  this.name = name;
  this.age = age;
}
User.prototype.show = function() {
  console.log(this.name, this.age);
};
const Request = {
  ajax() {
    return "请求后台";
  }
};
const Credit = {
  __proto__: Request,
  total() {
    console.log(super.ajax() + ",统计积分");
  }
};

function Admin(...args) {
  User.apply(this, args);
}
extend(Admin, User);
Object.assign(Admin.prototype, Request, Credit);
let hd = new Admin("you", 19);
hd.show();
hd.total(); //统计积分
hd.ajax(); //请求后台