深入学习 js 中原型,原型链,new与6种继承方式

本文带大家了解了原型及原型链等相关知识,包括构造函数,继承方式,new的原理,手写new,Object.create方法......

原型

原型是 JavaScript 中的一个重要概念,它是构成 JavaScript 对象模型的基础之一,用于实现对象之间的继承和属性共享

在 JavaScript 中,每个对象都有一个原型(prototype)。原型是一个对象,它包含可以被其他对象继承的属性和方法。当访问对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或方法的定义或者到达原型链的顶端。

原型链是由一系列对象连接而成的链条,每个对象都有一个指向它的原型的引用。如果一个对象在自身的属性和方法中没有找到所需的内容,它会继续在原型对象上查找,而原型对象本身也可以拥有自己的原型,这样就形成了原型链。

原型链的使用有助于对象之间的属性和方法的共享,减少了重复定义和内存消耗。通过原型继承,我们可以创建新的对象,并继承已有对象的属性和方法,从而实现代码的重用和扩展。

因此,原型在 JavaScript 中是一个非常有用和强大的概念,并且是面向对象编程的基石之一。它为 JavaScript 提供了一种灵活而强大的继承机制,使得对象可以共享属性和方法,并且可以根据需要进行扩展和修改。原型只是一种编程概念,用于实现对象之间的关系和继承。

js 复制代码
function Person(name) {
    this.name=name
}
console.log(Person.prototype)
1.  {constructor: ƒ}
1.  1.  constructor: ƒ Person(name)
    1.  [[Prototype]]: Object

可以看到,原型对象有一个自有属性constructor,这个属性指向该函数 Person.prototype.constructor==person1.constructor,同时person1.__proto__.constructor==person1.constructor(person1是Person的实例),我们平时通过 实例.constructor拿到的构造函数都是通过__proto__向上拿到了的constructor。

constructor 属性的主要作用是:

  1. 标识对象的构造函数:通过访问对象的 constructor 属性,可以知道该对象是由哪个构造函数创建的。
  2. 方便对象的类型判断:通过比较对象的 constructor 属性与特定构造函数的引用,可以判断对象的类型是否与该构造函数匹配。

在后文中,会有通过Object.create()方法去创建原型对象并赋值给子类原型对象的操作,此时需要手动挂载consturctor属性 ,或者通过Object.create()方法的第二个参数实现:

javascript 复制代码
// 子类继承父类
Rectangle.prototype = Object.create(Shape.prototype, {
  // 如果不将 Rectangle.prototype.constructor 设置为 Rectangle,
  // 它将采用 Shape(父类)的 prototype.constructor。
  // 为避免这种情况,我们将 prototype.constructor 设置为 Rectangle(子类)。
  constructor: {
    value: Rectangle,
    enumerable: false,
    writable: true,
    configurable: true,
  },
});
javascript 复制代码
Person.prototype.sayHello = function() {
  console.log("Hello, " + this.name);
};

const person = new Person("John");

console.log(person.constructor);  // 输出: [Function: Person]
console.log(person.constructor === Person);  // 输出: true

请注意,constructor 属性通常来自构造函数的 prototype 属性。person.constructor返回的是Person函数的引用,而不是简单的函数名字符串。

原型链

JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

备注: 遵循 ECMAScript 标准,符号 someObject.[[Prototype]] 用于标识 someObject 的原型。内部插槽 [[Prototype]] 可以通过 Object.getPrototypeOf()Object.setPrototypeOf() 函数来访问。这个等同于 JavaScript 的非标准但被许多 JavaScript 引擎实现的属性 __proto__ 访问器。为在保持简洁的同时避免混淆,在我们的符号中会避免使用 obj.__proto__,而是使用 obj.[[Prototype]] 作为代替。其对应于 Object.getPrototypeOf(obj)。 可以看MDN官方给出的示例:

继承属性:

js 复制代码
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

可以通过手动设置__proto__来改变他向上查找的层级,直到找到或者为null。 通过这个示例可以很好了解原型链向上查找的规则。

继承方法:

js 复制代码
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

