《无所不能的JavaScript · prototype 原型链》

📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗

🌻 近期刚转战 CSDN,会严格把控文章质量,绝不滥竽充数,欢迎多多交流。👍

文章目录


写在前面的话

又回到前端系列了,之前写后端和其他内容去了,进度落下,赶紧补一下。

本篇文章介绍一下 JavaScript 里面的一些经典知识点,先以prototype原型链开篇。

让我们开始!


快速了解

【概念说明】

JavaScript中,每个对象都有一个原型prototype属性,它指向另一个对象。这个被指向的对象也有自己的原型,以此类推,最终形成了一个原型链。原型链的顶端是Object.prototype,它是所有对象的根原型。

当我们访问一个对象的属性时,如果该对象自身没有这个属性,JavaScript会沿着原型链向上查找,直到找到匹配的属性或者到达原型链的末端。

【用代码说话】

直接上一段示例代码:

javascript 复制代码
function Person(name, age) {
	this.name = name;
	this.age = age;
}

Person.prototype.sayHello = function() {
	console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};

const alice = new Person('Alice', 20);
alice.sayHello();  // 输出:Hello, my name is Alice and I am 20 years old.

浏览器运行该代码,F12控制台查看alice对象,如下图所示:

分析一下上图的这些内容:

  • alice 对象包含 age 和 name 两个属性和一个原型对象
  • 原型对象可以通过 alice.proto 访问,或 Person.prototype,两者是相等的
  • 原型对象的再上一次层指向 Object.prototype,再往上一层就是 null
  • 对,没有错,你看到了一个类似链式调用的东西,这是原型链

Tips:学习前端,特别是JavaScript,直接运行Demo,F12查看效果是一个快速学习方式。


知识介绍

prototype 与原型链

在 JavaScript 中,prototype 是用于实现继承和共享属性与方法的一种机制。每一个 JavaScript 对象(除了 null)在创建时都与另一个对象(称为其 "prototype")相关联。这个 "prototype" 对象本身也有一个 "prototype",这样一直到一个对象的 prototype 为 null 为止,这形成了一个链条,被称为 "原型链"(prototype chain)。


关于 prototype 继承

所有的 JavaScript 对象都会从一个 prototype(原型对象)中继承属性和方法:

Date 对象从 Date.prototype 继承。

Array 对象从 Array.prototype 继承。

Person 对象从 Person.prototype 继承。

所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。

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

Date 对象、Array 对象、以及 Person 对象从 Object.prototype 继承。

javascript 复制代码
console.log(alice.__proto__ === Person.prototype);  // 输出:true
console.log(Person.prototype.__proto__ === Object.prototype);  // 输出:true
console.log(Object.prototype.__proto__ === null);  // 输出:true

使用 prototype 添加属性

JavaScript 对象要添加新的属性或方法,很简单。

有点像 Java 的Map那边随意,直接追加即可,类似 alice.xxx = '1111'

但如果想追加的属性是和其他对象,继承和共享的,那就可以用到 prototype 方式。

javascript 复制代码
Person.prototype.nationality = "English";
console.log(alice.__proto__.nationality);

使用 prototype 属性就可以给对象的构造函数添加新的属性和方法。


关键词 constructor

前面示例一定看到了原型对象上有一个 constructor 属性,这是何物?

其实就是和 Java 中的构造函数类似的概念,用下面代码可以看出来,原型的 constructor就是指向类本身。

javascript 复制代码
alice.__proto__.constructor === Person  // true

结合前面说的内容, 整个概念是否绕来绕去,找了下面这张图,感觉能比较直观了看明白之间的调用关系。

几个概念复述一下:

  • prototype 是 JavaScript 中实现继承和共享属性与方法的基础。
  • 构造函数的 prototype 属性用于定义所有实例共享的方法和属性。
  • 每个对象都有一个 proto 属性,对象通过该属性引用其原型,形成原型链。
  • 每个函数在定义时会自动获得一个 prototype 属性,这个属性是一个对象,包含一个 constructor 属性指向函数本身。
  • 当访问对象的一个属性或方法时,JavaScript 会首先在对象本身查找。如果没有找到,它会沿着原型链向上查找,直到找到或到达链的末端(即 null)。

Tips:理解 prototype 和原型链是掌握 JavaScript 面向对象编程的关键。通过这种机制,可以实现代码的重用和更高效的内存使用。


原型链的性能和优化

