JS基础原型原型链真的不能一知半解

12次阅读

共计 7265 个字符,预计需要花费 19 分钟才能阅读完成。

JS 的原型、原型链一直是比较难理解的内容,不少初学者甚至有一定经验的老鸟都不一定能完全说清楚,更多的 ” 很可能 ” 是一知半解,而这部分内容又是 JS 的核心内容,想要技术进阶的话肯定不能对这个概念一知半解,碰到问题靠“猜”,却不理解它的规则!

prototype

只有函数有 prototype 属性

let a = {}
let b = function () {}
console.log(a.prototype) // undefined
console.log(b.prototype) // {constructor: function(){...} }

Object.prototype 怎么解释?

其实 Object 是一个全局对象,也是一个构造函数,以及其他基本类型的全局对象也都是构造函数:

function outTypeName(data, type) {let typeName =  Object.prototype.toString.call(data)
    console.log(typeName)
}
outTypeName(Object) //[object Function]
outTypeName(String) // [object Function]
outTypeName(Number) // [object Function]

为什么只有函数有 prototype 属性

JS 通过 new 来生成对象,但是仅靠构造函数,每次生成的对象都不一样。

有时候需要在两个对象之间共享属性,由于 JS 在设计之初没有类的概念,所以 JS 使用函数的 prototype 来处理这部分 需要被共享的属性 ,通过函数的prototype 来模拟类:

当创建一个函数时,JS 会自动为函数添加 prototype 属性,值是一个有 constructor 的对象。

以下是共享属性 prototype 的栗子:

function People(name) {this.name = name}
People.prototype.age = 23 // 岁数
// 创建两个实例
let People1 = new People('OBKoro1')
let People2 = new People('扣肉')
People.prototype.age = 24 // 长大了一岁
console.log(People1.age, People2.age) // 24 24

为什么 People1People2可以访问到People.prototype.age

原因是:People1People2 的原型是People.prototype,答案在下方的:构造函数是什么以及它做了什么。

原型链

__proto__Object.getPrototypeOf(target):对象的原型

