JavaScript原型链笔记

目录

基于原型链的继承

继承属性

继承“方法”

构造函数

字面量的隐式构造函数

构建更长的继承链

深入理解原型

使用不同方法来创建对象和改变原型链

在对象初始化器中使用__proto__键来创建

使用构造函数

使用Object.create()

使用类

使用Object.setPrototypeOf()

使用__proto__访问器

性能

结论


javascript中只有基本类型和对象,包括function、array、enum其实都是对象

js对象则是动态的属性“包”,即对象中的内容都是以属性形式存在的

理解原型链行为的关键在于时刻把握动态性和沿原型链查找这一行为

基于原型链的继承

js对象有一个指向原型对象的链,当试图访问一个对象的属性时,会从当前对象开始,向上搜索整个原型链,直到原型链的末尾(null)或者找到第一个名字匹配的属性(属性遮蔽)

继承属性

const o = {
  a: 1,
  b: 2,
  // __proto__ 设置了 [[Prototype]]。它在这里被指定为另一个对象字面量。
  __proto__: {
    b: 3,
    c: 4,
    __proto__: {
      d: 5,
    },
  },
};

// { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> { d: 5 } ---> Object.prototype ---> null

console.log(o.d); // 5

继承“方法”

严格来说js并没有“方法”这一说法(基于类的语言中的概念),这里的“方法”指的就是添加到对象上作为属性的任何函数,函数作为属性和上面所说的继承属性完全一致,没有区别

当继承的函数被调用时,this指向的是当前继承的对象,而不是拥有该函数属性的原型对象

const parent = {
  value: 2,
  method() {
    return this.value + 1;
  },
};

console.log(parent.method()); // 3
// 当调用 parent.method 时,“this”指向了 parent

// child 是一个继承了 parent 的对象
const child = {
  __proto__: parent,
};
console.log(child.method()); // 3
// 调用 child.method 时,“this”指向了 child。
// 又因为 child 继承的是 parent 的方法,
// 首先在 child 上寻找“value”属性。但由于 child 本身
// 没有名为“value”的自有属性,该属性会在
// [[Prototype]] 上被找到,即 parent.value。

child.value = 4; // 在 child,将“value”属性赋值为 4。
// 这会遮蔽 parent 上的“value”属性。
// child 对象现在看起来是这样的:
// { value: 4, __proto__: { value: 2, method: [Function] } }
console.log(child.method()); // 5
// 因为 child 现在拥有“value”属性,“this.value”现在表示
// child.value

构造函数

解释:

  1. new Box(1)是通过Box构造函数创建的一个实例
  2. Box.prototype是一个普通的对象
  3. 通过构造函数创建的每一个实例都会自动将构造函数的prototype属性(一个对象)作为其[[prototype]],即Object.getPrototypeOf(new Box()) === Box.prototype
  4. Constructor.prototype默认具有一个自有属性:constructor,它引用了构造函数本身。即,Box.prototype.constructor === Box。这允许我们在任何实例中访问原始构造函数,即,instance.constructor(沿原型链查找)
  5. 类写法是构造函数写法的语法糖
  6. Constructor.prototype(构造函数的prototype属性,是一个对象)仅在构造实例时有用,它与Constructor.[[prototype]]无关,后者是构造函数的自有原型(构造函数本身也是对象),即Function.prototype。即,Object.getPrototypeOf(Constructor) === Function.prototype
// 一个构造函数
function Box(value) {
  this.value = value;
}

// 使用 Box() 构造函数创建的所有盒子都将具有的属性
Box.prototype.getValue = function () {
  return this.value;
};

const boxes = [new Box(1), new Box(2), new Box(3)];

注意!!!

我们可以通过改变Constructor.prototype来改变所有实例的行为,因为所有实例的[[prototype]]是绑定到Constructor.prototype的,任何改变都会影响任何时间创建的实例的行为,因为是沿着原型链动态查找的。但是重新赋值Constructor.prototype是一个不好的主意!!!原因如下:

  1. 在重新赋值之前创建的实例的[[Prototype]]引用的是与重新赋值之后创建的实例的[[Prototype]]不同的对象。原因在于,如前面所说,通过构造函数创建每一个实例的时候,都会自动将构造函数的prototype属性作为实例的[[prototype]],这里动作在实例构建时完成,后续重新赋值并不会改变。但改变Constructor.prototype不同,调用“方法”的时候是动态搜索原型链的。
  2. 除非手动重新设置constructor属性,否则无法通过instance.constructor追踪到构造函数,这可能会破坏用户期望的行为。一些内置操作也会读取 constructor属性,如果没有设置,它们可能无法按预期工作。原因同上。

字面量的隐式构造函数

// 对象字面量(没有 `__proto__` 键)自动将
// `Object.prototype` 作为它们的 `[[Prototype]]`
const object = { a: 1 };
Object.getPrototypeOf(object) === Object.prototype; // true

// 数组字面量自动将 `Array.prototype` 作为它们的 `[[Prototype]]`
const array = [1, 2, 3];
Object.getPrototypeOf(array) === Array.prototype; // true

// 正则表达式字面量自动将 `RegExp.prototype` 作为它们的 `[[Prototype]]`
const regexp = /abc/;
Object.getPrototypeOf(regexp) === RegExp.prototype; // true

扩展内置原型是一种常见的错误实践,比如定义Array.prototype.myMethod = function() {...},这种错误实践被称为猴子修补,会导致向前兼容的风险。扩展内置原型的唯一理由是向后移植新的js引擎特性,比如Array.prototype.forEach

