ES5创建对象
构造函数
作用:创建特定类型的对象
function Animal(species) { //构造函数借鉴其他OO语言,规定惯例,构造函数始终以大写字母开头命名
this.species = species;
this.saySpecies = function(){
console.log(this.species);
};
}
任何函数,只要通过new操作符来调用,那它就可以作为构造函数。那如果不使用new操作符来调用Person()呢?
Animal("兔");
//属性和和方法都被添加给Global对象了,如果此时是在浏览器中,就是window对象
window.saySpecies(); // "兔"
构造函数的主要问题:每个方法要在每个实例上重新创建一遍,他们不是同一个Function的实例,创建两个完成同样任务的Function实例没必要
alert(animal1.saySpecies == animal2.saySpecies); // false
//为什么说他们是Function的实例?下面这种写法方便理解,但不推荐
function Animal(species) {
this.species = species;
this.saySpecies = new Function("alert(this.species)");
}
解决这个问题的两个方案:全局函数(不推荐)和原型模式
function Animal(species) {
this.species = species;
this.saySpecies = saySpecies;
//第二个sayName函数名包含的是一个指向函数的指针,所以animal1和animal2中的this.saySpecies调用的是同一块内存地址里的saySpecies()函数
}
function saySpecies(){
console.log(this.species);
}
原型模式
构造函数、原型对象、实例对象三者间的关系: 无论在什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。所有的原型对象会自动获得一个constructor(构造函数)属性,这个属性包含一个指针,指向prototype属性所在函数(即构造函数)。
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针,指向构造函数的原型对象。ECMA-261第5版管这个指针叫[[prototype]],一些浏览器厂商支持一个叫__proto__的属性。注意:这个指针连接的是实例和原型对象,而不是实例和构造函数。
放上一张经典老图:
当为对象实例添加一个属性时,这个属性就有屏蔽原型对象中的同名属性。即添加这个属性只会组织我们访问原型中的那个属性,但不会修改那个属性。
function Animal() {
//构造函数被掏空
}
Animal.prototype.species = "某种动物";
Animal.prototype.areas = ["中国", "美国"]; Animal.prototype.saySpecies = function(){
console.log(this.species);
};
var animal1 = new Animal(); //省略了为构造函数传递初始化参数了,所以所有实例在默认情况下取得相同属性值
var animal2 = new Animal();
//animal1和animal2共享了Animal原型对象的属性和方法,但animal1想有自己的专属种类
animal1.species = "狗";
console.log(animal1.species);// “狗”——来自实例
console.log(animal2.species); // “某种动物”——来自原型
// 可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值,但如果是重写包含引用类型值的属性值呢?
animal1.areas.push("日本");
console.log(animal2.areas); // [ '中国', '美国', '日本' ]
//我本来只想改下animal1的地区的,但结果是吧animal2也改了
//所以通常组合使用函数模式和原型模式,构造函数模式用来定义实例属性,原型模式用来定义方法和共享的属性
ES5继承机制
js的设计者最初只是想设计一种简易的脚本语言,让浏览器可以和网页互动,所以不需要有”继承“机制,但需要有一种机制把所有的对象联系起来。所以最后还是设计了”继承“,但是没有引入类的概念。
使用原型链:
原理:让原型对象等于另一个类型的实例
function Animal(species){
this.species = species;
this.areas = ["中国", "美国"];
}
Animal.prototype.saySpecies = function(){
console.log(this.species);
};
//---------------------------------------
function Dog(name){
this.name = name;
}
Dog.prototype = new Animal("狗");// 此处把Animal实例赋给Dog.prototype
Dog.prototype.sayName = function(){
console.log(this.name);
};
//---------------------------------------
var myDog1 = new Dog("旺财");
myDog1.saySpecies; // 狗 成功继承了Animal类型的saySpecies方法
//---------------------------------------
var myDog2 = new Dog("旺旺");
myDog2.areas.push("日本");
console.log(myDog1.areas);//[ '中国', '美国', '日本' ]
//引用类型值的问题又来了,我改的是Dog2的地区,把Dog1的地区也改了
原型链的问题:
- 之前说过,如果原型属性包含了引用类型值,那么它会被所有的实例共享,所以后来现在我们把一些不想被共享的属性放在了构造函数里。在通过原型链来实现继承的时候,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章变成了现在的原型属性了。
- 创建子类型的实例时,不能向超类型的构造函数中传递参数
原型链示例图:要注意现在myDog.prototype.constructor指向Animal,另外别忘了Object,所有的引用类型默认都继承了Object
组合继承
使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承。
function Animal(species){
this.species = species;
this.areas = ["中国", "美国"];
}
Animal.prototype.saySpecies = function(){
console.log(this.species);
};
//---------------------------------------
function Dog(name, species){
this.name = name;
Animal.call(this, species); // 新增了这么一句,去掉了Dog.prototype = new Animal("狗");
}
// 借用构造函数继承:使用call或者apply方法,在子类型构造函数的内部调用超类型构造函数
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;
// 因为Dog.prototype = new Animal();这句话让Dog.prototype.constructor指向Animal了,更重要的是每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性
// 如果dog1.constructor也指向Animal,这就很奇怪了,明明dog1是用构造函数Dog生成的,所以我们要手动把它扳回来,让Dog.prototype对象的constructor值改为Dog
Dog.prototype.sayName = function(){
console.log(this.name);
};
//---------------------------------------
var myDog1 = new Dog("旺财", "狗");
var myDog2 = new Dog("旺旺");
myDog2.areas.push("日本");
console.log(myDog1.areas); //[ '中国', '美国']
// 这次没有影响到myDog1了
console.log(myDog1.saySpecies == myDog2.saySpecies) // true
这样既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性。
ES6创建类
引入了Class(类)这个概念,新的class写法让对象原型的写法更加清晰,将抽象思维具体化。更加像面向对象的语言。其实它的绝大部分功能ES5也可以做到。
// 创建Animal类
class Animal {
constructor(species){ // 构造函数没了,变成了constructor构造方法
this.species = species; // species是实例对象animal1自身的属性(因为定义在this变量上)
}
saySpecies(){ // 方法前面的function关键字没了
console.log(this.species);
}
}
// 实例化Animal类 let animal1 = new Animal("狗"); animal1.saySpecies(); // ”狗“
相似点
- 实例的属性除非显式定义在其本身(即定义在
this对象上),否则都是定义在原型上(即定义在class上)。在上面的代码中species是实例对象animal1自身的属性(因为定义在this变量上),而saySpecies方法是原型对象的方法(因为定义在Animal类上)。 - 类可以看成是构造函数的另一种写法,构造函数的
prototype属性,在 ES6 的“类”上面继续存在。而且类的数据类型就是函数(typeof Point // "function"),类本身就指向构造函数。事实上,类的所有方法都定义在类的prototype属性上面。在类的实例上调用方法其实就是调用原型上的方法,比如说下面的例子:animal1实例的constructor方法,其实就是Animal类原型的constuctor方法。console.log(animal1.constructor === Animal.prototype.constructor) // true - prototype对象的 constructor 属性,直接指向“类”的本身,这与 ES5 的行为是一致的。
console.log(Point.prototype.constructor === Point) // true
不同点:
- 类必须使用new调用,否则会报错,这和前面提到的es5的构造函数有所不同
- 类不存在变量提升,这一点也和ES5不同,如果类的使用在前,定义在后,就会报错;这么做的原因与类的继承有关,因为要保证子类在父类之后定义。
new Animal(); // ReferenceError class Animal {}
- this的指向: 类的方法内部如果含有 this ,它默认指向类的实例。注意以下情况:把方法提取出来单独用很危险
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger; //printName方法被单独拎出来了
printName(); // TypeError: Cannot read property 'print' of undefined
// 因为此时printName方法中的this指向方法运行时的环境,所以找不到print方法,就报错了
常用的解决办法:在构造方法里绑定this。无论printName方法被提取到哪里使用,它的this指向的永远是当前实例化对象。用这个更好: www.npmjs.com/package/aut…
constructor() {
this.printName = this.printName.bind(this);
}
ES6类的继承
class Animal {
constructor(species) {
this.species = species;
}
saySpecies() {
console.log(this.species);
}
} //定义一个Dog类继承Animal类
class Dog extends Animal {
constructor(species, name) {
super(species); // 调用父类的constructor(species),super用来新建父类的this对象,子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用 super方法,子类就得不到 this 对象。
this.name = name;
}
sayNameAndSpecies() {
console.log(this.name, this.species);
}
}
let dog1 = new Dog("狗","旺财");
dog1.sayNameAndSpecies();
从形式上来看:ES5先创造子类的实例对象 this ,然后再将父类的方法添加到 this 上面( Parent.apply(this) )。
ES6 先创造父类的实例对象 this (所以必须先调用 super 方法),然后再用子类的构造函数修改 this 。
类的prototype属性和__proto__属性:
在ES5中,每个实例对象都有__proto__属性,指向原型对象,构造函数的prototype属性也指向原型对象。而class同时具有prototype属性和 proto 属性.
ES6中有两条继承链:
- 子类的 proto 属性,表示构造函数的继承,总是指向父类。
- 子类 prototype 属性的 proto 属性,表示方法的继承,总是指向父类的 prototype 属性。
class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
可以这样理解:作为一个对象,子类( B )的原型( proto 属性)是父类( A );作为一个构造函数,子类( B )的原型对象 ( prototype 属性)是父类的原型对象( prototype 属性)的实例。
ES6类的继承实现原理
es6类的继承用babel转换后(重点是_inherits函数)
"use strict";
function _typeof(obj) {
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
_typeof = function _typeof(obj) {
return typeof obj;
};
} else {
_typeof = function _typeof(obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
}
return _typeof(obj);
}
//----------------------------------------------------------------------------
function _possibleConstructorReturn(self, call) { //生成并返回一个调用父类的构造函数的this,再在主函数中用子类的构造函数进行加工
if (call && (_typeof(call) === "object" || typeof call === "function")) {
// 如果父类返回的是对象或函数,则返回父类的构造函数生成的this,否则返回self
return call;
}
return _assertThisInitialized(self);
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
//-----------------------------------------------------------------------------------
function _getPrototypeOf(o) { //返回子类o的父类
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
//-------------------------------------------------------------------------------------
function _inherits(subClass, superClass) { // 用于实现继承的函数
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, { // 让子类的原型对象继承父类的原型对象
// 给子类添加 constructor属性
subclass.prototype.constructor === subclass
constructor: {
value: subClass,
writable: true,
configurable: true,
}
});
if (superClass) _setPrototypeOf(subClass, superClass); // 子类__proto__ 指向父类
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; };
return _setPrototypeOf(o, p);
}
//------------------------------------------------------------------------------------
function _instanceof(left, right) {
if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
return !!right[Symbol.hasInstance](left);
} else {
return left instanceof right;
}
}
function _classCallCheck(instance, Constructor) { // 防止以函数的方式调用class
if (!_instanceof(instance, Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
//------------------------------------------------------------------------------------
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false; //默认类的所有方法都是不允许枚举的
descriptor.configurable = true; //表示能够通过delete删除属性从而重新定义
if ("value" in descriptor) descriptor.writable = true; //默认方法可修改
Object.defineProperty(target, descriptor.key, descriptor); //在目标对象target上定义一个新属性
}
}
function _createClass(Constructor, protoProps, staticProps) {
// 利用_defineProperties给类添加方法
if (protoProps) _defineProperties(Constructor.prototype, protoProps); //把非静态方法添加到构造函数的原型对象上
if (staticProps) _defineProperties(Constructor, staticProps); //把静态方法添加到构造函数上
return Constructor;
}
var Animal =
/*#__PURE__*/
function () {
function Animal(species) {
_classCallCheck(this, Animal);
this.species = species; //在构造函数里绑定实例的属性
}
_createClass(Animal, [{
//传递两个参数,类、绑定在类的prototype上的方法,如果有静态方法的话也要传过去 key: "saySpecies",
value: function saySpecies() {
console.log(this.species);
}
}]);
return Animal;
}();
var Dog =
/*#__PURE__*/
function (_Animal) {
_inherits(Dog, _Animal); //继承父类
function Dog(species, name) {
var _this;
_classCallCheck(this, Dog);
_this = _possibleConstructorReturn(this, _getPrototypeOf(Dog).call(this, species));
//把super()那句变成了这个,_possibleConstructorReturn第一个参数是指向子类实例的this,另一个参数是调用父类的构造函数返回的父类实例 //_getPrototypeOf(Dog)函数
//_getPrototypeOf(Dog).call(this, species)对比es5借用构造函数继承这句Animal.call(this, species);
// 都是在子类型构造函数内部调用父类型构造函数,有哪里不同?
_this.name = name;
return _this;
}
_createClass(Dog, [{
key: "sayNameAndSpecies",
value: function sayNameAndSpecies() {
console.log(this.species, this.name);
}
}]);
return Dog;
}(Animal);
var dog1 = new Dog("狗", "旺财");
dog1.sayNameAndSpecies();
Object.create(proto[, propertiesObject])方法
Object.create方法用法:

用法:
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
什么情况用类,什么情况不用
参考链接:你可以不会 class,但是一定要学会 prototype
使用类可以带来的好处:使用类的话,在发生内存泄露时,查看memory里面的profiles,通过Class filter能够更加快速方便的找到它,可以很清晰的看到在哪里调用了它