一、对象
ECMA-262把对象定义为:”无序属性的集合,其属性可以包含基本值、对象或者函数“;
每个对象的属性或者方法都有一个名字,每个名字都映射到一个值(key: value);
创建一个对象最简单的办法就是创建一个Object实例
可以为对象添加属性和方法
var person = new Object()
person.name = 'zcy';
person.age = '22';
person.sayName = function() {
console.log(this.name);
}
上述例子用对象字面量语法可以写成
var person = {
name: 'zcy',
age: '22',
sayName: function () {
console.log(this.name); //this指向对象本身
},
};
person.sayName(); //zcy
ECMAScript中有两种属性,数据属性和访问器属性,如果要修改属性的默认特征,必须要使用Object.defineProperty()
方法
具体参考https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
二、构造函数
构造函数本身也是一个函数,与普通函数相同,不过为了规范将首字母大写,构造函数与普通函数的区别就是可以用new生成构造函数的实例,普通函数是直接调用无法创建实例,像原生构造函数Object()、Array()等,在运行时会自动出现在执行环境中。此外也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
};
this.job = job;
}
var person1 = new Person('zcy', '22', 'web');
person1.sayName();//构造函数的this指向被创建的对象
构造函数和普通函数的唯一区别就是调用它们的方式不同,构造函数通过new调用,普通函数直接调用,不过任何函数只要通过new操作符来调用,那么它就是构造函数,而任何函数不通过new调用,那它和普通函数也不会有什么不同。
例如上述例子直接调用Person()函数,函数里的对象和方法将挂到全局(window)上;
constructor
每一个被构造出来的对象都有一个constructor属性
constructor返回创建实例对象时构造函数的引用,就是说 p.constructor
返回它的构造函数的引用Person
;
function Person(age) {
this.age = age;
}
var p = new Person(50);
p.constructor === Person; // true
构造函数的缺点就是每个方法都要在每个实例上重新创建一遍。
var person1 = new Person();
var person2 = new Person();
person1和person2都有一个名为sayName()的方法,但是这两个方法不是同一个Function实例,他们是不相等的;
可以把sayName()理解为:
this.sayName = new Function(console.log(this.name))
所以每个Person实例都会有不同的Function实例,如果一直以这种方式创建,会导致不同的作用域链和标识符解析;
如果一直创建对象就会一直创建Function实例,浪费内存,好在这些问题可以通过使用原型来解决;
三、原型
我们创建的每个函数都有一个prototype
属性,也是就原型属性,无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性是一个指针,指向一个该函数的原型对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
按照字面意思来理解,prototype就是通过调用构造函数而创建的那个对象实例的原型对象
在默认情况下,所有的原型对象都会自动获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针;
Person.prototype.constructor === Person //tue
挂在原型上的方法和属性,实例出来的对象访问的都是同一组属性和同一个方法
所以这个就是比单纯使用构造函数更好的地方
构造函数的Person的prototype属性指向该构造函数原型对象,该原型对象的constructor指向构造函数本身
[[Prototype]]和 __proto__
当调用构造函数创建一个新实例后,该实例的内部包含一个指针(内部属性),指向构造函数的原型对象,这个指针叫[[Prototype]]
[[Prototype]]和__proto__有什么关系呢?
其实[[prototype]]和__proto__意义相同,均表示对象的内部属性,其值指向对象原型。前者在一些书籍、规范中表示一个对象的原型属性,后者则是在浏览器实现中指向对象原型。
需要注意的一点是,这个连接存在于构造出来的实例person1
与构造函数的原型对象Person.prototype
中,而不存在实例与构造函数中;
所以person1.__proto__指向构造person1的构造函数的原型
构造函数的原型对象prototype上也有__proto__属性,其指向构造Person的构造函数的原型对象
也就是Object的原型对象
原型对象上有isPrototypeOf
和hasOwnProperty
方法,他们的作用分别是
Person.prototype.isPrototypeOf(person1)
true
判断对象person1的内部指针__proto__是不是指向Person
person1.play = 'happy';
person1.hasOwnProperty("play");
true
person1.hasOwnProperty("sayName");
false
判断属性是否是对象自身的属性,如果是自身属性则返回true,是原型上的则返回false
ECMAScript还提供了一个方法getPrototypeOf
,用来判断对象__proto__指向
Object.getPrototypeOf(person1) == Person.prototype
true
person1的__proto__指向构造函数的原型对象
当我们去访问一个对象的属性时,会执行一次搜索,如果在该对象找到了这个属性,则返回,如果没找到,则顺着该对象的__proto__向构造该对象的原型对象上查找,找到则返回,没找到继续沿着__proto__向上查找,直到查找到Object.__proto__都还没找到,原型链的顶端是null,没找到就返回undefined,这就是原型链
比如:当调用person1的sayName()方法时,问:“person1有sayName属性吗”,回答没有,然后继续向上搜索,通过查找person1.__proto__指向person1构造函数的prototype对象,该原型对象上有sayName属性,则返回该属性。
最后梳理一下各种名词的解释
//Person是一个构造函数
function Person() {}
//构造函数的prototype属性指向该构造函数的原型对象
Person.prototype.sayName = function () {
console.log('hello');
};
var person1 = new Person();
//实例化出来的对象的constructor指向构造函数
person1.constructor == Person; //true
//构造函数的原型对象上有constructor属性,指向构造函数本身
Person.prototype.constructor == Person; //true
//每个实例化出来的对象有__proto__属性,指向构造该对象的构造函数的原型对象,通过该属性实现原型链
person1.__proto__ == Person.prototype; //true
//person1.__proto__.__proto__就是构造函数原型对象的__proto__,其指向构造该构造函数的原型对象,也就是Object.prototype
person1.__proto__.__proto__ == Person.prototype.__proto__; //true
Person.prototype.__proto__ == Object.prototype; // true
//最终Object.prototype.__proto__指向null,所以说Object.prototype就是原型链的顶端,不存在构造Object的构造函数的原型对象
Object.prototype.__proto__ == null; // true
那么使用原型的方式构造函数就没有缺点了吗,不是的,当我们在原型对象上定义包含引用类型的值的时候,就会出现问题
function Person() {}
Person.prototype.friends = ['zcy', 'zcy2'];
var person1 = new Person();
var person2 = new Person();
person1.friends.push('zcy3');
console.log(person2.friends);//['zcy', 'zcy2', 'zcy3'];
person1.friends == person2.friends; //true
当我们在原型上定义一个数组,并且实例化两个对象时,这个两个对象的friends属性指向同一个数组的引用,当修改其中数组的值会影响到另外一个;
为了解决这个问题,我们可以组合使用构造函数模式和原型模式
function Person() {
this.friends = ['zcy1', 'zcy2'];
}
Person.prototype.name = 'zcy';
var person1 = new Person();
var person2 = new Person();
person1.friends.push('zcy3');
console.log(person2.friends); //['zcy', 'zcy2']
console.log(person1.friends == person2.friends); //false
四、继承
1. 原型链继承
使用原型链实现继承的基本思想是让一个引用类型继承另一个引用类型的属性和方法;
要实现原型链继承就是让一个构造函数的原型属性指向另一个构造函数的实例对象
Children.prototype = new Parent();
原型链继承就是子构造函数Children()创建出来的实例对象也会继承父构造函数Parent()的属性和方法,当我们在通过子构造函数Children()构造出来的对象children上访问一个属性时,当children没有该属性,本应该去访问其构造函数Children()的原型对象,但是此时Children原型对象指向父Parent的实例,所以先去Parent实例(parent)上查找,如果parent上也没有,会通过parent.__proto__
去父构造函数的原型对象Parent.prototype上查找,所以这就通过原型链实现了继承;
注意:children.constructor == Parent;//true
,这是因为子构造函数的原型属性指向父构造函数的实例,而这个实例的constructor就等于Parent;
//构造函数a
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperTypeValue = function () {
return this.property;
};
//构造函数b
function SubType() {
this.subproperty = false;
}
//让构造函数b的原型属性指向构造函数a的实例
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subpropertys;
};
//通过构造函数b构造出来的对象上有构造函数a的方法,这就实现了继承
var instance = new SubType();
instance.getSuperTypeValue(); //true
instance.__proto__ == SubType.prototype; //true
SubType.prototype.__proto__ == SuperType.prototype; //true
instance.constructor == SuperType; //true
注意:给原型添加方法的代码一定要放在替换原型语句之后
SubType.prototype = new SuperType();
SubType.prototype.aaa = 'aaa';
不能使用对象字面量创建原型方法,这样做就会重写原型链
SubType.prototype = new SuperType();
SubType.prototype = {
getSubValue: function () {},
};
原型链继承存在的问题:
- 使用原型链式的继承无法实现多继承,一个子构造函数没办法同时继承两个父构造函数(一般在js开发中多继承用的不多)
- 使用原型链式的继承和使用原型模式给构造函数添加属性相同,当我们在原型链上添加一个带引用类型的属性时,比如在父类上有个数组,通过父类继承了一个子类,子类实例化的对象给数组添加元素,再用子类实例化一个对象2,对象2的数组会有刚刚新添加那条数据;
- 还有就是原型链继承没办法向父构造函数中传递参数
2. 借用构造函数继承
借用构造函数继承的思想式,在子类型构造函数的内部调用父类型的构造函数,并通过apply()或者call()改变this的指向,将子类的this指向父类构造函数的函数体内,这样就能实现向父类构造函数传递参数并且在子类中执行了父类的代码之后在子类的每个实例都会具有自己的属性副本;
function Father(name) {
this.name = name;
this.color = ['1', '2', '3'];
}
function Children() {
//继承了父类属性,并且还传递了参数
Father.call(this, 'zcy');
//实例化自身属性
this.age = 22;
}
var children1 = new Children();
children1.color.push('4');
console.log(children1.name); //zcy
console.log(children1.age); //22
var children2 = new Children();
console.log(children2.color); // ['1', '2', '3']
借用构造函数继承可以实现多继承,并且解决了共享的问题,每次在子类中调用Father在内存中分配的地址都不同;
存在的问题:
- 子类只能继承父类构造函数内的方法和属性,无法继承父类原型对象上的属性和方法;
- 和构造函数模式问题相同,属性和方法没办法复用,每次都会构造出新的对象,浪费内存
3.组合式继承
组合式继承有时候也叫伪经典继承,指的是将原型链继承和借用构造函数继承的思想整合到一起,使用原型链继承实现对属性和方法的继承,通过借用构造函数模式来实现对实例属性的继承,这样既可以通过在原型上定义方法实现了函数的复用,又能保证每个实例都有它自己的属性,指向不同的内存空间,互不影响
function Father(name) {
this.name = name;
this.color = ['1', '2', '3'];
}
Father.prototype.sayName = function () {
return this.name;
};
function Children(name, age) {
//继承了父类属性,并且还传递了参数
Father.call(this, name);
//实例化自身属性
this.age = age;
}
//将子类的原型属性指向父类的实例
Children.prototype = new Father();
//并且让子类原型上的constructor指向子类自身
Children.prototype.constructor = Children;
Children.prototype.sayAge = function () {
return this.age;
};
var children1 = new Children('zcy', '22');
children1.color.push('4');
console.log(children1.color); //['1', '2', '3', '4']
console.log(children1.sayAge()); //22
console.log(children1.sayName()); //zcy
var children2 = new Children('vvv', '23');
console.log(children2.color); //['1', '2', '3'] 实例不受影响
console.log(children2.sayAge()); //23
console.log(children2.sayName()); //vvv
可以实现多继承,可以实现传参
实现继承时为何总是要修正constructor的指向呢?
constructor其实没有什么用处,只是JavaScript语言设计的历史遗留物。由于constructor属性是可以变更的,所以未必真的指向对象的构造函数,只是一个提示。不过,从编程习惯上,我们应该尽量让对象的constructor指向其构造函数,以维持这个惯例。
这个方式的缺点:
- 父类的构造函数在这种继承下被调用了两次,第一次是在子类的构造函数当中,使用call或者apply的时候,第二次调用在设置原型的时候,子.prototype = new 父();假如父类的构造函数很消耗性能,那么整个继承的性能就行下降
- 子类构造出来的实例,它的属性在实例中和原型中各存在一份,占用多余内存
4.原型继承
原型继承的思想是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
在object()函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时构造函数的实例,从本质上讲,object()函数对传入的对象执行了一次浅复制;
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
var person = {
name: 'zcy',
friends: ['1', '2', '3'],
};
//在通过object()函数返回的F()构造函数的实例中,它的__proto__属性指向传入的对象
var children1 = object(person);
children1.__prpto__ == person; //true
children1.name = 'Grge';
children1.friends.push('4');
var children2 = object(person);
children2.__prpto__ == person; //true
children2.friends.push('5');
console.log(children2.name); //zcy 继承自person
console.log(person.friends); // ['1', '2', '3', '4', '5'] 共享属性
所以就实现了新对象继承person对象,不过包含引用类型的属性始终都会共享相应的值,就像使用原型模式一样
ES5提供的Object.create()方法规范化了原型继承模式,该方法接受两个参数
- 一个用作新对象原型的对象
- 为新对象定义的格外属性的对象
var person = {
name: 'zcy',
friends: ['1', '2', '3'],
};
var children1 = Object.create(person, {
age: {
value: '22',
},
});
children1.__proto__ == person; //true
console.log(children1.age); //22
console.log(children1.friends); //['1', '2', '3']
5、class继承
ES6中的Class可以通过extends实现继承,这比ES5中通过修改原型链实现继承要清晰和方便很多;
class Parent {
//constructor方法默认返回实例对象,即this指向被实例化的对象
constructor(name, age) {
this.name = name;
this.age = age;
this.friends = ['1', '2', '3'];
}
sayName() {
return this.name + this.age;
}
}
class Children extends Parent {
//子类的constructor里必须调用super方法,否则会报错,this关键字只能在super方法之后使用
constructor(name, age, city) {
//super方法的作用就是让子类继承父类的this对象,然后对其进行加工,子类本身没有this
super(name, age);
this.city = city;
}
sayName() {
//继承父类的方法通过super.xxx
return super.sayName() + this.city;
}
}
var children1 = new Children('zcy', 22, 'sz');
children1.sayName(); //zcy22sz
children1.friends.push('4');
var parent1 = new Parent();
console.log(parent1.friends); //['1', '2', '3']
通过子类构建出来的对象既是子类的实例也是父类的实例;
大多数浏览器的ES5实现之中,每一个对象都有proto属性,指向对应的构造函数的prototype属性。Class作为构造函数的语法糖,同时有prototype属性和proto属性,因此同时存在两条继承链。
- 子类的
__proto__
属性,表示构造函数的继承,总是指向父类。 - 子类prototype属性的
__proto__
属性,表示方法的继承,总是指向父类的prototype属性
Children.__proto__ == Parent
Children.prototype.__proto__ === Parent.prototype
所以就可以实现上述组合式继承中子类既可以实现继承父类的方法属性,又可以继承原型上的方法和属性
也就是既可以通过在原型上定义方法实现了函数的复用,又能保证每个实例都有它自己的属性,指向不同的内存空间,互不影响。