javascript原型和继承
“一切都是对象”这句话的重点在于如何去理解“对象”这个概念。当然,也不是所有的都是对象,值类型就不是对象。
javascript
中判断一个变量是不是对象?
function show(x) {
console.log(typeof x); // undefined
console.log(typeof 10); // number
console.log(typeof 'abc'); // string
console.log(typeof true); // boolean
console.log(typeof function () { }); //function
console.log(typeof [1, 'a', true]); //object
console.log(typeof { a: 10, b: 20 }); //object
console.log(typeof null); //object
console.log(typeof new Number(10)); //object
}
show();
以上代码列出了typeof输出的集中类型标识,其中上面的四种(undefined
, number
, string
, boolean
)属于简单的值类型,不是对象。剩下的几种情况——函数、数组、对象、null
、new Number(10)
都是对象。 他们都是引用类型。
var fn = function () { };
console.log(fn instanceof Object); // true
判断一个变量是不是对象非常简单。值类型的类型判断用typeof
,引用类型的类型判断用instanceof
。
有个疑问。在typeof
的输出类型中,function
和object
都是对象,为何却要输出两种答案呢?都叫做object
不行吗?
函数和对象的关系
函数是一种对象,但是函数却不像数组一样——你可以说数组是对象的一种,因为数组就像是对象的一个子集一样。但是函数与对象之间,却不仅仅是一种包含和被包含的关系,函数和对象之间的关系比较复杂,甚至有一点鸡生蛋蛋生鸡的逻辑,
用案例来解答:
function Fn() {
this.name = '小明';
this.sex = 'male';
}
var fn1 = new Fn();
上面的这个例子很简单,它能说明:对象可以通过函数来创建。
var obj = new Object();
obj.a = 10;
obj.b = 20;
var arr = new Array();
arr[0] = 5;
arr[1] = 'x';
arr[2] = true;
Object 和 Array 都是函数:
console.log(typeof (Object)); // function
console.log(typeof (Array)); // function
所以,可以很负责任的说——对象都是通过函数来创建的。
有个疑问:对象是函数创建的,而函数却又是一种对象,函数和对象到底是什么关系啊???
prototype原型
函数也是一种对象。他也是属性的集合,你也可以对函数进行自定义属性,javascript
自己就先做了表率,人家就默认的给函数一个属性——prototype
。每个函数都有一个属性叫做prototype
。
这个prototype
的属性值是一个对象(属性的集合,再次强调!),默认的只有一个叫做constructor
的属性,指向这个函数本身。
SuperType是是一个函数,右侧的方框就是它的原型。
原型既然作为对象,属性的集合,不可能就只有constructor
,肯定可以自定义的增加许多属性。例如Object
,他的prototype
里面,就有好几个其他属性。
你也可以在自己自定义的方法的prototype
中新增自己的属性
function Fn() { }
Fn.prototype.name = '小明';
Fn.prototype.getSex = function () {
return 'male';
};
var fn = new Fn();
console.log(fn.name);
console.log(fn.getSex());
Fn
是一个函数,fn
对象是从Fn
函数new
出来的,这样fn
对象就可以调用Fn.prototype
中的属性。因为每个对象都有一个隐藏的属性——“__proto__ ”,这个属性引用了创建这个对象的函数的prototype
。 即:fn.__proto__ === Fn.prototype
这里的"__proto__ "
成为“隐式原型”
隐式原型
每个函数function
都有一个prototype
,即原型。这里再加一句话——每个对象都有一个__proto__
,可成为隐式原型。
这个__proto__
是一个隐藏的属性,javascript
不希望开发者用到这个属性值,有的低版本浏览器甚至不支持这个属性值。
上面截图看来,obj.__proto__
和Object.prototype
的属性一样!这么巧!
答案就是一样。
obj这个对象本质上是被Object
函数创建的,因此obj.__proto__ === Object.prototype
。我们可以用一个图来表示。
即,每个对象都有一个__proto__
属性,指向创建该对象的函数的prototype
。
那么上图中的“Object prototype”
也是一个对象,它的__proto__
指向哪里?
在说明“Object prototype”
之前,先说一下自定义函数的prototype
。自定义函数的prototype
本质上就是和 var obj = {}
是一样的,都是被Object
创建,所以它的__proto__
指向的就是Object.prototype
。
但是Object.prototype
确实一个特例——它的__proto__
指向的是null
函数也是一种对象,函数也有__proto__
吗?
函数也是被创建出来的。谁创建了函数呢?Function
注意这个大写的“F”
。
function fn(x,y) {
return x + y;
};
console.log(fn(10, 20));
var fn1 = new Function('x' , 'y', 'return x + y;');
console.log(fn1(5,6));
以上代码中,第一种方式是比较传统的函数创建方式,第二种是用new Functoin
创建。
根据上面说的一句话——对象的__proto__
指向的是创建它的函数的prototype
,就会出现:Object.__proto__ === Function.prototype
。用一个图来表示。图中标出了:自定义函数Foo.__proto__
指向Function.prototype
,Object.__proto__
指向Function.prototype
Function.prototype
指向的对象,它的__proto__
是不是也指向Object.prototype
答案是肯定的。因为Function.prototype
指向的对象也是一个普通的被Object
创建的对象,所以也遵循基本的规则。
介绍一下instanceof
先看例子
function Foo() {};
var f1 = new Foo();
console.log(f1 instanceof Foo); //true
console.log(f1 instanceof Object) //true
Instanceof
运算符的第一个变量是一个对象,暂时称为A
;第二个变量一般是一个函数,暂时称为B
。Instanceof
的判断规则是:沿着A
的__proto__
这条线来找,同时沿着B
的prototype
这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回true
。如果找到终点还未重合,则返回false
。所以 f1 instanceof Object
这句代码就是true
介绍一下继承
javascript
中的继承是通过原型链来体现的
function Foo() { };
var f1= new Foo();
f1.a = 10;
Foo.prototype.a = 100;
Foo.prototype.b = 200;
console.log(f1.a);
console.log(f1.b);
f1
是Foo
函数new
出来的对象,f1.a
是f1
对象的基本属性,f1.b
是怎么来的呢?——从Foo.prototype
得来, 因为f1.__proto__
指向的是Foo.prototype
访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着__proto__
这条链向上找,这就是原型链。
访问f1.b
时,f1
的基本属性中没有b
,于是沿着__proto__
找到了Foo.prototype.b
。有疑问可以看看上一节将instanceof
时候那个大图
继承的演变
ES3 => ES5 => ES6 的变化过程
因为我们现在用的react
做的项目对ES6
语法用的比较多,所以再详细说一下:
extends
super
static
下面是ES3
的写法
function Student(props) {
this.name = props.name || 'Unnamed';
} //构造函数
Student.prototype.hello = function() {
console.log('Hello, ' + this.name + '!');
} //原型
function PrimaryStudent(props) {
Student.call(this, props); //拿到Student构造函数的this.name = props.name || 'Unnamed';
this.grade = props.grade || 1;
}
// es3实现继承
function F() {}
F.prototype = Student.prototype;
PrimaryStudent.prototype = new F();
PrimaryStudent.prototype.constructor = PrimaryStudent;
PrimaryStudent.prototype.getGrade = function() {
return this.grade;
}
var xiaoming = new PrimaryStudent({
name: '小明',
grade: '2',
});
console.log(xiaoming.name); // 小明
console.log(xiaoming.grade) // 2
// 验证原型:
console.log(xiaoming.__proto__ === PrimaryStudent.prototype) // true
console.log(xiaoming.__proto__ .__proto__ === Student.prototype) // true
// 验证继承关系
console.log(xiaoming instanceof PrimaryStudent); // true
console.log(xiaoming instanceof Student); // true
ES5
的继承写法:
function Student(props) {
this.name = props.name || 'Unnamed';
}
Student.prototype.hello = function() {
console.log('Hello, ' + this.name + '!');
}
function PrimaryStudent(props) {
Student.call(this, props);
this.grade = props.grade || 1;
}
PrimaryStudent.prototype.getGrade = function() {
return this.grade;
}
// es5实现继承
PrimaryStudent.prototype = Object.create(Student.prototype);
PrimaryStudent.prototype.constructor = PrimaryStudent;
var xiaoming = new PrimaryStudent({
name: '小明',
grade: '2',
});
console.log(xiaoming.name); // 小明
console.log(xiaoming.grade) // 2
// 验证原型:
console.log(xiaoming.__proto__ === PrimaryStudent.prototype) // true
console.log(xiaoming.__proto__ .__proto__ === Student.prototype) // true
// 验证继承关系
console.log(xiaoming instanceof PrimaryStudent); // true
console.log(xiaoming instanceof Student); // true
ES6
的继承写法:
function Student(props) {
this.name = props.name || 'Unnamed';
}
Student.prototype.hello = function() {
console.log('Hello, ' + this.name + '!');
}
class PrimaryStudent extends Student {
constructor(props) {
super(props);
this.grade = props.grade || 1;
}
getGrade() {
return this.grade;
}
}
var xiaoming = new PrimaryStudent({
name: '小明',
grade: '2',
});
console.log(xiaoming.name); // 小明
console.log(xiaoming.grade) // 2
// 验证原型:
console.log(xiaoming.__proto__ === PrimaryStudent.prototype) // true
console.log(xiaoming.__proto__ .__proto__ === Student.prototype) // true
// 验证继承关系
console.log(xiaoming instanceof PrimaryStudent); // true
console.log(xiaoming instanceof Student); // true
extends 继承内建类的能力
因为语言内部设计原因,我们没有办法自定义一个类来继承 JavaScript
的内建类的。继承类往往会有各种问题。ES6
的extends
的最大的卖点,就是不仅可以继承自定义类,还可以继承 JavaScript 的内建类,比如这样:
class MyArray extends Array {
}
这种方式可以让开发者继承内建类的功能创造出符合自己想要的类。所有 Array 已有的属性和方法都会对继承类生效。这确实是个不错的诱惑,也是继承最大的吸引力。举个例子:
var a = new Array(1, 2, 3)
a.length // 3
var b = new MyArray(1, 2, 3)
b.length // 3
super 在 constructor 和普通方法里的不同
在 constructor
里面,super
的用法是super(..)
。它相当于一个函数,调用它等于调用父类的 constructor
。但在普通方法里面,super
的用法是super.prop
或者 super.method()
。它相当于一个指向对象的__proto__
的属性。这是ES6
标准的规定,
如果写子类的constructor
需要操作this
,那么 super
必须先调用!这是ES6
的规则。所以写子类的constructor
时尽量把super
写在第一行。
super
是编译时确定(可以理解为定义时确定),不是运行时确定
总结:
如何记住原型链,这里和html
事件冒泡差不多
<null>
<Object>//原型链顶端
<parent>
<child></child>
</parent>
</Object>
</null>
ES6
中继承:
class Parent {
constructor(firstName, lastName) {
console.log(firstName,lastName)
}
fullName() {
return `${this.firstName} ${this.lastName}`
}
}
class Child extends Parent { //extends:不仅可以继承自定义类,还可以继承 JavaScript 的内建类;
static findAll() { //static: 该方法不会被实例继承, 而是直接通过类来调用
return super.findAll()
}
constructor(firstName, lastName, age) {
super(firstName, lastName); // super: Parent.call(this, firstName, lastName)相当于函数执行Parent();
this.age = age
}
}
ES5
最经典的继承方法是用组合继承的方式,原型链继承方法,借用函数继承属性,ES6
也是基于这样的方式,但是封装了更优雅简洁的api
,让Javascript
越来越强大,修改了一些属性指向,规范了继承的操作,区分开了继承实现和实例构造,此外ES6
继承还能实现更多的继承需求和场景。