重学JS|原型/原型链/继承

发布于:2022-11-27 ⋅ 阅读:(318) ⋅ 点赞:(0)

说起原型,前端的小伙伴们不会陌生,说这个感觉都烂大街了,我为啥说这个?(还不是我不会,呜呜呜~~~,留下了技术的泪水),下面是我对原型及原型链的一些理解,如有不对的地方请大佬指出,谢谢~

JavaScript 中的对象有一个特殊的 [[Prototype]]内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时 [[Prototype]] 属性都会被赋予一个非空的值【摘自你不知道的JS-5.1】。这个内置属性即为对象的原型。这个属性也是一个对象值。

起源: 因为JS中没有类的概念(class只是个语法糖),但是,Javascript里面都是对象,必须有一种机制,将所有对象联系起来。所以,Brendan Eich(十天设计出JS的那个男人)最后还是设计了"继承"【JS继承机制阮一峰】。然后JS也就成了完整的面向对象语言了,参考 javac++ ,使用 new 命令生成对象与实例并且会调用类的构造函数,在JS中他设计的是 new 命令后面跟的不是类,而是构造函数。由构造函数来生成实例对象来共享原型对象的方法与属性。

开始


来点你平常常见的代码:

var person = new Object({
	age: 12;
})
person.age // 12
person intanceof Object // true

Object即为构造函数,person是一个实例对象, person使用.age时。调用的自身的的age属性

小提示:a instanceof b,检测a是否是b的实例

person能调用tostring,但是该方法却不是自己的方法。

person.hasOwnProperty("age") // true
person.tostring() // "[object Object]"
person.hasOwnProperty("tostring") // false

小提示:Object.prototype.hasOwnproperty(),检测的是自身的属性。 in 关键字会检测原型链上的。

对象上的方法(tostring)为啥实例能够引用? 其实是 new 的功劳,大概是这个样子。(关于new, 文章后面有一个简易实现)

person.__proto__ = Object.prototype;
//这里的__proto__指的就是前面提到的[[prototype]],
// __proto__更像是是一个指针,一种关联

person.tostring时,自身并没有找到,于是在__proto__上找,那么Object.prototype上有没有呢?
很明显是有的,大家可以通过控制台去验证.如果没有,会在Object.prototype.__ proto__上去找,
当然 Object.prototype的__proto __是null
在这里插入图片描述
我们把__proto__当做一个属性来使用(不是所有浏览器都支持,代码环境也不要用),其实更像一个getter / setter

Object.defineProperty( Object.prototype, "__proto__", {
  get: function() {
   return Object.getPrototypeOf( this );
  },
  set: function(o) {
    // ES6 中的 setPrototypeOf(..)
    Object.setPrototypeOf( this, o );
    return o;
  }
} );

除了tostring,大家看到还有其他的方法,平常我们重写原型上的方法就是这个意思。
原型链说的直白点就是利用__proto__产生的关联关系。
再看看下面代码

var p1 = { age: 12 }
var p2 = { sex: '男' }
p1.__proto__ = p2;
p1.age // 12
p1.sex //"男"

p1自身找不到sex,会在.__proto__上找,这时找到了p2,发现有就打印出来了

小提示,在原型链上查找时,当找到时即不再继续查找,即使它的上游有同名属性,此谓原型屏蔽


前面提到的构造函数我是以Object来讲解的,当然String也是一样。下面为了更方便大家理解,用函数来举例

function person(sex = "male"){
  this.age = 12;
  this.sex = sex
}
var p1 = new person();
var p2 = new person("female");
p1.sex //"male";
p2.sex //"female";

上面产生的原型链:

p1.__proto__ = person.prototype
p2.__proto__ = person.prototype

大家可以通过 getPrototypeOf 或者 isPrototypeOf 验证

Object.getPrototypeOf(p1) === person.prototype //true
person.prototype.isPrototypeOf(p2) //true

为什么上面person.protype能用对象的 isPrototypeOf 方法,因为还有另外的原型

p1.__proto__.__proto__ === person.prototype.__proto__ === Object.prototype
p2.__proto__.__proto__ === person.prototype.__proto__ === Object.prototype

在Object.prototype这个对象上存在一些常用的对象方法,如isPrototypeOf, toString,toLocalString等,如下
在这里插入图片描述

Object.prototype再往上查找就是null,前面提到过

p1.__proto__.__proto__.__proto__ === null;

Object.create

更多详情可以去【MDN】上查看更多细节)
现有如下代码:

var O =  { porp1: "xxxx" }
var o1 = Object.create(O);

