likes
comments
collection
share

你可能不太理解的JavaScript - 原型与原型链

作者站长头像
站长
· 阅读数 17

前言

本文适合有前端基础,或者有过前端工作经验的朋友。下面所讲的概念只是简单让大家在脑海中有一个基本的概念,如果哪里有误,或者理解不了,也欢迎朋友们指正或讨论。


一、概念

1. 构造函数

构造函数是用于创建和初始化一个对象的特殊方法,通常我们会使用new关键字来调用构造函数,创建新的对象实例。 疑问:构造函数和普通函数有什么区别? 1,构造函数的名称通常首字母大写,而普通函数首字母通常小写(这只是一种约定,并非强制规则)。 2,在构造函数中,JavaScript会隐式地为新对象创建一个this引用,代表新创建的对象实例,而普通函数则不会。 3,如果构造函数没有返回其他对象,那么调用该构造函数创建的新对象就是该构造函数的实例,而普通函数不具有这样的特性。 4,构造函数除了初始化对象的属性外,还会将对象原型(prototype)的属性和方法赋予新对象。而普通函数则不具有这种特性。

2. Object

在JavaScript中,Object构造函数创建一个对象包装器。如果给定值是null或undefined,将会创建并返回一个空对象,否则,将返回一个与给定值对应类型的对象。当以非构造函数(即,没有使用new)的方式被调用时,Object函数将转换为对象。它可以将任何值转换为对象。这种方式通常被用于将基本数据类型(如数字、字符串和布尔值)转换为相应的对象。

例如下面代码

	let num = 123;
	let obj = Object(num);
	
	console.log(obj); // 输出:Number {123}
	console.log(typeof obj); // 输出:"object"

根据上面构造函数的定义,Object是满足当做一个构造函数的要求的。既然是个函数了,那么就会有prototype属性。 这里先埋一个伏笔,到现在只需要记住,Object是JS运行时就创建好了的,是JS内置的,并且Object身上有个prototype属性即可

3. Function

Function是一个特殊的构造函数,它是在JavaScript运行时就创建的一个对象,Function是所有函数的构造函数,先通过代码的方式举例一下通过new Function()的方式创建一个函数。

例如下面代码

	const sum = new Function('a', 'b', 'return a + b;');
	console.log(sum(2, 3)); // 输出 5
	
	const greet = new Function('name', 'console.log("Hello, " + name + "!");');
	greet('John'); // 输出 "Hello, John!"

​ 到这里我们知道了new Function() 构造函数可以动态地创建函数,从这里先解释一下上面有关Object的解释中的伏笔。既然所有函数都是Function生产出来的,那么Object这个构造函数是不是也是Function生产出来的呢? 答案是的。那Function自身怎么来的呢?答案是Function创造了Function,没错这是一个特殊的情况,因为万物都有个源头,Function和Object一样,都是JS在运行时就创建好了。

例如下面代码,可以先略过。看完下面有关constructor 的解释再回来看这个就理解了。

    function abc () {}
    console.log(abc.constructor === Function) // 输出 true
    console.log(Function.constructor  === Function) // 输出 true

这段代码就印证了,所有函数都是Function生产的(包括Function自身也是)

4. constructor

在JavaScript中,构造器(Constructor)是一个特殊类型的方法,它用于初始化一个新创建的对象。构造器通常在声明一个类的时候定义,并且当我们利用这个类创建新的对象时,构造器将会被自动调用。constructor 在实例对象中是一个属性,这个属性包含了(指向了)构造这个实例的构造函数,举例,Object.constructor 指向了Function。 也就是相当于。

例如下面代码

   console.log(Object.constructor === Function); // 输出:true

还有一个值得一提的是,大多数JavaScript对象和函数都有constructor属性,但这并不是绝对的。constructor属性的值取决于对象是如何创建的,以及是否有特定的构造函数与之关联,比如说,如果一个对象通过字面量方式创建,它的constructor属性会指向Object,而不是一个具体的构造函数。用代码举个例子