原型链在JavaScript中的运作会带来一定的性能开销。在访问属性时,查找过程需要沿着原型链逐级查找,直到找到属性或者到达原型链末端。因此,过深的原型链结构可能导致性能下降。为了优化性能,可以合理设计对象的原型链,避免过于庞大和复杂的结构。

Tips:简单来说,就是不要太复杂,但一般前端设计也不应该很重,最多父子类的结构了,损耗可以忽略不计。


Java 和 JavaScript 面向对象

JavaScript 和 Java 都是面向对象编程语言,但它们在类和对象的实现和使用上有一些显著的差异。以下是对 JavaScript 中类和对象的介绍,并通过 Java 的概念进行对比说明。
类:

JavaScript ES6 引入类,使得类定义更加简洁和直观,但其底层依然基于原型链。

Java 中类是核心概念,定义了对象的属性和行为。
对象:

JavaScript 对象是键值对的集合,可以动态修改。

Java 对象是类的实例,属性和方法在类中定义。
继承:

JavaScript 通过 extends 关键字实现类的继承,使用 super 调用父类的构造函数和方法。

Java 通过 extends 关键字实现类的继承,使用 super 关键字调用父类的构造函数和方法。
总结:

尽管 JavaScript 和 Java 在语法和特性上有很多不同,但它们都支持面向对象编程的基本概念,如类、对象和继承。JavaScript 的类和对象模型更加灵活和动态,而 Java 的类和对象模型更为静态和严格。


用法拓展

创建对象的N种方式

对于 Java 后端程序猿而言,类与对象不是 Java 面向对象的概念吗,前端怎么也有?

其实 JavaScript 已经不是后端眼中随意简单的脚本语言了,Java 有的概念它也有,Java 能做的很多事情,它也能做。

我们有多种方式可以创建对象,根据不同的创建方式,JavaScript对象的原型链也会有所不同。

构造函数方式

使用构造函数方式创建对象的原型是构造函数的原型对象(prototype)。

这个前面章节都在介绍这种方式,示例代码再贴一下?

javascript 复制代码
function Person(name, age) {
	this.name = name;
	this.age = age;
}

Person.prototype.sayHello = function() {
	console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};

const alice = new Person('Alice', 20);
alice.sayHello();  // 输出:Hello, my name is Alice and I am 20 years old.

console.log(alice.__proto__ === Person.prototype);  // 输出:true

字面量方式创建对象

使用字面量方式创建对象的原型是 Object.prototype,即所有字面量创建的对象都是 Object 的实例。

javascript 复制代码
const lwObj = {
	name: '战神',
	age: 32,
	sayHello() {
		console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
	}
};

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

Class 方式创建对象

在 ES6 之前,JavaScript 没有类的概念,开发者主要通过构造函数和原型链来实现面向对象编程。ES6 引入了类的语法,使得定义和继承类更加直观和类似于其他面向对象语言(如 Java)。

Class 方式创建的对象和构造函数方式创建的对象一样,其原型是Class构造函数的prototype属性所指向的对象。

Tips:这也是推荐方式,不过一般复杂的前端逻辑才会去专门设计和定义类。

javascript 复制代码
class PersonClass {
	constructor(name, age) {
		this.name = name;
		this.age = age;
	}
	sayHello() {
		console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
	}
}

const hanMei = new PersonClass('HanMei', 25);
hanMei.sayHello();  // 输出:Hello, my name is HanMei and I am 25 years old.	

console.log(hanMei.__proto__ === PersonClass.prototype);  // 输出:true

prototype 实现继承

直接看下面的代码,就是基于原型链的特性。

javascript 复制代码
// 定义父类函数
function Company(){  
    this.boss = 'lw';  
	  this.name = 'cjwmy';  
    this.show = function(){  
        return this.boss + ',' + this.name;  
    }  
}  

// 定义子类函数
function Zysoft(){  
    this.soft = ['zyhip','zycomm'];  
}  

// 绑定原型链
Zysoft.prototype = new Company();  
let one = new Zysoft();  
console.log(one.show());  //lw,cjwmy

扩展原有对象的方法 Demo

首先,我们要先了解一下类的概念,JavaScript 本身是一种面向对象的语言,它所涉及的元素根据其属性的不同都依附于某一个特定的类。我们所常见的类包括:数组变量(Array)、逻辑变量(Boolean)、日期变量(Date)、结构变量(Function)、数值变量(Number)、对象变量(Object)、字符串变量(String) 等,而相关的类的方法,也是程序员经常用到的(在这里要区分一下类的注意和属性发方法),例如数组的push方法、日期的get系列方法、字符串的split方法等等,但是在实际的编程过程中不知道有没有感觉到现有方法的不足?prototype 方法应运而生!

