夜猫的小站

js中的原型链

Published on
阅读时间:13分钟2464

本文最近一次更新于 492 个天前,其中的内容很可能已经有所发展或是发生改变。

前言

最近被裁员了,不得已重新复习下基础知识。本章巩固下 javascript 中原型链相关的知识,基于高级程序设计以及 mdn 上的文档整理相关知识。

整体图

image.png

根据 mdn 上文档,思维导图上的 proto 指向,实际等同于 Object.getPrototypeOf(xxxxxx)preson1.__proto__ 为非标准写法,在浏览器控制台上实际上能看到的 [[Prototype]] 才是真正的原型,并通过 Object.getPrototypeOf 函数访问。

遵循 ECMAScript 标准,符号 someObject.[[Prototype]] 用于标识 someObject 的原型。内部插槽 [[Prototype]] 可以通过 Object.getPrototypeOf() 和 Object.setPrototypeOf() 函数来访问。这个等同于 JavaScript 的非标准但被许多 JavaScript 引擎实现的属性 __proto__ 访问器。为在保持简洁的同时避免混淆,在我们的符号中会避免使用 obj.__proto__,而是使用 obj.[[Prototype]] 作为代替。其对应于 Object.getPrototypeOf(obj)。它不应与函数的 func.prototype 属性混淆,后者指定在给定函数被用作构造函数时分配给所有对象_实例_的 [[Prototype]]。我们将在后面的小节中讨论构造函数的原型属性。

同时 mdn 上也提醒我们{ __proto__: ... } 语法与 obj.__proto__ 访问器不同:前者是标准且未被弃用的。

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

// o.[[Prototype]] 具有属性 b 和 c。
// o.[[Prototype]].[[Prototype]] 是 Object.prototype(我们会在下文解释其含义)。
// 最后,o.[[Prototype]].[[Prototype]].[[Prototype]] 是 null。
// 这是原型链的末尾,值为 null,
// 根据定义,其没有 [[Prototype]]。
// 因此,完整的原型链看起来像这样:
// { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> Object.prototype ---> null

console.log(o.a); // 1
// o 上有自有属性“a”吗?有,且其值为 1。

console.log(o.b); // 2
// o 上有自有属性“b”吗?有,且其值为 2。
// 原型也有“b”属性,但其没有被访问。
// 这被称为属性遮蔽(Property Shadowing)

console.log(o.c); // 4
// o 上有自有属性“c”吗?没有,检查其原型。
// o.[[Prototype]] 上有自有属性“c”吗?有,其值为 4。

console.log(o.d); // undefined
// o 上有自有属性“d”吗?没有,检查其原型。
// o.[[Prototype]] 上有自有属性“d”吗?没有,检查其原型。
// o.[[Prototype]].[[Prototype]] 是 Object.prototype 且
// 其默认没有“d”属性,检查其原型。
// o.[[Prototype]].[[Prototype]].[[Prototype]] 为 null,停止搜索,
// 未找到该属性,返回 undefined。

创建对象

1 .创建对象最简单的方式就是使用对象直接量

let empty = {};                          // An object with no properties
let point = { x: 0, y: 0 };              // Two numeric properties
let p2 = { x: point.x, y: point.y+1 };   // More complex values
let book = {
    "main title": "JavaScript",          // These property names include spaces,
    "sub-title": "The Definitive Guide", // and hyphens, so use string literals.
    for: "all audiences",                // for is reserved, but no quotes.
    author: {                            // The value of this property is
        firstname: "David",              // itself an object.
        surname: "Flanagan"
    }
};

2. Object.create()

Object.create() 可以用来创建对象,可以通过传入参数 null 来创建一个没有原型的新对象,但通过这种方式创建的对象不会继承任何东西,甚至不包括基础方法,比如 toString(),也就是说,它将不能和“+”运算符一起正常工作:

let o2 = Object.create(null); // o2 inherits no props or methods.

如果想创建一个普通的空对象(像通过 或 new Object() 创建的对象),需要传入 Object.prototype:

let o3 = Object.create(Object.prototype); // o3 is like {} or new Object().

根据红宝书上的描述实现 Object.create 可以粗略实现:

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

3. class 关键字

class Person {
	constructor() {}
}
let p = new Person()

4. 工厂模式、构造函数模式、原型模式等设计模式

工厂模式

function createPerson(name, age, job) {
	let o = new Object();
	o.name = name;
	o.age = age;
	o.job = job;
	o.sayName = function() {
	console.log(this.name);
	};
	return o; 
}
let person1 = createPerson("Nicholas", 29, "Software Engineer"); 
let person2 = createPerson("Greg", 27, "Doctor");

缺点

  • 没有解决对象标识问题(即新创建的对象是什么类型,以上例子 person1 和 person2 都认为是 Object 的实例)

构造函数模式


function Person(name, age, job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function() {
    console.log(this.name);
}; }
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); // Nicholas 10 person2.sayName(); // Greg

优点

  • 定义自定义构造函数可以确保实例被标识为特定类型

缺点

  • 其定义的方法会在每个实例上 都创建一遍

原型模式

function Person() {}
    Person.prototype.name = "Nicholas";
    Person.prototype.age = 29;
    Person.prototype.job = "Software Engineer";
    Person.prototype.sayName = function() {
      console.log(this.name);
};
let person1 = new Person();
person1.sayName(); // "Nicholas"
let person2 = new Person();
person2.sayName(); // "Nicholas"
console.log(person1.sayName == person2.sayName); // true

