理解JS中的原型(Prototypes)

简介: 全新对象;给对象赋值;Prototypes;设置属性;对象字面量

该文章是直接翻译国外一篇文章,关于JS原型。

都是基于原文处理的,其他的都是直接进行翻译可能有些生硬,所以为了行文方便,就做了一些简单的本地化处理。

如果想直接根据原文学习,可以忽略此文。

全新对象

在JS中,对象是有很多keyvalue构成的一种数据存储结构。例如,如果想描述一个人的基本信息,可以构建一个拥有firstNamelastName的对象,并且分别被赋值为北宸。在JS对象中的key的值是String类型的。

在JS中,可以用Object.create创建一下全新的对象:

//构建了一个空对象
var person = Obeject.create(null);
复制代码

此时有些开发会说,为什么不用var person ={} 来构建一个空对象。其实之所以能用这种处理方式,只是JS引擎给你做了处理。为了能够用最原始的代码去了解原型,我们也需要循序渐进的去接触这些东西。

如果通过一个key遍历对象,但是对象中没有对应的key能进行匹配,JS就会返回一个 undefined

person["name"] //undefined
复制代码

如果key是一个合法的标识符,也可以用如下的语法进行对象数据的访问:

person.name //undefined
复制代码

合法的标识符格式:

in general, an identifier starts with a unicode letter, $, _, followed by any of the starting characters or numbers. A valid identifier must also not be a reserved word. There are other allowed characters, such as unicode combining marks, unicode connecting punctuation, and unicode escape sequences.

给对象赋值

现在你已经有了一个空对象,但是好像并没有啥卵用。在我们为对象新增自定义属性之前,我们还需要对对象的额外属性(named data property)有一个更深的了解。

一般而言,对象的自定义属性有一个name属性和与name属性相对应的value属性。但是自定义属性还可以被enumerableconfigurablewritable这些隐藏属性控制,来在不同的场景中表现出不同的行为。

如果一个自定义属性是enumerable的,通过for(prop in obj)来操作该自定义属性的宿主对象,这个自定义属性会被循环操作捕获。 如果是writable的,你可以对这个自定义属性进行赋值。如果是configurable的,可以对这个自定义属性进行删除或者是改变其他的隐藏属性。

一般在我们创建一个新的自定义属性,我们总是希望这个属性是enumerablewritableconfigurable的。

我们可以利用Object.defineProperty来向对象新增一个自定义属性。

var person = Object.create(null);
Object.defineProperty(person, 'firstName', {
  value: "Yehuda",
  writable: true,
  enumerable: true,
  configurable: true
});
Object.defineProperty(person, 'lastName', {
  value: "Katz",
  writable: true,
  enumerable: true,
  configurable: true
});
复制代码

这样新增属性代码有些冗余,我们可以将配置信息提出来:

var config = {
  writable: true,
  enumerable: true,
  configurable: true
};
var defineProperty = function(obj, name, value) {
  config.value = value;
  Object.defineProperty(obj, name, config);
}
var person = Object.create(null);
defineProperty(person, 'firstName', "Yehuda");
defineProperty(person, 'lastName',   "Katz");
复制代码

虽然代码有一些精简了,但是还是看起来很别扭。所以我们需要一个更加合理或者是更加简洁的方式来定义属性。

Prototypes

从上面我们得知,JS中的对象就是一系列key和对应value组成的数据格式。但是在JS中还存在一个属性(一个指向其他对象的指针)。我们称这个指针为对象的原型(prototype)

如果你在某个对象中,查找一个key,但是在该对象范围内没有找到。JS会继续在prototype所指向的对象中继续寻找。以此类推,直到prototype所指向的对象是一个null,查询结束。并且返回undefined

让我们来回顾一下,在调用Object.create(null)的时候,会发生些什么。 方法中接收一个null为参数。其实也就是说,在利用Object.create(paramsObj)构建出的对象,他的prototype指向了paramsObj

可以通过Object.getPrototyOf来查询指定对象的prototype.