var obj = {}; console.log(obj.constructor === Object); // 输出: true

还可以通过Object.create(null)创建一个没有constructor属性的对象

var obj = Object.create(null); console.log(obj.constructor); // 输出: undefined

5. prototype

在JavaScript中,prototype是一个非常重要的概念,它主要用于实现继承和共享属性。每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型(prototype),每一个对象都会从原型"继承"属性。prototype是函数的属性,所有函数都会有这个属性,这个属性指向了这个函数(构造函数)的原型对象。因此,所有的通过同一个构造器函数创建出来的实例都会共享这个原型对象的属性。如果一个实例需要访问一个属性,它首先查找自己是否有这个属性,如果没有,它会在原型链上查找,直到找到这个属性或者查找到null为止。 疑问:通过字面量的方式创建的空对象原型是什么? 1,所有通过字面量方式创建的空对象都是以Object.prototype为原型的,比如当我们创建一个新的函数时。

例如下面代码

   function Person() {}
   console.log(Person.prototype.__proto__ === Object.prototype) // 输出true

所以大多数情况下JavaScript 对象的__proto__属性指向函数的prototype属性,同时这个函数的prototype.constructor属性又指回这个函数本身,形成一个环形引用。

6. __ proto __

在JavaScript中,一个对象的__proto__属性指向的就是这个对象构造函数prototype属性,即这个对象的原型对象。当我们通过new操作符创建新对象时,新对象的__proto__属性会被赋值为构造器函数的prototype。但__proto__属性并不是ECMAScript标准的一部分,而是一些浏览器特定的实现。在实际的编程中,通常不推荐直接使用__proto__,而是应该使用Object.getPrototypeOf()方法来获取一个对象的原型。

二、原型链

1.什么是原型链

原型链(Prototype Chain)是 JavaScript 中实现继承的一种机制。它是基于对象的,每个 JavaScript 对象都有一个原型(Prototype)属性,它指向另一个对象。当我们访问对象的属性或方法时,如果该对象本身没有该属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或方法或达到原型链的顶端(即 Object.prototype)。

原型链的形成是通过对象之间的原型继承实现的。在 JavaScript 中,每个对象都有一个隐藏的 [[Prototype]] 属性,它指向该对象的原型。当我们创建一个新对象时,该对象的原型会被设置为创建该对象的构造函数的 prototype 属性。

例如下面代码

	function Person(name) {
	  this.name = name;
	}
	
	Person.prototype.sayHello = function() {
	  console.log("Hello, " + this.name + "!");
	};
	
	var person = new Person("world");
	person.sayHello(); // 输出 "Hello, world!"

在上面的例子中,Person 是一个构造函数,通过 new 关键字创建了一个 person 对象。person 对象继承了 Person.prototype 的属性和方法,因此可以调用 sayHello() 方法。如果调用 person 对象的 sayHello() 方法时,JavaScript 引擎会首先查找 person 对象自身是否有 sayHello() 方法,如果没有,它会继续沿着原型链向上查找,直到找到 Person.prototype 上的 sayHello() 方法。

总结:通过原型链,可以实现属性和方法的共享,减少重复的代码。它是 JavaScript 中实现继承的一种简洁而灵活的方式。

2.原型链之间的链条关系

为了清晰的解释这个关系,我们先在脑海中抽象出两个线路,一个是函数线,一个对象线。这两个线,只是暂时的把函数和对象两个概念做个区分。但实际上,函数属于对象,只是一个特殊的对象而已,毕竟一切皆对象

牢记下面我抽象出来的两个知识点

函数prototype__proto__ , 对象__proto__

写一段伪代码,只是帮助你理解原型链。

	Function = {
		prototype,
		__proto__
	}
	Object= {
		__proto__
	}

函数

我们通过前面的概念了解到了,Function是所有函数的构造对象(也包括Function自身)