{ value: 4, __proto__: { value: 2, method: [Function] } },通过这种方式可以很好判断this.value到底访问的是哪个value!!当使用方法去获取value时,查看到当前对象中就存在value,就不会向原型链中去寻找value。

构造函数与New

javascript 复制代码
// 一个构造函数
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)];

构造函数需要使用new去调用,在"new"的过程中,主要实现四件事:

  1. 创建一个空的简单 JavaScript 对象(即 {} );
  2. 为步骤 1 新创建的对象添加属性 __proto__ ,将该属性链接至构造函数的原型对象
  3. 将步骤 1 新创建的对象作为 this 的上下文;
  4. 如果该函数没有返回对象 ,则返回 this。(正常情况下构造函数是不会有返回值的,如果返回非对象,则返回值不生效;如果返回对象a,则new 构造函数的最终结果就是返回的对象a)

接下来我们来手写new,看看到底如何实现,从代码层面帮助大家理解:

javascript 复制代码
function Person(name, age){
    this.name = name;
    this.age = age;
}
Person.prototype.sayName = function () {
    console.log(this.name)
}
const person1 = new Person('Tom', 20)
console.log(person1)  // Person {name: "Tom", age: 20}
t.sayName() // 'Tom'
  • new 通过构造函数 Person 创建出来的实例可以访问到构造函数中的属性
  • new 通过构造函数 Person 创建出来的实例可以访问到构造函数原型链中的属性(即实例与构造函数通过原型链连接了起来)

现在在构建函数中显式加上返回值,并且这个返回值是一个原始类型

javascript 复制代码
function Test(name) {
  this.name = name
  return 1
}
const t = new Test('xxx')
console.log(t.name) // 'xxx'

可以发现,构造函数中返回一个原始值,然而这个返回值并没有作用

下面在构造函数中返回一个对象

javascript 复制代码
function Test(name) {
  this.name = name
  console.log(this) // Test { name: 'xxx' }
  return { age: 26 }
}
const t = new Test('xxx')
console.log(t) // { age: 26 }
console.log(t.name) // 'undefined'

从上面可以发现,构造函数如果返回值为一个对象,那么这个返回值会被正常使用

这样当我们使用new去创建实例的时候,实例都能够获取构造函数的原型对象,也就能拿到该示例中的getValue()函数.

手写new:

javascript 复制代码
function mynew(Func, ...args) {
    // 1.创建一个新对象
    const obj = {}
    // 2.新对象原型指向构造函数原型对象
    obj.__proto__ = Func.prototype
    // 3.将构建函数的this指向新对象
    let result = Func.apply(obj, args)
    // 4.根据返回值判断
    return result instanceof Object ? result : obj
}

备注: 如果你没有使用 new 运算符,构造函数会像其他的常规函数一样被调用,并不会创建一个对象。在这种情况下, this 的指向也是不一样的。

忘了在哪里搜集的图片了,非常非常全面:

js 复制代码
-   构造函数生成实例对象`person`,`person`的`__proto__`指向构造函数`Person`原型对象
-   `Person.prototype.__proto__` 指向内置对象,因为 `Person.prototype` 是个对象,默认是由 ` Object  `函数作为类创建的,而 `Object.prototype` 为内置对象
-   `Person.__proto__` 指向内置匿名函数 `anonymous`,因为 Person 是个函数对象,默认由 Function 作为类创建
-   `Function.prototype` 和 ` Function.__proto__  `同时指向内置匿名函数 `anonymous`,这样原型链的终点就是 `null`

Function.__proto__
ƒ () { [native code] }  //指向图中的function anonymous
Function.__proto__.__proto__ //指向了内置对象

{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, ...}constructor: ƒ Object()hasOwnProperty: ƒ hasOwnProperty()isPrototypeOf: ƒ isPrototypeOf()propertyIsEnumerable: ƒ propertyIsEnumerable()toLocaleString: ƒ toLocaleString()toString: ƒ toString()valueOf: ƒ valueOf()__defineGetter__: ƒ __defineGetter__()__defineSetter__: ƒ __defineSetter__()__lookupGetter__: ƒ __lookupGetter__()__lookupSetter__: ƒ __lookupSetter__()__proto__: (...)get __proto__: ƒ __proto__()set __proto__: ƒ __proto__()

