一、函数与对象,原型
首先要理解函数与对象的关系
函数是对象的一种是没有错的,数组也是对象的一种
对象都是通过函数创建的。
//函数都有prototype属性指向原型对象,prototype都有一个constructor属性指向函数
func.prototype.constructor === func;//函数原型的构造函数是函数本身
//实例不指向构造函数,而是指向构造函数的原型对象
var fObj = new f();
fObj.__proto__.constructor === f;
判断是对象实例的属性还是原型的属性
function hasPrototypeProperty(obj, name){
return name in obj && !obj.hasOwnProperty(name);
}
//Animal构造函数在下面
Animal.prototype.c=3;
var p = new Animal('小明')
console.log(p.hasOwnProperty("name")); //true
console.log(hasPrototypeProperty(p, 'c')); //true
JS实现继承的几种方式
JS作为面向对象的弱类型语言,继承也是其非常强大的特性之一。那么如何在JS中实现继承呢?让我们拭目以待~
既然要实现继承,那么首先我们得有一个父类,代码如下:
// 定义一个动物类
function Animal (name) {
// 属性
this.name = name || 'Animal';
this.eat= ['肉']; // 引用类型属性
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
// 原型方法和属性
Animal.prototype.eating = function(food) {
console.log(this.name + '正在吃:' + food);
};
Animal.prototype.playing = '爱玩篮球'
一、对象冒充(很少用)
也叫借用构造函数(伪造对象)
原理:通过一个临时的属性,将父对象的属性和方法继承给子对象。
注意:只继承对象本身的属性和方法,不继承它的原型的属性和方法。
- 普通的对象冒充
function Person(username, password) {
//实现将Animal的属性和方法追加到Person中,从而实现继承
//第一步:this.method是作为一个临时的属性,并且指向Animal所指向的对象,
//第二步:执行this.method方法,即执行Animal所指向的对象函数
//第三步:销毁this.method属性,即此时Person就已经拥有了Animal的所有属性和方法
this.method = Animal;
this.method(username);
delete this.method;
/*新增属性和方法*/
this.password = password;
this.world = function() {
console.log(this.password);
}
};
var p1 = new Animal('二货');
var p2 = new Person('二货','yyyyyy');
console.log(p2)
console.log(p1.eating('香蕉')); //二货正在吃:香蕉
console.log(p2.eating('香蕉')); //报错:p2.eating is not a function
优点:父类有引用类型对象的属性时,继承后实例间也不会相互影响
缺点:
- 只能继承构造函数内部的属性和方法,无法继承父类原型的属性和方法
- call/apply方式
在子类型构造函数中执行父类构造函数,并将父类构造函数的this指向子类的new出来的对象上
在特定的作用域中调用函数,等于设置函数体内this对象的值,以扩充函数赖以运行的作用域。
- apply()方法 接收两个参数,一个是函数运行的作用域(this),另一个是参数数组。
语法:apply([thisObj [,argArray] ]);,调用一个对象的一个方法,2另一个对象替换当前对象。 - call()方法 第一个参数和apply()方法的一样,但是传递给函数的参数必须列举出来。
语法:call([thisObject[,arg1 [,arg2 [,…,argn]]]]);,应用某一对象的一个方法,用另一个对象替换当前对象。 - 如果call和apply的第一个参数写的是null,那么this指向的是window对象
call方法
- call方法是Function类中的方法
- call方法的第一个参数的值赋值给类(即方法)中出现的this
- call方法的第二个参数开始依次赋值给类(即方法)所接受的参数
function Person(username, password) {
Animal.call(this, username);
/*新增属性和方法*/
this.password = password;
this.world = function() {
console.log(this.password);
}
};
var p2 = new Person('二货','yyyyyy');
console.log(p2);
apply方法
function Person(username, password) {
Animal.apply(this, new Arrar(username));
//或者:Animal.apply(this, [username]);
/*新增属性和方法*/
this.password = password;
this.world = function() {
console.log(this.password);
}
};
var p2 = new Person('二货','yyyyyy');
console.log(p2);
关于call/apply,可以看看这篇文章:https://www.jianshu.com/p/00dc4ad9b83f
二、原型链方式(很少用)
原理:让子类构造函数的原型指向父类型构造函数的一个实例
让新的构造函数的原型 继承父类构造函数创建的实例的属性和方法,供新的构造函数创建的实例使用。
这样的话,新的构造函数创建的实例对象,既继承了原构造函数创建的实例的属性和方法,也继承了其原型的属性和方法!
写一个例子!
//instanceof 在继承中判断实例是否属于它的父类
//这里,Student和Person都在s的原型链中
function Person(){};
function Student(){};
var p =new Person();
Student.prototype=p;//继承原型
var s=new Student();
console.log(s instanceof Student);//true
console.log(s instanceof Person);//true
缺点:
- 子类原型的构造函数成了父类构造函数
- 当父类有引用类型属性的时候,子类原型继承后,所有的实例共用同一个属性,一个实例修改了属性,所有实例的属性均受影响。(父构造函数中的属性都会成为共享属性,当父构造函数中有引用类型属性时,继承后实例间会相互影响)
function Cat() {}
Cat.prototype = new Animal();
var cat1 = new Cat();
var cat2 = new Cat();
cat1.eat.push('蔬菜');
console.log(cat1.eat); // ['肉','蔬菜']
console.log(cat2.eat);// ['肉','蔬菜'] 受到实例cat1的影响
三、经典继承(规范化的原生式继承)(少用!)
Object.create()方法接受两个参数:Object.create(obj,propertiesObject) ;
- obj:一个对象,应该是新创建的对象的原型。值只能是对象或者null,否则会报错!
- propertiesObject:可选。该参数对象是一组属性与值,该对象的属性名称将是新创建的对象的属性名称,值是属性描述符。
Object.create(null) 创建的对象是一个空对象,在该对象上没有继承 Object.prototype 原型链上的属性或者方法,例如:toString(), hasOwnProperty()等方法;与{},new Object()不同,创建空对象的三种方式如下(它们三者的原型__proto__是相等的):
- Object.create(Object.prototype)
- { }
- new Object()
例子
使用Object.create()是将对象继承到prototype原型上,本质是置换原型
var a = { person : { name: 'kelen' } };
var b = Object.create(a);
console.log(b);
console.log(a.hasOwnProperty('person'))
console.log(b.hasOwnProperty('person'))
b.person.name = 'kobe';
console.log( a.person.name ); // kobe
Object.create的兼容写法:
if(!Object.create){
Object.create = function(parent){
function F(){};
F.prototype = parent;
return new F;
}
}
优点:不需要兴师动众使用构造函数
缺点:对象中有引用类型值属性时,实例间会相互影响
四、混合方式 – 推荐使用!!!
混合了对象冒充、原型继承方式
这种方式解决了原型链继承和对象冒充继承两种方式带来的问题,融合了两种方式的优点,目前最常用的方式!
存在一个小问题,此时子类的constructor不再是自身构造函数了
function Cat(name) {
Animal.call(this, name); //新的构造函数创建的实例中可以传任意参数
}
Cat.prototype = new Animal();
var cat1 = new Cat('cat1');
var cat2 = new Cat('cat2');
cat1.eating('苹果');//cat1正在吃:苹果 -- 继承了父类原型的属性和方法
console.log(cat1.playing);//爱玩篮球 -- 继承了父类原型的属性和方法
cat1.eat.push('蔬菜');
console.log(cat1.eat);//["肉", "蔬菜"]
console.log(cat2.eat);//["肉"] -- 实例之间的属性不会相互影响
五、寄生组合式继承(完美!)
在组合继承的前提下,子类原型指向父类原型的一个副本而不是父类的实例
这种继承方式解决了组合方式的子类原型构造函数指向问题
// 定义猫咪构造函数
function Cat(name) {
this.name = name;
// 继承Animal 将Animal绑定到this对象上并执行!!!
Animal.call(this,'red');
}
function inheritPrototype(son, father) {
var prototype = Object(father.prototype); // 将父类的原型创建一个副本
prototype.constructor = son; //给副本指定constructor为子类构造函数
son.prototype = prototype; //将子类原型指向这个副本
}
inheritPrototype(Cat,Animal);
// 实例化Cat
var c1 = new Cat('咪咪');
var c2 = new Cat('泡泡');
c1.eat.push('猫薄荷');
c1.eating(); //我能吃肉,猫薄荷
console.log(c1.constructor); // cat函数体,
console.log(c1.eat); // 肉,猫薄荷
console.log(c2.eat); // 肉 不受实例c1的影响
六、混入式继承
实质是遍历对象属性,将原对象上的所有成员复制一份,添加到当前对象上
o1.extend = function(){
//arguments是实参
var i=0,
k,
len = arguments.length,
args = arguments;
for(;i<args.length;i++){
for(k in args[i]){
this[k]=args[i][k];
}
}
}
o1.extend(o2,o3......);