var man = Object.create(null);
defineProperty(man, 'sex', "male");
var yehuda = Object.create(man);
defineProperty(yehuda, 'firstName', "Yehuda");
defineProperty(yehuda, 'lastName', "Katz");
yehuda.sex       // "male"
yehuda.firstName // "Yehuda"
yehuda.lastName  // "Katz"
Object.getPrototypeOf(yehuda) // returns the man object
复制代码

我们可以通过这种方式来新增函数,这样这个函数就会被多处使用:

var person = Object.create(null);
defineProperty(person, 'fullName', function() {
  return this.firstName + ' ' + this.lastName;
});
//将man的prototype指向person ,这样,在man对象还有已man为prototype的对象都可以访问这个函数
var man = Object.create(person);
defineProperty(man, 'sex', "male");
var yehuda = Object.create(man);
defineProperty(yehuda, 'firstName', "Yehuda");
defineProperty(yehuda, 'lastName', "Katz");
yehuda.sex        // "male"
yehuda.fullName() // "Yehuda Katz"
复制代码

设置属性

由于构建一个具有writableconfigurableenumerable属性的对象很常见。所以JS为了让代码看起来简洁,采用另外一种给对象新增属性的方式。

通过简化方式,让代码看起来很短,也不需要额外的处理writableconfigurableenumerable等属性,同时这些属性的值都是true

var person = Object.create(null);
//在此处我们可以直接定义想要给对象新增的属性,而writable,
// configurable, 和 enumerable这些属性由JS统一处理
person['fullName'] = function() {
  return this.firstName + ' ' + this.lastName;
};
//将man的prototype指向person ,这样,在man对象还有已man为prototype的对象都可以访问这个函数
var man = Object.create(person);
man['sex'] = "male";
var yehuda = Object.create(man);
yehuda['firstName'] = "Yehuda";
yehuda['lastName'] = "Katz";
yehuda.sex        // "male"
yehuda.fullName() // "Yehuda Katz"
复制代码

对象字面量

JS提供了一个种字面量语法来构建一个对象。同时可以一次性将所有需要新增的属性都指出并赋初值

var person  ={ firstName: "北宸", lastName: "范" }
复制代码

其实上面的实现是如下代码的"语法糖":

var person = Object.create(Object.prototype);
person.firstName = "北宸";
person.lastName  = "范";
复制代码

person的原型链为Object.prototype。这个可以在控制台中实践一下

Object.prototype对象中包含了很多我们希望在定义的对象中可以通过原型链访问的方法和属性。通过上面的分析可以得知,我们通过字面量构建的对象,它的原型就是Object.prototype

当然,我们也有机会在定义的对象中对存储在Object.prototype中的方法进行重写。

var alex = { firstName: "Alex", lastName: "Russell" };
alex.toString() // "[object Object]"
var brendan = {
  firstName: "北宸",
  lastName: "范",
  toString: function() { return "范北宸"; }
};
brendan.toString() // "范北宸"
复制代码

但是基于字面量构建对象的原型是无法进行指定的。也就是说,字面量构建的对象的原型永远都是Object.prototype。这样做的话,就无法利用原型来分享共有属性和方法。所以,我们需要对这种模式进行改进。

var fromPrototype = function(prototype, object) {
//用于将自定义的原型和目标对象进行关联,这样在新的对象中就可以访问原型中的方法和属性(原型搭建)
  var newObject = Object.create(prototype);
    //遍历目标对象,将属于目标对象中的属性都复制到新对象中,(属性迁移)
  for (var prop in object) {
    if (object.hasOwnProperty(prop)) {
      newObject[prop] = object[prop];      
    }
  }
  return newObject;
};
var person = {
  toString: function() {
    return this.firstName + ' ' + this.lastName;
  }
};
var man = fromPrototype(person, {
  sex: "male"
});
var jeremy = fromPrototype(man, {
  firstName: "北宸",
  lastName:  "范"
});
jeremy.sex        // "male"
jeremy.toString() // "范北宸"
复制代码

如上的对象构建的对象结构如下:

基于原型的面向对象编程

有一点很明确,原型(prototype)可以用于继承(继承是OOP语言最明显的特点之一)。为了利用原型来实现继承,JS提供了new操作符。

为了实现面向对象编程,JS允许你使用一个函数对象将prototypeconstructor封装起来。