Function.__proto__.__proto__.__proto__  
null   //指向null
Function.prototype  //prototype同理
ƒ () { [native code] }
Function.prototype.__proto__
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, ...}
Function.prototype.__proto__.__proto__
null

每个对象的__proto__都是指向它的构造函数的原型对象prototype

ini 复制代码
person1.__proto__ === Person.prototype

构造函数是一个函数对象 ,是通过 Function 构造器产生的

ini 复制代码
Person.__proto__ === Function.prototype

原型对象本身是一个普通对象 ,而普通对象的构造函数都是Object

javascript 复制代码
Person.prototype.__proto__ === Object.prototype

刚刚上面说了,所有的构造器都是函数对象,函数对象都是 Function 构造产生的

javascript 复制代码
Object.__proto__ === Function.prototype

Object 的原型对象也有__proto__属性指向nullnull是原型链的顶端

javascript 复制代码
Object.prototype.__proto__ === null

下面作出总结:

  • 一切对象都是继承自Object对象,Object 对象直接继承根源对象 null
  • 一切的函数对象(包括 Object 对象),都是继承自 Function 对象
  • Object 对象直接继承自 Function 对象
  • Function对象的__proto__会指向自己的原型对象,最终还是继承自Object对象

实现继承

下面给出JavaScripy常见的继承方式:

  • 原型链继承
  • 构造函数继承(借助 call)
  • 组合继承
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承

原型链继承:

js 复制代码
function Parent() {
    this.name = 'parent1';
    this.play = [1, 2, 3]
  }
  function Child() {
    this.type = 'child2';
  }
Child.prototype=new Parent()     //可以看看new时发生了什么

let s1=new Child()
let s2=new Child()
s1.play.push("add")      
s1.play
(4) [1, 2, 3, 'add']
s2.play
(4) [1, 2, 3, 'add']

我们会发现这种写法存在很大的问题,当修改s1中的属性时 s2也会同步修改,这是因为paly是定义在原型上的,创建的s1和s2实例是公用当前的原型的,也就是说通过访问__proto__去修改原型上的数据,其他实例在访问原型上的属性时都会访问到被修改的值:

js 复制代码
s1.__proto__.name
'parent1'
s1.__proto__.name='s1'
's1'
当s2去访问时:
s2.name
's1'

当我们使用实例去设置值时,例如:

js 复制代码
s1.name='s1'
// **会在当前对象添加name 如果需要修改原型中name 需要使用__proto__去修改**
1.  Child {type: 'child2', name: 's1'}
1.  1.  name: "s1"
    1.  type: "child2"
    1.  [[Prototype]]: Parent
    1.  1.  name: "parent1"
        1.  play: (4) [1, 2, 3, 'add']
        1.  [[Prototype]]: Object

构造函数继承

借助call去调用parent函数:

js 复制代码
function Parent2(){
    this.name="parent2"
    this.getName=function () {
        return this.name
    }
}
Parent1.prototype.say = function () {}
function Child2() {
    Parent2.call(this)
    this.type="child2"
}
console.log(new Child2())

构造函数借助call实现继承,Parent2.call(this)中,this指向Child2,这种方式的缺点是只能继承父类实例中的属性和方法,不能继承父类原型中的属性和方法。

组合继承

将以上两种方式进行组合实现:

js 复制代码
function Parent3 () {
    this.name = 'parent3';
    this.play = [1, 2, 3];
}

Parent3.prototype.getName = function () {
    return this.name;
}
function Child3() {
    // 第二次调用 Parent3()
    Parent3.call(this);
    this.type = 'child3';
}

// 第一次调用 Parent3()
Child3.prototype = new Parent3();
// 手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play);  // 不互相影响
console.log(s3.getName()); // 正常输出'parent3'
console.log(s4.getName()); // 正常输出'parent3'

第一次调用Parent3:将child3的原型对象链接到parent3的原型上 这样可以访问parent3原型上的方法 第二次调用Parent3:实在构造Child3实例时,继承到Parent3实例上的属性和方法 缺点就是Parent3调用了两次,会造成性能浪费