【最简单的例子,自己写add等方法,了解 prototype】

是不是很简单?这一节仅仅是告诉读者又这么一种方法,这种方法是这样运用的。

javascript 复制代码
(1) Number.add(num):作用,数字相加
实现方法:Number.prototype.add = function(num){return(this+num);}
试验:alert((3).add(15)) -> 显示 18

(2) Boolean.rev(): 作用,布尔变量取反
实现方法:Boolean.prototype.rev = function(){return(!this);}
试验:alert((true).rev()) -> 显示 false

(3)测试demo
String.prototype.test = function(value) {
    alert(1)
};

【已有方法的实现和增强,初识 prototype】

javascript 复制代码
(1) Array.push(new_element)
//作用:在数组末尾加入一个新的元素
Array.prototype.push = function(new_element){
    this[this.length]=new_element;
    return this.length;
}
//让我们进一步来增强他,让他可以一次增加多个元素!
Array.prototype.pushPro = function() {
    var currentLength = this.length;
    for (var i = 0; i < arguments.length; i++) {
        this[currentLength + i] = arguments[i];
    }
    return this.length;
}

(2)String.length
//作用:这实际上是 String 类的一个属性,但是由于 JavaScript 将全角、半角均视为是一个字符,
//在一些实际运用中可能会造成一定的问题,现在我们通过 prototype 来弥补这部不足。
String.prototype.cnLength = function(){
    var arr=this.match(/[^\x00-\xff]/ig);
    return this.length+(arr==null?0:arr.length);
}
alert("EaseWe空间Spaces".cnLength()) -> 显示 16

【新功能的实现,深入 prototype】

在实际编程中所用到的肯定不只是已有方法的增强,更多的实行的功能的要求。

javascript 复制代码
(1) String.left()
//问题:用过 vb 的应该都知道left函数,从字符串左边取 n 个字符,但是不足是将全角、半角均视为是一个字符,造成在中英文混排的版面中不能截取等长的字符串
//作用:从字符串左边截取 n 个字符,并支持全角半角字符的区分
String.prototype.left = function(num,mode){
    if(!/\d+/.test(num))return(this);
    var str = this.substr(0,num);
    if(!mode) return str;
    var n = str.Tlength() - str.length;
    num = num - parseInt(n/2);
    return this.substr(0,num);
}
alert("EaseWe空间Spaces".left(8)) -> 显示 EaseWe空间
alert("EaseWe空间Spaces".left(8,true)) -> 显示 EaseWe空
//本方法用到了上面所提到的String.Tlength()方法,自定义方法之间也能组合出一些不错的新方法呀!

(2) Date.DayDiff()
作用:计算出两个日期型变量的间隔时间(年、月、日、周)
Date.prototype.DayDiff = function(cDate,mode){
       try{
           cDate.getYear();
       }catch(e){
           return(0);
       }
       var base =60*60*24*1000;
       var result = Math.abs(this - cDate);
       switch(mode){
           case "y":
               result/=base*365;
               break;
           case "m":
               result/=base*365/12;
               break;
           case "w":
               result/=base*7;
               break;
           default:
               result/=base;
               break;
       }
       return(Math.floor(result));
   }
alert((new Date()).DayDiff((new Date(2002,0,1)))) -> 显示 329
alert((new Date()).DayDiff((new Date(2002,0,1)),"m")) -> 显示 10
//当然,也可以进一步扩充,得出响应的小时、分钟,甚至是秒。

(3) Number.fact()
//作用:某一数字的阶乘
Number.prototype.fact=function(){
   var num = Math.floor(this);
   if(num<0)return NaN;
   if(num==0 || num==1)
       return 1;
   else
       return (num*(num-1).fact());
}
alert((4).fact()) -> 显示 24
//这个方法主要是说明了递归的方法在 prototype 方法中也是可行的!

(4) Array.indexof()
//下面是给js中的Array加的一个方法(本来js中Array是没有返回下标的方法的)
Array.prototype.indexof = function(value) {
    var a = this;
    for (var i = 0; i < a.length; i++) {
        if (a[i] == value)
            return i;
    }
}
var arr = ['1', '2', '3', '4', '5', '6', '7'];
alert(arr.indexof('3'));

prototype 基础测试 Demo

本章节,是博主早期针对 prototype 做的一些测试代码,可以跳过。