继承

构造函数、原型和实例的关系: 每个构造函数都有一个原型对象,原型有一个属性 (constructor)指回构造函数,而实例有一个内部指针 (proto)指向原型。 image.png

1. 原型链继承

function Parent(name) {
	this.name = name
	this.colors = ['red']
}
function Child() {}
// 继承
Child.prototype = new Parent('john')

let s1 = new Child()
s1.colors.push('black')
console.log(s1.colors) // red, black

let s2 = new Child()
console.log(s1.colors) // red, black

缺点

  • 所有实例共享了colors 属性。因为本质上其实是对 s1.prototype.colors 上进行操作了
  • 子类型初始化不能给父类型的构造函数传参

2 .盗用构造函数

原理:在子类构造函数中调用父类构造函数。

function Parent(name) {
	this.name = name
	this.colors = ['red']
}
function Child() {
	// 继承
	Parent.call(this,'john')
}
let s1 = new Child()
s1.colors.push('black')
console.log(s1.colors) // red, black

let s2 = new Child()
console.log(s1.colors) // red

缺点

  • 必须在构造函数中定义方法,函数不能重用
  • 子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。

3. 组合继承

function Parent(name) {
	this.name = name
	this.colors = ['red']
}
function Child() {
	// 继承
	Parent.call(this,'john')
}

// 继承
Child.prototype = new Parent('john')
let s1 = new Child()
s1.colors.push('black')
console.log(s1.colors) // red, black

let s2 = new Child()
console.log(s1.colors) // red

优点

  • 组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的继承模式。
  • 而且组合继承也保留了 instanceof 操作符和 isPrototypeOf()方法识别合成对象的能力。

缺点

  • 存在效率问题,父类构造函数调用了 2 次

4. 原型式继承

 function object(o) {
      function F() {}
      F.prototype = o;
      return new F();
}
let person = {
	name: "Nicholas",
	friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

let yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"

优点

  • 原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合

缺点

  • 属性中包含的引用值始终会在相关对象间共享

5. 寄生式继承

寄生式继承背后的思路类似于寄生构造函数工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

function createAnother(original){
	let clone = object(original); // 通过调用函数创建一个新对象 这里创建类的方法可以是任意的创建对象方式
	clone.sayHi = function() { // 以某种方式增强这个对象
        console.log("hi");
    };
	return clone; // 返回这个对象 
}

let parent = {
	name: 'john',
	colors: ['red']
}
let child = createAnother(parent)
child.sayHi() // 'hi'

特点:同样适合主要关注对象,而不在乎类型和构造函数的场景

6. 寄生组合式继承

function Parent(name) {
	this.name = name
	this.colors = ['red']
}
Parent.prototype.sayHi = function() {
	console.log("hi", this.name);
}

function inheritPrototype(subType, superType) {
	let prototype = object(superType.prototype); // 创建对象
	prototype.constructor = subType; // 增强对象 
	subType.prototype = prototype; // 赋值对象
}
function Child(name) {
	// 继承
	Parent.call(this, name)
}
inheritPrototype(Parent, Child)


优点

  • 只调用了一次 Parent 构造函数
  • Parent.prototype 避免增加用不到的属性
  • 原型链保持不变,instanceof 以及 isPrototypeOf 正常有效

class Parent{
	// 构造函数
	constructor(name){
		this.name = name
		this.colors = ['red']
	}
	sayHi() {
		console.log("hi", this.name);
	}
	static identify() {
	    console.log('vehicle');
	}
}

class Child extends Parent {
	constructor(name){
		// 不要在调用super()之前引用this,否则会抛出ReferenceError
		super(name)
	}
	// 在静态方法中可以通过 super 调用继承的类上定义的静态方法
	static identify() {
	    super.identify();
	}
}

ES6 给类构造函数和静态方法添加了内部特性[[HomeObject]],这个特性是一个 指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在 JavaScript 引擎内部 访问。super 始终会定义为[[HomeObject]]的原型。

类混入

把不同类的行为集中到一个类是一种常见的 JavaScript 模式。虽然 ES6 没有显式支持多类继承,但 通过现有特性可以轻松地模拟这种行为。

Object.assign()方法是为了混入对象行为而设计的。只有在需要混入类的行为 时才有必要自己实现混入表达式。如果只是需要混入多个对象的属性,那么使用 Object.assign()就可以了。

class Vehicle {}
    let FooMixin = (Superclass) => class extends Superclass {
      foo() {
        console.log('foo');
      }
    };
    let BarMixin = (Superclass) => class extends Superclass {
      bar() {
        console.log('bar');
} };
    let BazMixin = (Superclass) => class extends Superclass {
      baz() {
    console.log('baz');
  }
};
function mix(BaseClass, ...Mixins) {
  return Mixins.reduce((accumulator,
}
current) => current(accumulator), BaseClass);
class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}
let b = new Bus();
b.foo();  // foo
b.bar();  // bar
b.baz();  // baz

很多 JavaScript 框架(特别是 React)已经抛弃混入模式,转向了组合模式(把方法 提取到独立的类和辅助对象中,然后把它们组合起来,但不使用继承)。这反映了那个众 所周知的软件设计原则:“组合胜过继承(composition over inheritance)。”这个设计原则被 很多人遵循,在代码设计中能提供极大的灵活性。

参考资料