原型继承

通过Object.create()实现普通对象的继承

Object.create() 静态方法以一个现有对象作为原型,创建一个新对象。

lua 复制代码
let parent4 = {
    name: "parent4",
    friends: ["p1", "p2", "p3"],
    getName: function() {
      return this.name;
    }
  };

  let person4 = Object.create(parent4);
  person4.name = "tom";
  person4.friends.push("jerry");

  let person5 = Object.create(parent4);
  person5.friends.push("lucy");

  console.log(person4.name); // tom
  console.log(person4.name === person4.getName()); // true
  console.log(person5.name); // parent4
  console.log(person4.friends); // ["p1", "p2", "p3","jerry","lucy"]
  console.log(person5.friends); // ["p1", "p2", "p3","jerry","lucy"]

person4.friends.push("jerry");person5.friends.push("lucy");都会顺着原型链进行查找,进而找到parent4:

js 复制代码
person5.__proto__===parent4
true

parent4和parent5都会向上查找到friends,对其修改后会影响到所有的子类,所以再访问时会拿到修改后的值。

寄生式继承

js 复制代码
let parent5 = {
    name: "parent5",
    friends: ["p1", "p2", "p3"],
    getName: function() {
        return this.name;
    }
};

function clone(original) {
    let clone = Object.create(original);
    clone.getFriends = function() {
        return this.friends;
    };
    return clone;
}

let person5 = clone(parent5);

console.log(person5.getName()); // parent5
console.log(person5.getFriends()); // ["p1", "p2", "p3"]

可以添加一些方法,但是还是使用的同一个对象作为原型,创建的实例可以沿着原型链去修改原型中的变量,从而影响所有的后面的实例去访问原型中的变量。

寄生组合式继承

js 复制代码
function clone (parent, child) {
    // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
    child.prototype = Object.create(parent.prototype);
    child.prototype.constructor = child;
}

function Parent6() {
    this.name = 'parent6';
    this.play = [1, 2, 3];
}
Parent6.prototype.getName = function () {
    return this.name;
}
function Child6() {
    Parent6.call(this);
    this.friends = 'child5';
}

clone(Parent6, Child6);

Child6.prototype.getFriends = function () {
    return this.friends;
}

let person6 = new Child6(); 
console.log(person6); //{friends:"child5",name:"child5",play:[1,2,3],__proto__:Parent6}
console.log(person6.getName()); // parent6
console.log(person6.getFriends()); // child5

寄生组合式继承使用call方式继承父类的实例对象(通过this,)的属性和方法,Parent6.call(this);this指向Child6,并在Parent6原型对象上 绑定方法,再通过Object.create()方法创建原型对象并赋值给Child6(通过create方法创建完原型并赋值后需要指定constructor构造函数为Child6,否则构造函数为继承的parent6),

  1. 通过call方法去继承父类this上的属性和方法
  2. 通过create方法继承父类原型上的属性和方法
  3. 创建的child实例对象是相互独立的
js 复制代码
person6.play.push(11)
console.log(person6); 
console.log(person7);
相关推荐
风清扬_jd37 分钟前
Chromium 硬件加速开关c++
java·前端·c++
谢尔登1 小时前
【React】事件机制
前端·javascript·react.js
2401_857622662 小时前
SpringBoot精华:打造高效美容院管理系统
java·前端·spring boot
etsuyou2 小时前
Koa学习
服务器·前端·学习
Easonmax2 小时前
【CSS3】css开篇基础(1)
前端·css
粥里有勺糖3 小时前
视野修炼-技术周刊第104期 | 下一代 JavaScript 工具链
前端·javascript·github
大鱼前端3 小时前
未来前端发展方向:深度探索与技术前瞻
前端
昨天;明天。今天。3 小时前
案例-博客页面简单实现
前端·javascript·css
天上掉下来个程小白3 小时前
请求响应-08.响应-案例
java·服务器·前端·springboot
前端络绎3 小时前
初识 DT-SDK:基于 Cesium 的二三维一体 WebGis 框架
前端