例如下面代码

	function abc() {}
	console.log(abc.constructor  === Function) // true
	console.log(Function.constructor  === Function) // true

可以通过创建一个abc的实例,然后使用Object.getPrototypeOf方法来获取该实例的原型对象,然后和abc.prototype进行比较。这样可以表示abc.prototype指向的是abc的原型对象,代码如下:

	function abc() {}
	let instance = new abc();
	console.log(abc.prototype === Object.getPrototypeOf(instance)); // 输出 true

目前我们知道了,abc的prototype指向的是自身的原型对象。那么自身的原型对象又指向那里呢

	console.log(abc.prototype.__proto__); // 输出 Object的原型对象

上面这个输出可以分为两段来看,第一段是 abc.prototype 指向了 自身的原型对象,既然是对象了,那么就没有prototype属性了,不信的话可以在控制台输出一句console.log(abc.prototype.prototype),看看返回什么。所以我们只能用__proto__了,目前我们已经知道了__proto__指向的是当前这个对象的构造函数的原型对象。那么谁是对象的构造函数? 答案是Object(), 到这里是不是醍醐灌顶? 我们通过代码来印证一下

	console.log(abc.prototype.__proto__ === Object.prototype); // 输出: true

到这里是不是还会有个疑问?既然我们自定义的函数的原型链是这样的,那么 Function这个所有函数的构造函数有没有原型链呢?答案是有的,只不过有些特殊。通过下面代码来解释一下:

	console.log(Function.prototype); // 输出: ƒ () { [native code] }

从这段输出,我们看到了。 Function.prototype指向的是 ƒ () { [native code] }ƒ () { [native code] }代表的是一个空函数,这个空函数其实是JS在运行时创建的,他就是是Function的原型对象。别看是个空函数,但他是个对象。是一个特殊的原型对象。那么继续考虑,这个特殊对象, 毕竟它也是一个函数,既然都叫函数了,Function.prototype应该有prototype,,通过代码来看一下:

	console.log(Function.prototype.prototype); // 输出: undefined

输出的是undefined,奇怪吧? 函数的prototype输出的是undefined。这其实就是特殊的地方,这个空函数虽然是函数,但是你可以理解为它就是个对象。如果你把它直接当成对象来考虑一切就都通透了,开头我们说过了,对象只有__proto__。那么通过代码看一下:

	console.log(Function.prototype.__proto__ === Object.prototype); // 输出: true

上面代码输出的是true,其实到这里,函数这个线暂时到头了。也证明了,函数是对象,可以使用对象原型的方法

对象

我们通过前面的概念了解到了,Object是所有对象的构造函数。

例如下面代码,通过构造函数的方式创建了一个空对象

	let obj = new Object()
    console.log(obj) // 输出 {} 

现在开始先从Object()这个构造函数开始思考, Object(),是一个构造函数对吧,既然是个函数那么一定有prototype属性,下面通过代码看一下:

    console.log(Object.prototype) // 输出了Object的对象原型 {constructor: ƒ, __defineGetter__: ƒ,…}

输出了Object的对象原型,那么既然是原型对象了,那么就没有prototype,只有__proto__了,下面输出一下

    console.log(Object.prototype.__proto__) // null

输出了 Null, 这个Null其实就是对象线的尽头了,也是原型链的尽头了,不管是我们抽象出来的函数线还是对象线,最后都会在汇成一条线,并最终指向Null

结合上面所说,思考一下Object.__proto__ === Function.prototype 这段代码是true还是false,可以在评论区写一下,并解释一下为什么。

总结

1,每个对象都有 proto 属性,但只有函数对象才有 prototype 属性。proto 指向创建该对象的构造函数的 prototype。

2,当试图访问一个对象的某个属性时,JavaScript 会首先在该对象自身的属性中查找。如果没有找到,它就会沿着原型链往上查找,直到找到为止。如果整个原型链上都没有找到这个属性,那么返回 undefined。

本文纯原创,感谢支持,谢谢观看!!!