JavaScript面向对象名词详解

JavaScript是一门基于原型设计的语言。
这句话其实描述了JavaScript这门语言关于面向对象设计的一个最重要的特性。区别于常见的面向对象语言,JavaScript对于OOP的实现,有自己的一套设计逻辑和实现方式。正是因为有别于传统常见的方式,只有掌握了它相关的基础概念,才能彻底掌握和理解JavaScript关于面向对象的内容。这篇文章致力于全面梳理JavaScript中关于面向对象和继承相关的名词,彻底理解它们,是至关重要的一步。

原型

JavaScript是基于原型设计的语言,那第一步,当然是先搞清楚到底什么是原型?要搞清楚什么是原型,先了解下JavaScript中的数据类型。
JavaScript中的数据类型有两种:基本数据类型和引用数据类型。

  • 基本数据类型包括:数字(Number),字符串(String),布尔值(Boolean),null,undefined,Symbol
  • 引用数据类型包括:对象(Object),数组(Array),函数(Function)
    基本类型的数据暂且不说,引用类型的数据都有什么特性呢?引用类型的数据都可以有自己的属性,来看一下。
let arr = [1,2,3];
arr.width = 5;

let obj = {};
obj.width = 7;

let fn = function() {};
fn.width = 9;

console.log(arr.width);     // 5
console.log(obj.width);     // 7
console.log(fn.width);      // 9
复制代码

这是JavaScript里最基本的概念,引用类型的数据可以有自己的属性,甚至方法,但是这跟原型有什么关系?
原型,是JavaScript中函数类型数据才有的属性。
只有函数才有原型,原型只是函数的一个特殊属性,仅此而已。那么原型是用来干什么的?其实也很简单,原型就是用来"传承"一个函数的属性和方法的,也就是说,如果一个函数的属性和方法挂载到它的prototype属性上,那么,通过new这个函数所创建的对象,都可以使用prototype属性上的属性和方法。

let fn = function() {};
fn.prototype.name = 'abc';
fn.prototype.say = function() {
    console.log(this.name);
}

let f1 = new fn();
f1.name;    // abc
f1.say();   // abc
复制代码

这就是JavaScript里继承的原理,把属性和方法挂载到函数的原型(prototype)上,这样通过new这个函数创建的实例,就能使用这些属性和方法了。但是有一点需要注意,实例可以使用这些属性和方法,但不是说这些属性和方法是实例的,这就牵涉到另一个概念:原型链。

原型链 && __proto__

希望我已经说清楚了什么是原型,原型只是函数这种类型数据所拥有的一种特殊属性而已,挂载到原型上的属性和方法能够被实例所使用。而且我们说,这些属性和方法并不是实例的,而是函数的,那实例是通过什么方式获取和使用这些属性和方法的呢?这就是原型链的作用了。
我们知道,JavaScript有许多内置的函数(也叫构造函数),比如:String,Array,Date,RegExp,Functioin,Number等等。除了undefined和null这两个另类,JavaScript中所有的数据,都是可以由这些内置的函数创建的。我们都知道,每种类型的数据都会有自己的方法,比如字符串有splice,split等方法,函数有call,bind等方法,数字有toFixed等方法。
通过原型的概念,我们也了解到,这些方法都是通过挂载到构造函数的prototype属性上,他们才能获取使用的。那么每一种类型的数据,是通过什么找到自己对应的构造函数的原型上的属性和方法的呢?这需要通过一个属性:__proto__。
原型是函数才有的属性,__proto__是所有数据都有的属性(除了null和undefined)。
字符串有自己的__proto__,数字有自己的__proto__,函数有自己的__proto__,原型,也有自己的__proto__。通过__proto__,大家都可以找到自己构造函数的原型(也就是说数据的__proto__是指向它构造函数的原型的)。我们来看下。

'abc'.__proto__ === String.prototype;       // true
567..__proto__ === Number.prototype;        // true(数字的写法需要注意一下,要两个".")
[1,2,3].__proto__  === Array.prototype;     // true
复制代码

构造函数又可以通过它自己的__proto__属性,往上查找它自己的构造函数的原型。比如。

Array.__proto__ === Function.prototype;     // true
Date.__proto__ === Function.prototype;      // true
Error.__proto__ === Function.prototype;     // true
Function.__proto__ === Function.prototype;  // true
复制代码

连Function的__proto__都指向它自己的原型,我们不禁好奇,Function.prototype的__proto__又指向谁?(我们说过了,除了null和undefined,所有数据都是有__proto__属性的)。

Function.prototype.__proto__ === Object.prototype;      // true
复制代码

继续,看看Object.prototype的__proto__又指向谁。

Object.prototype.__proto__;     // null
复制代码

竟然是个null,一个没有__proto__属性的东西。至此,这个链条就走到了它的尽头了。这就是原型链,我们从一个最具体的数据,然后通过它的__proto__属性,一层一层往上找,一直到Object.prototype,一直到null。这就是“毅种循环”,也就是JavaScript中的原型链。我们再通过一个数字的原型链之旅来感受一下。

let n = 123;

n.__proto__ === Number.prototype;
Number.__proto__ === Function.prototype;
Function.__proto__ === Function.prototype;
Function.prototype.__proto__ === Object.prototype;
Object.prototype.__proto__ === null;
复制代码

把n替换成任何null和undefined之外的数据,都是一样的。在这个链条上,所有挂载在prototype上的方法,n都可以使用,但是n自己身上并没有这些方法。这就是原型链的作用。

constructor

constructor这个单词的字面意思就是构造器。凡是能够通过函数创建的数据,都有constructor属性。也就是说,数据的constructor属性指向它的构造函数。看几个例子就明白了。

// 数组:Array
let arr = [1,2,3];
arr.constructor === Array;      // true

// 数字:Number
let num = 123;
num.constructor === Number;     // true

// 构造函数
Array.constructor === Function;         // true
Object.constructor === Function;        // true
Function.constrcutor === undefined;     // true
复制代码

到了Function这里constructor就断了,这里就是尽头(constructor总是指向函数的,更确切的说是构造函数)。稍微有些不同的是,prototype对象也有constructor属性,它指向函数本身。

Array.prototype.constructor === Array;          // true
Number.prototype.constructor === Number         // true
Function.prototype.constructor === Function;    // true
复制代码

所以,关于constructor这个属性,我们记住两点就可以了:
1.数据的constructor属性,指向它的构造函数。
2.原型(prototype)对象的constructor属性,指向函数本身。

构造函数

JavaScript中构造函数其实就是函数,有特定用途的函数。这种特定用途是什么?一般来说,是用于创建特定的对象,而这种创建对象的方式,就是通过new关键字来调用构造函数。

let f1 = function() {};
let f2 = function() {
    this.name = 'jack';
}

let a = new f1();
let b = new f2();
console.log(a);     // {}
console.log(b);     // {name: "jack"}
复制代码

在这个例子中,f1,f2都是函数,f1,f2也都是构造函数。但是我们一般不会把f1叫作构造函数,因为没有意义--f1内部无论代码如何庞大复杂,只要没有出现一个关键字,通过new方式调用f1,都是没有意义的,这个关键字就是:this。
我们一般会把具有这两样特征的函数,称为构造函数:
1.函数内部会有this能够设定一些属性和方法;
2.函数的原型(prototype)上会挂载一些属性和方法。
只有这样,我们通过new去调用这个函数的时候,才能生成一些特定的对象,这样才有意义嘛,就像上面例子中的f2函数一样,可以通过new调用生成一个具有name属性的对象。
现在我们知道,构造函数就是函数,没有区别。只是通常构造函数会有许多属性和方法,无论是在函数内部,还是在函数的原型上,这样就能用于生成特定的对象了。构造函数的首字母通常会采用大写字母,用于区别普通的函数。

关于JavaScript中的面向对象,还有另外重要的一环,就是对this的理解,这个至关重要。关于这方面的解释,有兴趣可以参考本人的另外一篇文章:《JavaScript中的this详解》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值