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); // 5console.log(obj.width); // 7console.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; // abcf1.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; // true567..__proto__ === Number.prototype; // true(数字的写法需要注意一下,要两个".")[1,2,3].__proto__ === Array.prototype; // true复制代码
构造函数又可以通过它自己的__proto__属性,往上查找它自己的构造函数的原型。比如。
Array.__proto__ === Function.prototype; // trueDate.__proto__ === Function.prototype; // trueError.__proto__ === Function.prototype; // trueFunction.__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属性指向它的构造函数。看几个例子就明白了。
// 数组:Arraylet arr = [1,2,3];arr.constructor === Array; // true// 数字:Numberlet num = 123;num.constructor === Number; // true// 构造函数Array.constructor === Function; // trueObject.constructor === Function; // trueFunction.constrcutor === undefined; // true复制代码
到了Function这里constructor就断了,这里就是尽头(constructor总是指向函数的,更确切的说是构造函数)。稍微有些不同的是,prototype对象也有constructor属性,它指向函数本身。
Array.prototype.constructor === Array; // trueNumber.prototype.constructor === Number // trueFunction.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的理解,这个至关重要。关于这方面的解释,有兴趣可以参考本人的另外一篇文章: