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

📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 近期刚转战 CSDN,会严格把控文章质量,绝不滥竽充数,欢迎多多交流。👍


写在前面的话

又回到前端系列了,之前写后端和其他内容去了,进度落下,赶紧补一下。
本篇文章介绍一下 JavaScript 里面的一些经典知识点,先以prototype原型链开篇。
让我们开始!


快速了解

【概念说明】
JavaScript中,每个对象都有一个原型prototype属性,它指向另一个对象。这个被指向的对象也有自己的原型,以此类推,最终形成了一个原型链。原型链的顶端是Object.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.

浏览器运行该代码,F12控制台查看alice对象,如下图所示:
image.png
分析一下上图的这些内容:

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

image.png

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 继承。

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 方式。

Person.prototype.nationality = "English";
console.log(alice.__proto__.nationality);

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


关键词 constructor
前面示例一定看到了原型对象上有一个 constructor 属性,这是何物?
其实就是和 Java 中的构造函数类似的概念,用下面代码可以看出来,原型的 constructor就是指向类本身。

alice.__proto__.constructor === Person  // true

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

几个概念复述一下:

  • 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)。
这个前面章节都在介绍这种方式,示例代码再贴一下?

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 的实例。

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:这也是推荐方式,不过一般复杂的前端逻辑才会去专门设计和定义类。

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 实现继承

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

// 定义父类函数
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】
是不是很简单?这一节仅仅是告诉读者又这么一种方法,这种方法是这样运用的。

(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】

(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】
在实际编程中所用到的肯定不只是已有方法的增强,更多的实行的功能的要求。

(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 做的一些测试代码,可以跳过。

===== 第一例,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
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

战神刘玉栋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值