latex 复制代码
===== 第一例,prototype初使用--为类型增加额外的方法和属性  
Object.prototype.Property = 1;
Object.prototype.Method = function (){ 
    alert(1);
}
var obj = new Object();
alert(obj.Property);
obj.Method();
结论:可以在类型上使用proptotype来为类型添加行为,这些行为只能在类型的实例上体现,并且在增加之前实例化的对象也享有这些变化。
规则1:JS中允许的内置类型有Array, Boolean, Date, Enumerator, Error, Function, Number, Object, RegExp, String;
规则2:直接用Object.Method()也可以调用(但一般不这么使用),因为Object是所有对象(包括函数对象)的基础,其他内置类型是不可以。

===== 第二例,在实例对象上不能使用prototype,否则发生编译错误 
var obj = new Object();
obj.prototype.Property = 1; //Error
obj.prototype.Method = function() //Error
{
    alert(1);
}

===== 第三例,直接给类型定义"静态"属性和方法,只能使用类型本身调用,对象无法调用,同Java的static关键字声明的类变量
Object.Property = 1;
Object.Method = function()
{
    alert(1);
}
alert(Object.Property);
Object.Method();    //正常调用
var obj = new Object();
alert(obj.Property);  //Error实例不能调用类型的静态属性或方法,否则发生对象未定义的错误。

===== 第四例,演示通常在JavaScript中定义一个自定义类型的方法
function Aclass()
{
this.Property = 1;
this.Method = function()
{
    alert(1);
}
}
var obj = new Aclass();
alert(obj.Property);
obj.Method();

===== 第五例,接第四例,可以在外部使用prototype为自定义的类型添加属性和方法。
Aclass.prototype.Property2 = 2;
Aclass.prototype.Method2 = function
{
    alert(2);
}
var obj = new Aclass();
alert(obj.Property2);
obj.Method2();

===== 第六例,接第四例,在外部不能通过prototype改变自定义类型的属性或方法,不会报错但改变无效;
但如果属性本身就是通过prototype扩展的,则可以继续用prototype修改。
Aclass.prototype.Property = 2;
Aclass.prototype.Method = function
{
    alert(2);
}
var obj = new Aclass();
alert(obj.Property); //仍然弹出1
obj.Method();

===== 第七例,接第四例,可以在对象上改变属性。(这个是肯定的)
var obj = new Aclass();
obj.Property = 2;
obj.Method = function()
{
    alert(2);
}
alert(obj.Property);
obj.Method();
也可以在对象上改变方法。(和普遍的面向对象的概念不同)

===== 第八例,接第四例,可以在对象上增加属性或方法   
var obj = new Aclass();
obj.Property2 = 2;
obj.Method2 = function()
{
    alert(2);
}
alert(obj.Property2);
obj.Method2();

===== 第九例,接第四例,这个例子说明了一个类型如何从另一个类型继承。  
function AClass2()
{
       this.Property2 = 2;
       this.Method2 = function()
       {
              alert(2);
       }
}
AClass2.prototype = new AClass();
 
var obj = new AClass2();
alert(obj.Property);
obj.Method();
alert(obj.Property2);
obj.Method2();

===== 第十例,接上例, 这个例子说明了子类如何重写父类的属性或方法。但不能改变自身的属性和方法。
AClass2.prototype = new AClass();()
AClass2.prototype.Property = 3;
AClass2.prototype.Method = function()
{
       alert(4);
}
var obj = new AClass2();
alert(obj.Property);
obj.Method();
可见JavaScript能够实现的面向对象的特征有:·公有属性(public field)·公有方法(public Method)·私有属性(private field)·私有方法(private field)·方法重载(method overload)·构造函数(constructor)·事件(event)·单一继承(single inherit)·子类重写父类的属性或方法(override)·静态属性或方法(static member)

总结陈词

本篇文章分享一下 JS 世界中prototype的用法,希望能帮助到大家。

后端程序猿如果想往全栈工程狮发展,那JavaScript必须和Java玩得一样明白,接下来系列文章,带你一起探索,无所不能的JavaScript

💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。

相关推荐
小陈同学呦4 小时前
前端如何处理订单状态导航的数据竞态问题
前端·javascript
开发者每周简报4 小时前
网海三部曲·无名宗师传
javascript·人工智能
isyangli_blog5 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb2008115 小时前
FastAPI APIRouter
开发语言·python
Benszen5 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆5 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木5 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
杨充6 小时前
1.3 浮点型数据设计灵魂
开发语言·python·算法
噜噜噜阿鲁~6 小时前
python学习笔记 | 11.3、面向对象高级编程-多重继承
java·开发语言
basketball6166 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang