重学JS(4) - 原型和原型链

455 阅读5分钟

原型和原型链

(当前个人理解,仅仅供参考)

一、函数的调用方式

要知道原型和原型链是什么.首先是需要知道函数的调用方式,所有知识点都不应该是孤立存在的,有了对于函数调用全盘的了解才能够明白原型链产生的过程发生了啥.这里简单做一些表述.

1.1 直接调用方式

function fn() {}
fn()

const a = {
  name: '妈咋',
  fn: function() {
    console.log(this.name)
  }
}
a.fn() // 妈咋

这种方式是绝大多数人学习函数第一个学会的.但是我在看B站某个老师的视频上面有说到,函数直接调用方法,其实默认是fn.apply(null) 来进行调用的.只是隐藏了.apply(null).

但是我自己没有在MDN上面找到答案,不知道是否如此,仅供参考.

在对象中的函数约定俗成也可以叫做方法.

直接调用函数的话, 默认是不用显性的return一个值的.它会默认给你返回一个基础类型undefined.

This指向windows全局

1.2 call和apply

function Test1() {
    this.name = '蚂蚱精'
}

function Test2() {
    Test1.apply(this)
}

const test = new Test2()
console.log(test) // 蚂蚱精

直接作用于this的指向call和apply的差别,在于,它后面跟着的是数组形式.

function Test1(name, color) {
    this.name = name
    this.color = color
}

function Test2() {
    Test1.apply(this, '蚂蚱', 'red')
    Test1.call(this, ['蚂蚱', 'red'])
}

有传闻,call调用方式效率比apply高, 下面是别人实践的结果,

call和apply的性能对比 · Issue #6 · noneven/__ (github.com)

1.3 构造调用方式

这个方式就是我们形成原型链的调用方式,构造函数的过程: 内部函数顶部创建this,然后返回它.

由上面四种方式可以知道,它们主要的不同在于 This**指向.排除这一点的话, ** 函数的调用应该只有一种方式.

image.png

二、原型

要学习原型和原型链的话,还需要接触到这几个名词:

prototype、__proto__、constructor

  • prototype: 构造函数实例化 返回的属性,** 属于构造函数实例化,而不是构造函数的**

  • protp: 构造函数实例化过程中,return 的this中的一个属性,这个属性的值是 .prototype, 它就相当于一个指针名,可以叫做a、b、bb;在浏览器的控制器中打开现实是[[prototype]].

  • constructor: 指向构造函数本身,重点是该属性可以更改

image_1.png

function Person() {
  this.name = '羊羊'
}
Person.prototype = {
  age: 26
}
const girl = new Person()

console.log(girl.age) // 26

其中prototype 就叫做原型. 它是一个对象.所以引用类型有的特性它都有.

成功打印出age 这个属性的值. 实现的过程和之前说过的构造函数不能说一模一样,只能说是相差无几. .prototype 也是通过了某种神秘的方法, 在构造函数实例化中挂在在了this上面.

这种神秘的方法, 前端可以不用了解

function Person() {
  var this = {
    name: '羊羊',
    __proto__: Person.prototype
  }
  return this
}

const girl = new Person()

Person构造函数中, this是在实例化过程中产生的.

如果在.age在this.第一层找不到的时候,会通过某种不需要前端知道的方法,进而去到__proto__上面找.

不知道从什么时候开始很多教材和视频说原型的例子,控制台打印的都是__proto__,但是我现在打印的都是[[prototype]].

有一个很重要的一点, 就是原型是挂载在实例上面的,而不是构造函数上面的

fucntino Car() {}
Car.prototype = {
  color: 'white'
}

cons a = new car()
console.log(a.color) // 'white'

car.prototype.color = 'red;
console.log(a.color) // 'red'

直接改变原型上面的值,是会引起实例属性的变化的, 毕竟你只是修改值.

但是下面这种情况就会出现问题了

functino Car() {}
Car.prototype = {
  color: 'white'
}
const a = new Car()
Car.prototype = {
  color: 'red'
}
console.log(a.color) // white

原型赋值的混淆点

上面的例子直接的指出了对象a上面的[[prototype]]的值是原型. 重要的是这个过程不是一个复制 的过程.

而是一个关联的过程.在创建过程中,或者说是new的过程中,a和内部的__proto__都会关联到Car.prototype上面.

这一知识点出自《你不知道的Javascript》上卷5.2.3

正是因为如此,下面的代码才会成立:

function Car() {
  this.color = 'red'
}

Car.prototype = {
  age: 18,
  constructor: Motorcycle
}

let car = new Car()
car.__proto__.age = 30
console.log(car) // [[prototype]]中的age 为30

原型上面的age 被改变了. 进而也导致了同样实例化Car 的变量也跟着改变.

let car2 = new Car()
console.log(car2.age) // 30

这不是儿子打老子,小儿子跟着受罪么?

image_2.png

有人说,原型的存在就是为了继承.所以我们必须要屏蔽掉原型的共享属性 的副作用.

三、原型链

function Car() {
  this.name = 'one'
}
Car.prototype = {
  color = 'red'
}
const car = new Car()

image_3.png

左边一路下来的就是原型链了.

console.log(car.color) // 'red'

在对象上面找不到的属性,会找到上一个原型上面继续找,知道找到位置,不然就是undefined.

从上面的图片也可以知道,car上面有Object.prototype,就是说,Object.prototype上面有啥属性,car都可以使用.

截屏2021-12-25 22.31.59.png

__proto 是 [[Prototype]] 的具体表现属性. 所以图中的[[Prototype]] 就是代表__proto

情况似乎有点不对.上面这么没有Function 构造函数呀...

这是因为实例化生成的是一个对象,所以原型链上面自然没有Functino才对,直接指向Object.正确的原型链图应该如下:

image.png

所以下面这题就能够理解了.

var F = function() {};

Object.prototype.a = function() {
  console.log('a');
};

Function.prototype.b = function() {
  console.log('b');
}

var f = new F();

f.a(); // 'a'
f.b(); // 报错, b 为定义

F.a(); // 'a'
F.b(); // 'b'