跳至主要內容

第七章 原型

前端JavaScriptJavaScriptweb大约 8 分钟

一、原型对象

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

  • 可以将原型理解为对象的父亲,对象从原型对象继承来属性
  • 原型就是对象除了是某个对象的父母外没有什么特别之处
  • 所有函数的原型默认是 Object的实例,所以可以使用toString/toValues/isPrototypeOf 等方法的原因
  • 使用原型对象为多个对象共享属性或方法
  • 如果对象本身不存在属性或方法将到原型上查找
  • 使用原型可以解决,通过构建函数创建对象时复制多个函数造成的内存占用问题
  • 原型包含 constructor 属性,指向构造函数
  • 对象包含 __proto__ 指向他的原型对象
  • 函数有一个 prototype 属性,当函数作为构造函数时,new 出来的对象的__proto__指向 prototype

下例使用的就是数组原型对象的 concat 方法完成的连接操作

let zn = ["a"];
console.log(zn.concat("b"));
console.log(zn);

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

let zn = { name: "楠哥" };
console.log(zn);

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

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

我们也可以创建一个极简对象(纯数据字典对象)没有原型(原型为 null)

let zn = { name: 3 };
console.log(zn.hasOwnProperty("name"));

let ng = Object.create(null, {
  name: {
    value: "楠哥",
  },
});
console.log(ng.hasOwnProperty("name")); //Error

//Object.keys是静态方法,不是原型方法所以是可以使用的
console.log(Object.keys(ng));

函数拥有多个原型,prototype 用于实例对象使用,__proto__用于函数对象使用

function User() {}
User.__proto__.view = function () {
  console.log("User function view method");
};
User.view();

User.prototype.show = function () {
  console.log("楠哥");
};
let zn = new User();
zn.show();
console.log(User.prototype == zn.__proto__);

下面是原型关系分析,与方法继承的示例

let zn = new Object();
zn.name = "楠哥";
Object.prototype.show = function () {
  console.log("hodunren.com");
};
zn.show();

function User() {}
let ng = new User();
ng.show();
User.show();

下面是使用构造函数创建对象的原型体现

  • 构造函数拥有原型
  • 创建对象时构造函数把原型赋予对象
function User() {}
let ng = new User();
console.log(ng.__proto__ == User.prototype);

下面使用数组会产生多级继承继原型链

let zn = [];
console.log(zn);
console.log(zn.__proto__ == Array.prototype);

let str = "";
console.log(str.__proto__ == String.prototype);

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

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

使用自定义构造函数创建的对象的原型体现

function User() {}
let zn = new User();
console.log(zn);

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

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

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

使用对象的 constructor 创建对象

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 zn = new User("楠哥");
let ng = createByObject(zn, "楠哥", 12);
console.log(ng);

二、原型链

通过引用类型的原型,继承另一个引用类型的属性与方法,这也是实现继承的步骤。

使用Object.setPrototypeOf 可设置对象的原型,下面的示例中继承关系为 obj>zn>cms。

Object.getPrototypeOf 用于获取一个对象的原型。

let obj = {
  name: "楠哥",
};
let zn = {
  web: "xinzhi",
};
let cms = {
  soft: "zncms",
};
//让obj继承zn,即设置obj的原型为zn
Object.setPrototypeOf(obj, zn);
Object.setPrototypeOf(zn, cms);
console.log(obj.web);
console.log(Object.getPrototypeOf(zn) == cms); //true

三、原型检测

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

function A() {}
function B() {}
function C() {}

const c = new C();
B.prototype = c;
const b = new B();
A.prototype = b;
const a = new A();

console.dir(a instanceof A); //true
console.dir(a instanceof B); //true
console.dir(a instanceof C); //true
console.dir(b instanceof C); //true
console.dir(c instanceof B); //false

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

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 只检测当前对象

let a = { url: "xinzhi" };
let b = { name: "楠哥" };
Object.setPrototypeOf(a, b);
console.log("name" in a);
console.log(a.hasOwnProperty("name"));
console.log(a.hasOwnProperty("url"));

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

let zn = { name: "楠哥" };
let ng = Object.create(zn, {
  url: {
    value: "xinzhi.com",
    enumerable: true,
  },
});
for (const key in ng) {
  console.log(key);
}

hasOwnProperty 方法判断对象是否存在属性,而不会查找原型。所以如果只想遍历对象属性使用以下代码

let zn = { name: "楠哥" };
let ng = Object.create(zn, {
  url: {
    value: "xinzhi.com",
    enumerable: true,
  },
});
for (const key in ng) {
  if (ng.hasOwnProperty(key)) {
    console.log(key);
  }
}

五、借用原型

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

下面的 ng 对象不能使用max方法,但可以借用 zn 对象的原型方法