//Person的constructor
var Person = function(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}
//Person的prototype
Person.prototype = {
  toString: function() { return this.firstName + ' ' + this.lastName; }
}
复制代码

自此,我们就实现了一个用于作为constructor函数对象还有作为新对象的prototype的对象。

让我们通过构建一个函数来模拟new的操作流程。它的主要目的就是为了新建指定的构造函数的实例.

function newObject(func) {
  // 构建函数的参数list
  var args = Array.prototype.slice.call(arguments, 1);
  // 基于构造函数的原型构建一个对象
  var object = Object.create(func.prototype);
  // 由于构造函数中存在this的值,所以在构建实例的时候,需要将this的指向实例对象
  func.apply(object, args);
  // 返回基于指定构造函数构建的新对象
  return object;
}
var brendan = newObject(Person, "范", "北宸");
brendan.toString() // "范北宸"
复制代码

Note:这里func.apply(object, args)的操作有一个this指向问题。可以参考理解JS函数调用和"this"

在JS实际构建对象中,用的是new

var mark = new Person("范", "北宸");
mark.toString() // "范北宸"
复制代码

Note:关于new的运行机制,大致如下:

  1. 创建一个空对象,作为将要返回的对象实例。
  2. 将这个空对象的原型,指向构造函数的prototype属性。
  3. 将这个空对象赋值给函数内部的this关键字。
  4. 开始执行构造函数内部的代码。

本质上,JS函数中的"class"其实就是一个函数对象作为构造函数同时附带上一个prototype对象。

相关文章
|
7月前
|
JavaScript 前端开发
js开发:请解释原型继承和类继承的区别。
JavaScript中的原型继承和类继承用于共享对象属性和方法。原型继承通过原型链实现共享,节省内存,但不支持私有属性。
58 0
|
7月前
|
JavaScript 前端开发 Java
深入JS面向对象(原型-继承)(三)
深入JS面向对象(原型-继承)
55 0
|
1月前
|
JavaScript 前端开发
JavaScript中的原型 保姆级文章一文搞懂
本文详细解析了JavaScript中的原型概念,从构造函数、原型对象、`__proto__`属性、`constructor`属性到原型链,层层递进地解释了JavaScript如何通过原型实现继承机制。适合初学者深入理解JS面向对象编程的核心原理。
25 1
JavaScript中的原型 保姆级文章一文搞懂
|
4月前
|
JavaScript 前端开发
如何在JavaScript中实现基于原型的继承机制
【8月更文挑战第14天】如何在JavaScript中实现基于原型的继承机制
31 0
|
3月前
|
JSON JavaScript 前端开发
js原型继承|26
js原型继承|26
|
3月前
|
JavaScript 前端开发
JavaScript prototype(原型对象)
JavaScript prototype(原型对象)
36 0
|
3月前
|
JavaScript 前端开发
JavaScript基础知识-原型(prototype)
关于JavaScript基础知识中原型(prototype)概念的介绍。
43 1
|
4月前
|
JavaScript 前端开发
JavaScript中什么是原型?有什么用?
JavaScript中什么是原型?有什么用?
24 1
|
4月前
|
JavaScript 前端开发 Java
什么是JavaScript原型对象
【8月更文挑战第2天】什么是JavaScript原型对象
65 9
|
6月前
|
设计模式 JavaScript 前端开发
【JavaScript】深入浅出JavaScript继承机制:解密原型、原型链与面向对象实战攻略
JavaScript的继承机制基于原型链,它定义了对象属性和方法的查找规则。每个对象都有一个原型,通过原型链,对象能访问到构造函数原型上的方法。例如`Animal.prototype`上的`speak`方法可被`Animal`实例访问。原型链的尽头是`Object.prototype`,其`[[Prototype]]`为`null`。继承方式包括原型链继承(通过`Object.create`)、构造函数继承(使用`call`或`apply`)和组合继承(结合两者)。ES6的`class`语法是语法糖,但底层仍基于原型。继承选择应根据需求,理解原型链原理对JavaScript面向对象编程至关重要
153 7
【JavaScript】深入浅出JavaScript继承机制:解密原型、原型链与面向对象实战攻略
下一篇
DataWorks