构建更长的继承链

使用Object.setPrototypeOf()、extends、Object.create()

不推荐使用Object.create(),因为会导致重新赋值Constructor.prototype属性,从而导致前面注意中提到的问题

深入理解原型

constructor.prototype是一个对象,是在通过构造函数创建实例时设置给instance.[[prototype]]的对象;constructor.[[prototype]]也是一个对象,是constructor的原型对象

解释:

  1. prop是doSomeInstancing的自有属性
  2. foo是doSomeInstancing的[[prototype]]的自有属性,通过原型链获得
  3. doSomething是一个构造函数,它本身没有foo和prop属性,追溯其原型链,上游是Object.prototype,再向上是null,也没有foo和prop,因此这里是undefined
  4. doSomething.prototype这一对象并没有prop属性,其原型链中也没有
  5. foo属性是doSomething.prototype这一对象的自有属性
function doSomething() {}
doSomething.prototype.foo = "bar";
const doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value";
console.log("doSomeInstancing.prop:     ", doSomeInstancing.prop);
console.log("doSomeInstancing.foo:      ", doSomeInstancing.foo);
console.log("doSomething.prop:          ", doSomething.prop);
console.log("doSomething.foo:           ", doSomething.foo);
console.log("doSomething.prototype.prop:", doSomething.prototype.prop);
console.log("doSomething.prototype.foo: ", doSomething.prototype.foo);

输出:

doSomeInstancing.prop:      some value
doSomeInstancing.foo:       bar
doSomething.prop:           undefined
doSomething.foo:            undefined
doSomething.prototype.prop: undefined
doSomething.prototype.foo:  bar

使用不同方法来创建对象和改变原型链

各种方法的具体优缺点查mdn docs,推荐使用构造函数,速度快、标准、容易被JIT优化

在对象初始化器中使用__proto__键来创建

const o = { a: 1 };
// 新创建的对象 o 以 Object.prototype 作为它的 [[Prototype]]
// Object.prototype 的原型为 null。
// o ---> Object.prototype ---> null

const b = ["yo", "whadup", "?"];
// 数组继承了 Array.prototype(具有 indexOf、forEach 等方法)
// 其原型链如下所示:
// b ---> Array.prototype ---> Object.prototype ---> null

function f() {
  return 2;
}
// 函数继承了 Function.prototype(具有 call、bind 等方法)
// f ---> Function.prototype ---> Object.prototype ---> null

const p = { b: 2, __proto__: o };
// 可以通过 __proto__ 字面量属性将新创建对象的
// [[Prototype]] 指向另一个对象。
// (不要与 Object.prototype.__proto__ 访问器混淆)
// p ---> o ---> Object.prototype ---> null

使用构造函数

function Graph() {
  this.vertices = [];
  this.edges = [];
}

Graph.prototype.addVertex = function (v) {
  this.vertices.push(v);
};

const g = new Graph();
// g 是一个带有自有属性“vertices”和“edges”的对象。
// 在执行 new Graph() 时,g.[[Prototype]] 是 Graph.prototype 的值。

使用Object.create()

const a = { a: 1 };
// a ---> Object.prototype ---> null

const b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (inherited)

const c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

const d = Object.create(null);
// d ---> null(d 是一个直接以 null 为原型的对象)
console.log(d.hasOwnProperty);
// undefined,因为 d 没有继承 Object.prototype

使用类

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

class Square extends Polygon {
  constructor(sideLength) {
    super(sideLength, sideLength);
  }

  get area() {
    return this.height * this.width;
  }

  set sideLength(newLength) {
    this.height = newLength;
    this.width = newLength;
  }
}

const square = new Square(2);
// square ---> Square.prototype ---> Polygon.prototype ---> Object.prototype ---> null

使用Object.setPrototypeOf()

动态修改[[prototype]]都会导致性能问题,因为许多引擎会优化原型,动态设置原型会破坏优化。

const obj = { a: 1 };
const anotherObj = { b: 2 };
Object.setPrototypeOf(obj, anotherObj);
// obj ---> anotherObj ---> Object.prototype ---> null

使用__proto__访问器

const obj = {};
// 请不要使用该方法:仅作为示例。
obj.__proto__ = { barProp: "bar val" };
obj.__proto__.__proto__ = { fooProp: "foo val" };
console.log(obj.fooProp);
console.log(obj.barProp);

性能

原型链上较深层的属性的查找时间可能会对性能产生负面影响

在遍历对象的属性时,原型链中的每个可枚举的属性都将被枚举。要检查对象是否具有在其自身上定义的属性(而不是在原型链上的某个地方),则有必要使用hasOwnProperty或Object.hasOwn方法(自有属性)

结论

js中除了基本类型,一切都是对象(实例),函数本身也是Function构造函数的实例,而Function构造函数是以Object.prototype对象为原型的对象

js中的所有构造函数都有一个被称为prototype的特殊属性,它与new运算符一起使用。对原型对象的引用被复制到新实例的内部属性[[Prototype]]中。例如,当执行const a1 = new A()时,js(在内存中创建对象之后,为其定义this并执行A()之前)设置a1.[[prototype]] = A.prototype

要注意代码中原型链的长度,在必要时可以将其分解,以避免潜在的性能问题。此外,除非是为了与新的 JavaScript 特性兼容,否则永远不应扩展原生原型

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值