let zn = {
  data: [1, 2, 3, 4, 5],
};
Object.setPrototypeOf(zn, {
  max: function () {
    return this.data.sort((a, b) => b - a)[0];
  },
});
console.log(zn.max());

let ng = {
  lessons: { js: 100, php: 78, node: 78, linux: 125 },
  get data() {
    return Object.values(this.lessons);
  },
};
console.log(zn.__proto__.max.apply(ng));

上例中如果方法可以传参,那就可以不在 ng 对象中定义 getter 方法了

let zn = {
  data: [1, 2, 3, 4, 5],
};
Object.setPrototypeOf(zn, {
  max: function (data) {
    return data.sort((a, b) => b - a)[0];
  },
});
console.log(zn.max(zn.data));

let ng = {
  lessons: { js: 100, php: 78, node: 78, linux: 125 },
};
console.log(zn.__proto__.max.call(ng, Object.values(ng.lessons)));

因为 Math.max 就是获取最大值的方法,所以代码可以再次优化

let zn = {
  data: [1, 2, 3, 4, 5],
};
console.log(Math.max.apply(null, Object.values(zn.data)));

let ng = {
  lessons: { js: 100, php: 78, node: 78, linux: 125 },
};
console.log(Math.max.apply(ng, Object.values(ng.lessons)));

下面是获取设置了 class 属性的按钮,但 DOM 节点不能直接使用数组的filter 等方法,但借用数组的原型方法就可以操作了。

<body>
  <button message="楠哥" class="red">楠哥</button>
  <button message="zncms">zncms</button>
</body>
<script>
  let btns = document.querySelectorAll("button");
  btns = Array.prototype.filter.call(btns, item => {
    return item.hasAttribute("class");
  });
</script>

六、prototype

函数也是对象也有原型,函数有 prototype 属性指向他的原型

为构造函数设置的原型指,当使用构造函数创建对象时把这个原型赋予给这个对象

function User(name) {
  this.name = name;
}
User.prototype = {
  show() {
    return this.name;
  },
};
let ng = new User("楠哥");
console.log(ng.show());

函数默认prototype 指包含一个属性 constructor 的对象,constructor 指向当前构造函数

function User(name) {
  this.name = name;
}
let ng = new User("楠哥");
console.log(ng);
console.log(User.prototype.constructor == User); //true
console.log(ng.__proto__ == User.prototype); //true

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

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

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 原型对象添加方法,将影响所有函数

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

了解了原型后可以为系统对象添加方法,比如为字符串添加了一截断函数。

  • 不能将系统对象的原型直接赋值
String.prototype.truncate = function (len = 5) {
  return this.length <= len ? this : this.substr(0, len) + "...";
};
console.log("楠哥每天不断视频教程".truncate(3)); //楠哥...

七、Object.create

使用Object.create创建一个新对象时使用现有对象做为新对象的原型对象

使用Object.create 设置对象原型

let user = {
  show() {
    return this.name;
  },
};

let zn = Object.create(user);
zn.name = "楠哥";
console.log(zn.show());

强以在设置时使用第二个参数设置新对象的属性

let user = {
  show() {
    return this.name;
  },
};
let zn = Object.create(user, {
  name: {
    value: "楠哥",
  },
});
console.log(zn);

八、proto

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

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

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

let ng = {};
ng.__proto__ = "楠哥";
console.log(ng);

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

let zn = Object.create(null);
zn.__proto__ = "楠哥";
console.log(zn); //{__proto__: "楠哥"}

下面通过改变对象的 __proto__ 原型对象来实现继承,继承可以实现多层,

let zn = {
  name: "楠哥",
};
let xinzhi = {
  show() {
    return this.name;
  },
};
let ng = {
  handle() {
    return `用户: ${this.name}`;
  },
};
xinzhi.__proto__ = ng;
zn.__proto__ = xinzhi;
console.log(zn.show());
console.log(zn.handle());
console.log(zn);

构造函数中的 __proto__ 使用

function User(name, age) {
  this.name = name;
  this.age = age;
}
User.prototype.show = function () {
  return `姓名:${this.name},年龄:${this.age}`;
};
let lisi = new User("李四", 12);
let xiaoming = new User("小明", 32);
console.log(lisi.__proto__ == User.prototype); //true

可以使用 __proto__Object.setPrototypeOf 设置对象的原型,使用Object.getProttoeypOf 获取对象原型。

function Person() {
  this.getName = function () {
    return this.name;
  };
}
function User(name, age) {
  this.name = name;
  this.age = age;
}
let lisi = new User("李四", 12);
Object.setPrototypeOf(lisi, new Person());
console.log(lisi.getName()); //李四

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

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 只检测对象,所以结合后可判断属性是否在原型中

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 获取或设置原型。