__proto__ 是对象实例和它的构造函数之间建立的链接,它的值是:构造函数的 `prototype。

也就是说:__proto__ 的值是它所对应的原型对象,是某个函数的prototype

Object.getPrototypeOf(target)全等于__proto__

它是 ES6 的标准,兼容 IE9,主流浏览器也都支持,MDN,本文将以 Object.getPrototypeOf(target) 指代__proto__

不要再使用__proto__:

本段摘自阮一峰 -ES6 入门,具体解析请点击链接查看

  1. __proto__属性没有写入 ES6 的正文,而是写入了附录。
  2. 原因是它本质上是一个内部属性,而 不是一个正式的对外的 API,只是由于浏览器广泛支持,才被加入了 ES6
  3. 标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,而且新的代码最好认为这个属性是不存在的
  4. 所以无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,应该使用:Object.getPrototypeOf(target)(读操作)、Object.setPrototypeOf(target)(写操作)、Object.create(target)(生成操作)代替

构造函数是什么、它做了什么

出自《你不知道的在 js》:在 js 中, 实际上并不存在所谓的 ’ 构造函数 ’,只有对于函数的 ’ 构造调用 ’。

上文一直提到构造函数,所谓的构造函数,实际上就是通过关键字 new 来调用的函数:

let newObj = new someFn() // 构造调用函数

构造 /new 调用函数的时候做了什么

  1. 创建一个全新的对象。
  2. 这个新对象的原型 (Object.getPrototypeOf(target)) 指向构造函数的 prototype 对象。
  3. 该函数的 this 会绑定在新创建的对象上。
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
  5. 我们称这个新对象为构造函数的实例。

原型继承就是利用构造调用函数的特性

SubType.prototype = new SuperType();  // 原型继承:SubType 继承 SuperType
// 挂载 SuperType 的 this 和 prototype 的属性和方法到 SubType.prototype 上
  1. 构造调用的第二点:将新对象的 Object.getPrototypeOf(target) 指向函数的prototype
  2. 构造调用的第三点:该函数的 this 会绑定在新创建的对象上。
  3. 新对象赋值给SubType.prototype

原型类型有个缺点:多个实例对引用类型的操作会被篡改。

因为每次实例化引用类型的数据都指向同一个地址,所以它们读 / 写的是同一个数据,当一个实例对其进行操作,其他实例的数据就会一起更改。

原型链是什么

来看个例子:

function foo() {}
const newObj = new foo() // 构造调用 foo 返回一个新对象
const newObj__proto__ = Object.getPrototypeOf(newObj) // 获取 newObj 的原型对象
newObj__proto__ === foo.prototype // true 验证 newObj 的原型指向 foo
const foo__proto__ = Object.getPrototypeOf(foo.prototype) // 获取 foo.prototype 的原型
foo__proto__ === Object.prototype // true foo.prototype 的原型是 Object.prototype
``
如果用以前的语法,从 `newObj` 查找 `foo` 的原型,是这样的:

newObj.__proto__.__proto__ // 这种关系就是原型链


** 可以用以下三句话来理解原型链 **:1. ** 每个对象都拥有一个原型对象 **: `newObj` 的原型是 `foo.prototype`。2. ** 对象的原型可能也是继承其他原型对象的 **: `foo.prototype` 也有它的原型 `Object.prototype`。3. ** 一层一层的,以此类推,这种关系就是原型链 **。### 一个对象是否在另一个对象的原型链上

> 如果一个对象存在另一个对象的原型链上,我们可以说:它们是继承关系。判断方式有两种,但都是根据构造函数的 `prototype` 是否在原型链上来判断的:1. `instanceof `: 用于测试构造函数的 prototype 属性是否出现在对象的原型链中的任何位置

语法:`object instanceof constructor`

let test = function () {}
let testObject = new test();
testObject instanceof test // true test.prototype 在 testObject 的原型链上
testObject instanceof Function // false Function.prototype 不在 testObject 的原型链上
testObject instanceof Object // true Object.prototype 在 testObject 的原型链上

2. `isPrototypeOf `:测试一个对象是否存在于另一个对象的原型链上

语法:`prototypeObj.isPrototypeOf(object)`

let test = function () {}
let testObject = new test();
test.prototype.isPrototypeOf(testObject) // true test.prototype 在 testObject 的原型链上
Object.prototype.isPrototypeOf(testObject) // true Object.prototype 在 testObject 的原型链上


### 原型链的终点: `Object.prototype`

`Object.prototype` 是原型链的终点,所有对象都是从它继承了方法和属性。**`Object.prototype` 没有原型对象 **:

const proto = Object.getPrototypeOf(Object.prototype) // null

下面是两个验证例子,有疑虑的同学多写几个测试用例印证一下。** 字符串原型链的终点 **:`Object.prototype`

let test = ‘ 由 String 函数构造出来的 ’
let stringPrototype = Object.getPrototypeOf(test) // 字符串的原型
stringPrototype === String.prototype // true 字符串的原型是 String 对象
Object.getPrototypeOf(stringPrototype) === Object.prototype // true String 对象的原型是 Object 对象


** 函数原型链的终点 **:`Object.prototype`

let test = function () {}
let fnPrototype = Object.getPrototypeOf(test)
fnPrototype === Function.prototype // true test 的原型是 Function.prototype
Object.getPrototypeOf(Function.prototype) === Object.prototype // true


### 原型链用来做什么?#### 属性查找:> 如果试图访问对象 (实例 instance) 的某个属性, 会首先在对象内部寻找该属性, 直至找不到, 然后才在该对象的原型 (instance.prototype) 里去找这个属性,以此类推

我们用一个例子来形象说明一下:

let test = ‘ 由 String 函数构造出来的 ’
let stringPrototype = Object.getPrototypeOf(test) // 字符串的原型
stringPrototype === String.prototype // true 字符串的原型是 String 对象
Object.getPrototypeOf(stringPrototype) === Object.prototype // true String 对象的原型是 Object 对象

当你访问 `test` 的某个属性时,浏览器会进行以下查找:1. 浏览器首先查找 `test` 本身
2. 接着查找它的原型对象:`String.prototype`
3. 最后查找 `String.prototype` 的原型对象:`Object.prototype`
4. 一旦在原型链上找到该属性,就会立即返回该属性,停止查找。5. 原型链上的原型都没有找到的话,返回 `undefiend`

这种查找机制还解释了字符串为何会有自带的方法: `slice`/`split`/`indexOf` 等。准确的说:* 这些属性和方法是定义在 `String` 这个全局对象 / 函数上的。* 字符串的原型指向了 `String` 函数的 `prototype`。* 之后通过查找原型链,在 String 函数的 `prototype` 中找到这些属性和方法。#### 拒绝查找原型链:`hasOwnProperty`: 指示对象自身属性中是否具有指定的属性

语法:`obj.hasOwnProperty(prop)`

参数: `prop` 要查找的属性

返回值: 用来判断某个对象是否含有指定的属性的 `Boolean`。

let test ={‘OBKoro1’: ‘ 扣肉 ’}
test.hasOwnProperty(‘OBKoro1’); // true
test.hasOwnProperty(‘toString’); // false test 本身没查找到 toString


这个 `API` 是挂载在 `object.prototype` 上,所有对象都可以使用,API 会忽略掉那些从原型链上继承到的属性。## 扩展:### 实例的属性

你知道构造函数的实例对象上有哪些属性吗?这些属性分别挂载在哪个地方?原因是什么?

function foo() {

this.some = '222'
let ccc = 'ccc'
foo.obkoro1 = 'obkoro1'
foo.prototype.a = 'aaa'

}
foo.koro = ‘ 扣肉 ’
foo.prototype.test = ‘test’
let foo1 = new foo() // foo1上有哪些属性, 这些属性分别挂载在哪个地方
foo.prototype.test = ‘test2’ // 重新赋值


上面这道是考察 `JS` 基础的题,很多人都没说对,原因是没有彻底掌握 `this`、` 原型链 `、` 函数 `。#### 想一下再看解析:#### 想一下再看解析:#### 想一下再看解析:#### 想一下再看解析:#### 想一下再看解析:1. `this.some`:`foo1` 对象的属性

通过构造调用 `foo` 的 `this` 指向 `foo1`,所以 `this.some` 挂载在 `foo1` 对象下。属性查找: `foo1.some`

 `foo1.some` 直接读取 `foo1` 的属性。2. `foo1.test`、`foo1.a`:`foo1` 对象的原型

根据上文提到的:构造 /new 调用函数的时候会创建一个新对象 (`foo1`),自动将 `foo1` 的原型(`Object.getPrototypeOf(foo1)`) 指向构造函数的 prototype 对象。构造调用会执行函数,所以 ` foo.prototype.a = 'aaaaa'` 也会执行,单就赋值这个层面来说写在 `foo` 外面和写在 `foo` 里面是一样的。属性查找:`foo1.test`、`foo1.a`

* `foo1` 本身没有找到, 继续查找
* `foo1` 的原型 `Object.getPrototypeOf(foo1)` 上找到了 `a` 和 `test`,返回它们,停止查找。3. `foo1.obkoro1` 和 `foo1.koro`:返回 undefined

#### 静态属性: `foo.obkoro1`、`foo.koro`

> 函数在 JS 中是一等公民,它也是一个对象, 用来模拟类。这两个属性跟 `foo1` 没有关系,它是对象 `foo` 上的两个属性(类似函数的:`arguments`/`prototype`/`length` 等属性),称为 ** 静态属性 **。它们只能通过 `foo.obkoro1` 和 `foo.koro` 来访问。#### 原型对象改变,原型链下游获取的值也会改变 

上面那个例子中的 `foo1.test` 的值是什么?

foo.prototype.test = ‘test’
let foo1 = new foo() // foo1上有哪些属性, 这些属性分别挂载在哪个地方
foo.prototype.test = ‘test2’ // 重新赋值


`foo1.test` 的值是 `test2`,原因是:`foo1` 的原型对象是 `Object.getPrototypeOf(foo1)` 存的指针,指向 `foo.prototype` 的内存地址,不是拷贝,每次读取的值都是当前 `foo.prototype` 的最新值。** 打印 `foo1`**:![](https://github.com/OBKoro1/articleImg_src/blob/master/2019/2019_8_25_prototype.png?raw=true)

### 小结

写了好几天,之前网上很多图文博客,那些线指来指去,就我个人看来还是比较难以理解的,所以本文纯文字的形式来描述这些概念,相信认真看完的同学肯定都有所收获,如果没看懂的话,建议多看几遍,这部分概念真的很重要!PS:实际上还有很多引申出来的东西没写全,准备放到其他文章中去写。#### 觉得我的博客对你有帮助的话,就给我点个 [Star](https://github.com/OBKoro1/web_accumulate) 吧![前端进阶积累](http://obkoro1.com/web_accumulate/)、[公众号](https://user-gold-cdn.xitu.io/2018/5/1/1631b6f52f7e7015?w=344&h=344&f=jpeg&s=8317)、[GitHub](https://github.com/OBKoro1)、wx:OBkoro1、邮箱:obkoro1@foxmail.com
 
 以上 2019/8/25


作者:[OBKoro1](https://github.com/OBKoro1)

参考资料:[MDN: 对象原型](https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects/Object_prototypes)

[JS 原型链与继承别再被问倒了](https://juejin.im/post/58f94c9bb123db411953691b#heading-14)

[从__proto__和 prototype 来深入理解 JS 对象和原型链](https://github.com/creeperyang/blog/issues/9) 

正文完
 0