他的作用是返回一个新的对象 o1,且存在

o1.__proto__ = O

这样 o1 不就能调用 O 上的属性了吗。
一个简单的polyfill

// proto也就是相当于上面的O
Object.create = function (proto, propertiesObject) {
   function F() {}
   F.prototype = proto;
   return new F();
};

关于Object.create() :
------详解Object.create(null)
------Object.create()、new Object()和{}的区别
------深入理解Object.create


为了更深入的理解,接下来我们看看面向对象和对象关联的两个小demo【你不知道的js-6.1.3】

demo1: 面向对象风格(“原型”)

function Foo(who) {
	this.me = who;
}
Foo.prototype.identify = function() {
	return "I am " + this.me;
};
function Bar(who) {
	Foo.call( this, who );
}
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.speak = function() {
	console.log( "Hello, " + this.identify() + "." );
};
var b1 = new Bar( "b1" );
var b2 = new Bar( "b2" );
b1.speak();
b2.speak();

有了Object.create的了解,我们来分析 b1.speak() 发生了什么事,

  1. 调用speak,也就是b1.__ proto __ .speak().原型链上向上查找一层
  2. 运行speak()里面的this.identify(),此时this = Bar.prototype,因为Bar.prototype__proto__ = Foo.prorotype,找到identify方法。
  3. 最后查找this.me,me是b1的自身属性,,直接使用。

上面第三步可能有点难以理解,参考下面示例

function b(){ this.b = 2; console.log(this) }
var a1 = { a1: 11 }
b.call(a1)
//{a1: 11, b: 2}
a1 //{a1: 11, b: 2}

使用call之后,原对象(a1)会与b内部 this对象合并(属性合并),原函数的this也会变为传入的对象。再来看看 new,下面是一个简易版

function New1(){
  //获取构造函数
  const con = [].shift.call(arguments);
  // 创建一个对象且.__proto__指向构造函数.prototype
  //为了能使用到con.prototype的属性
  const obj = Object.create(con.prototype);
  con.apply(obj, arguments);
  // 确保最后返回的是个对象
  return con instanceof Object ? con : obj;
}
// 这样使用
New1(Bar, "b1");

注意到了吗,新产生一个对象 obj,且这个对象的原型是Bar.prototype,由于前面对 object.create 的了解,有

obj.__proto__ = Bar.prototype

那么最后不难理解b1是含有一个me属性等于"b1"的对象了。
如果想了解更多,建议参考 阮一峰老师的 JS面向对象编程—封装

再介绍另外一个demo
demo2: 对象关联风格

Foo = {
	init: function(who) {
	  this.me = who;
	},
	identify: function() {
	  return "I am " + this.me;
	}
};
Bar = Object.create( Foo );
Bar.speak = function() {
  alert( "Hello, " + this.identify() + "." );
};
var b1 = Object.create( Bar );
b1.init( "b1" );
var b2 = Object.create( Bar );
b2.init( "b2" );
b1.speak();
b2.speak();

上面两个demo都是一样的功能,建议先自行分析一下,两种实现方式的差别。
b1 能使用 initspeak 方法是因为原型链
在这里插入图片描述
至于更多的详情,大家可以利用控制台用上图中打印去展开查找。
关于以上两个demo的关系图:
在这里插入图片描述
如果把上面的关系图理清楚了,原型,原型链的理论方面就没问题了。

小提示:利用控制台,以及 isPropertyOfObject.getPropertyOf 去验证,去理解

补充一些寄生组合继承(业界公认的继承的方法,babel 转class 也是 这种方式)

function Parent(value) {
    this.value = value;
}

Parent.prototype.getValue = function() {
    console.log(this.value);
}

function Child(value) {
    Parent.call(this, value)
}

Child.prototype = Object.create(Parent.prototype, {
    constructor: {
        value: Child,
        enumerable: false, // 不可枚举该属性
        writable: true, // 可改写该属性
        configurable: true // 可用 delete 删除该属性
    }
})

const child = new Child(1)
child.getValue();
child instanceof Parent;

总结
constructor 是指向实例对象的构造函数,只存在原型对象中;
prototype 是函数(Function,Object是内置的构造函数)独有的原型对象,存放共享属性和方法,函数申明时创建。
__ proto__ 指向自己的原型对象,从第一个到到最后的null的指向链称为原型链。

参考

详解原型链中的prototype和 proto

帮你彻底搞懂JS中的prototype、__proto__与constructor(图解)

轻松理解JS中的面向对象,顺便搞懂prototype和__proto__

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到