十:以理论结合实践方式梳理前端 ES 6+ ——— ES 6+ 原型对象

ES 6+ 原型对象

每个 实例对象 都有一个 constructor 属性,指向它的构造函数

function Person(name) {
	this.name = name;
}
var person = new Person('Steve'); // 自定义构造函数
var string = new String('Hello'); // 原生的构造函数

console.log(person.constructor === Person);			// 执行结果:true
console.log(string.constructor === String);			// 执行结果:true

每个 函数对象(包括构造函数) 都有一个 prototype 属性,指向函数的原型对象

这个 原型对象constructor 属性,指向函数本身

function Person(name) {
	this.name = name;
}
var person = new Person('Steve'); // 自定义构造函数
var string = new String('Hello'); // 原生的构造函数

console.log(Person.prototype.constructor === Person);			// 执行结果:true
console.log(String.prototype.constructor === String);			// 执行结果:true

每个 对象 都有一个 [[prototype]] 私有属性,指向它的构造函数的原型对象,但这个属性是不允许访问的

某些浏览器(例如 Chrome)提供 __proto__ 属性用于访问 [[prototype]] 私有属性

function Person(name) {
	this.name = name;
}
var person = new Person('Steve'); // 自定义构造函数
var string = new String('Hello'); // 原生的构造函数

console.log(person.__proto__ === Person.prototype);				// 执行结果:true
console.log(string.__proto__ === String.prototype);				// 执行结果:true
console.log(Person.__proto__ === Function.prototype);			// 执行结果:true
console.log(String.__proto__ === Function.prototype);			// 执行结果:true

构造函数的 constructor 属性都是指向 Function__proto__ 属性都是指向 Function.prototype

因为构造函数都是通过 new Function 来创建的,它们都是 Function 的实例对象,包括 Function 和 Object

function Person(name) {
	this.name = name;
}

console.log(Person.constructor === Function);					// 执行结果:true
console.log(Person.__proto__ === Function.prototype);			// 执行结果:true
console.log(String.constructor === Function);					// 执行结果:true
console.log(String.__proto__ === Function.prototype);			// 执行结果:true

console.log(Function.constructor === Function);					// 执行结果:true
console.log(Function.__proto__ === Function.prototype);			// 执行结果:true
console.log(Object.constructor === Function);					// 执行结果:true
console.log(Object.__proto__ === Function.prototype);			// 执行结果:true

Object 外,其它构造函数的 prototype 属性的 __proto__ 属性都是指向 Object.prototype

Objectprototype 属性的 __proto__ 属性指向 null

function Person(name) {
	this.name = name;
}

console.log(Person.prototype.__proto__ === Object.prototype);			// 执行结果:true
console.log(String.prototype.__proto__ === Object.prototype);			// 执行结果:true

console.log(Function.prototype.__proto__ === Object.prototype);			// 执行结果:true
console.log(Object.prototype.__proto__ === null);						// 执行结果:true

最后来一张图片总结一下,虽然看起来有点复杂,但是只要大家找到规律,就会很简单了

建议大家分类来看,注意哪里出现 constructor 属性,哪里出现 prototype 属性,哪里出现 __proto__ 属性

34
直白的说,原型对象是系统对每种对象进行原始的配置设置,在学习内置对象的时候,有没有发现,每种对象都具备自己独有的一些属性和方法

原型链设计

(1)原型链

上面我们说过,所有对象都有 __proto__ 属性,并且这个 __proto__ 属性指向一个原型对象

因为原型对象也是对象,这个原型对象也有 __proto__ 属性,我们把这种关系称为原型链

34

(2)属性访问

当需要访问一个对象的属性时,首先从该对象开始查找,如果能够找到,那么到此返回

如果没有找到,就在该对象的 __proto__ 属性指向的原型对象中继续查找,如果能够找到,那么到此返回

如果没有找到,那么一直往上查找原型对象,直至 __proto__ 属性指向 null,也就是原型链的顶端

若原型链上的所有原型对象都没有该属性,则返回 undefined

function Person(name) { this.name = name; }
Person.prototype.getName = function() { return this.name; }
var person = new Person('Steve');

var name = person.getName(); // 在 Person.prototype 中找到
var description = person.toString(); // 在 Object.prototype 中找到
var age = person.age; // 在原型链中无法找到

console.log(name);							// 执行结果:Steven
console.log(description);					// 执行结果:[object Object]
console.log(age);							// 执行结果:undefined

(3)属性检测

  • in 操作符:检测属性是否在对象的原型链上
  • hasOwnProperty 方法:检测属性是否来自对象本身
function Person(name) { this.name = name; }
Person.prototype.getName = function() { return this.name; }
var person = new Person('Steve');

console.log('age' in person);										// 执行结果:false
console.log('name' in person);										// 执行结果:true
console.log('getName' in person);									// 执行结果:true
console.log('toString' in person);									// 执行结果:true

console.log(person.hasOwnProperty('age'));							// 执行结果:false
console.log(person.hasOwnProperty('name'));							// 执行结果:true
console.log(person.hasOwnProperty('getName'));						// 执行结果:false
console.log(person.hasOwnProperty('toString'));						// 执行结果:false

原型对象的作用

JavaScript 中的继承是基于原型的,原型的作用在于不同实例对象之间可以共享数据,节省内存

function Person(name) { this.name = name; }
Person.prototype.getName = function() { return this.name; }
var person1 = new Person('Steve');
var person2 = new Person('Steven');

// 不同实例对象 person1 和 person2 都可以使用在 Person.prototype 上定义的 getName(共享数据)
// 从而避免在每个实例对象上都要重复定义 getName(节约内存)
var name1 = person1.getName();
var name2 = person2.getName();
console.log(name1);
console.log(name2);

原型继承

方法:将子类的原型指向父类的实例

原理:子类在访问属性或调用方法时,往上查找原型链,能够找到父类的属性和方法

  • 缺点:在调用子类构造函数时,无法向父类构造函数传递参数
  • 优点:子类实例可以访问父类的实例方法和原型方法
  • 缺点:所有子类实例共享父类的引用属性
function SuperType(name, info) {
    // 实例属性(基本类型)
    this.name = name || 'Super';
    // 实例属性(引用类型)
    this.info = info || ['Super'];
    // 实例方法
    this.getName = function() { return this.name; };
}
// 原型方法
SuperType.prototype.getInfo = function() { return this.info; };

// 原型继承
function ChildType(message) { this.message = message; }
ChildType.prototype = new SuperType('Child', ['Child']);

// 在调用子类构造函数时,无法向父类构造函数传递参数
var child = new ChildType('Hello');

// 子类实例可以访问父类的实例方法和原型方法
console.log(child.getName()); // Child
console.log(child.getInfo()); // ["Child"]

// 所有子类实例共享父类的引用属性
var other = new ChildType('Hi');
other.info.push('Temp');
console.log(other.info); // ["Child", "Temp"]
console.log(child.info); // ["Child", "Temp"]

构造继承

方法:在子类的构造函数调用父类的构造函数,并将 this 指向子类实例

原理:在构造子类时,调用父类的构造函数初始化子类的属性和方法

  • 优点:在调用子类构造函数时,可以向父类构造函数传递参数
  • 缺点:子类实例可以访问父类的实例方法,但是不能访问父类的原型方法,因此无法做到函数复用
  • 优点:每个子类实例的属性独立存在
function SuperType(name, info) {
    // 实例属性(基本类型)
    this.name = name || 'Super';
    // 实例属性(引用类型)
    this.info = info || ['Super'];
    // 实例方法
    this.getName = function() { return this.name; };
}
// 原型方法
SuperType.prototype.getInfo = function() { return this.info; };

// 构造继承
function ChildType(name, info, message) {
    SuperType.call(this, name, info);
    this.message = message;
}

// 在调用子类构造函数时,可以向父类构造函数传递参数
var child = new ChildType('Child', ['Child'], 'Hello');

// 子类实例可以访问父类的实例方法,但是不能访问父类的原型方法
console.log(child.getName()); // Child
console.log(child.getInfo()); // Uncaught TypeError

// 每个子类实例的属性独立存在
var other = new ChildType('Child', ['Child'], 'Hi');
other.info.push('Temp');
console.log(other.info); // ["Child", "Temp"]
console.log(child.info); // ["Child"]

组合继承

方法:同时使用原型继承和构造继承,综合两者的优势所在

原理:通过原型继承实现原型属性和原型方法的继承,通过构造继承实现实例属性和实例方法的继承

  • 优点:在调用子类构造函数时,可以向父类构造函数传递参数
  • 优点:子类实例可以访问父类的实例方法和原型方法
  • 优点:每个子类实例的属性独立存在
  • 缺点:在实现组合继承时,需要调用两次父类构造函数
function SuperType(name, info) {
    // 实例属性(基本类型)
    this.name = name || 'Super';
    // 实例属性(引用类型)
    this.info = info || ['Super'];
    // 实例方法
    this.getName = function() { return this.name; };
}
// 原型方法
SuperType.prototype.getInfo = function() { return this.info; };

// 组合继承
function ChildType(name, info, message) {
    SuperType.call(this, name, info);
    this.message = message;
}
ChildType.prototype = new SuperType();
ChildType.prototype.constructor = ChildType;

// 在调用子类构造函数时,可以向父类构造函数传递参数
var child = new ChildType('Child', ['Child'], 'Hello');

// 子类实例可以访问父类的实例方法和原型方法
console.log(child.getName()); // Child
console.log(child.getInfo()); // ["Child"]

// 每个子类实例的属性独立存在
var other = new ChildType('Child', ['Child'], 'Hi');
other.info.push('Temp');
console.log(other.info); // ["Child", "Temp"]
console.log(child.info); // ["Child"]

原型式继承

方法:实现一个函数,传入已有对象,在函数内部将新对象的原型指向原有对象,最后返回新对象

原理:返回的新对象继承原有对象,然后根据需求对得到的对象加以修改即可

  • 要求:创建子类实例必须基于一个已有对象
  • 缺点:所有新创建的实例都会重新定义已有对象的实例方法,因此无法做到函数复用
  • 缺点:所有新创建的实例共享已有对象的引用属性
var superObject = {
    name: 'Super',
    info: ['Super'],
    getName: function() { return this.name; }
};

// 原型式继承
function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

// 创建子类实例必须基于一个已有对象
var childObject = object(superObject);

// 根据需求对得到的对象加以修改
childObject.message = 'Hello';

// 新创建的实例可以访问已有对象的实例属性和实例方法
console.log(childObject.name); // Super
console.log(childObject.getName()); // Super

// 所有新创建的实例共享已有对象的引用属性
var otherObject = object(superObject);
otherObject.info.push('Temp');
console.log(otherObject.info); // ["Child", "Temp"]
console.log(childObject.info); // ["Child", "Temp"]

寄生式继承

方法:创建一个用于封装继承过程的函数,在函数内部以某种方式增强对象,且最后返回对象

原理:基于原型式继承,类似于工厂模式,将增强对象的过程封装到一个函数中

  • 要求:创建子类实例必须基于一个已有对象
  • 缺点:所有新创建的实例都会重新定义已有对象的实例方法,因此无法做到函数复用
  • 缺点:所有新创建的实例共享已有对象的引用属性
var superObject = {
    name: 'Super',
    info: ['Super'],
    getName: function() { return this.name; }
};

// 寄生式继承
function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}
function objectFactory(o) {
    var clone = object(o);   // 创建对象
    clone.message = 'Hello'; // 增强对象
    return clone;           // 返回对象
}

// 创建子类实例必须基于一个已有对象
var childObject = objectFactory(superObject);

// 新创建的实例可以访问已有对象的实例属性和实例方法
console.log(childObject.name); // Super
console.log(childObject.getName()); // Super

// 所有新创建的实例共享已有对象的引用属性
var otherObject = object(superObject);
otherObject.info.push('Temp');
console.log(otherObject.info); // ["Child", "Temp"]
console.log(childObject.info); // ["Child", "Temp"]

寄生式组合继承

方法:借用寄生式继承的思路,结合组合继承的方法,解决组合继承中需要调用两次父类构造函数的问题

原理:通过构造继承实现实例属性和实例方法的继承,通过寄生式继承实现原型属性和原型方法的继承

不用为了指定子类的原型而调用父类的构造函数,而是使用寄生式继承来继承父类的原型,然后指定给子类的原型

function SuperType(name, info) {
    // 实例属性(基本类型)
    this.name = name || 'Super';
    // 实例属性(引用类型)
    this.info = info || ['Super'];
    // 实例方法
    this.getName = function() { return this.name; };
}
// 原型方法
SuperType.prototype.getInfo = function() { return this.info; };

// 寄生式组合继承
function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}
function objectFactory(childType, superType) {
    var prototype = object(superType.prototype); // 创建对象
    prototype.constructor = childType;           // 增强对象
    childType.prototype = prototype;             // 将父类原型指定给子类原型
}
function ChildType(name, info, message) {
    SuperType.call(this, name, info);
    this.message = message;
}
objectFactory(ChildType, SuperType);

寄生式组合继承是 JavaScript 中最常用的继承方式,ES6 中新增的 extends 底层也是基于寄生式组合继承的

原型事件

call、apply、bind 都是定义在函数原型上的,也就是说每个函数都能调用这些方法

那么它们都有什么作用呢?它们之间存在什么异同呢?下面让我们一起来探讨一下

call 事件

call 可以用于改变函数的执行环境,简单来说就是可以改变函数内部 this 的指向

使用 call 可以让一个对象借用另外一个对象的方法,可以借此实现继承

第一个传入的参数是上下文执行环境,即函数运行时 this 的指向,之后传入的参数将会直接传递给调用函数

在 call 调用完成后,返回调用函数的返回值

// 借用方法
let apple = {
    color: 'red',
    getColor: function() { return this.color }
};

let banana = {
    color: 'yellow'
};

let color = apple.getColor.call(banana);
console.log(color);
// 执行结果:yellow
------------------------------------------------------------------------------------------------------------------------------
// 实现继承
function Parent(age, name) {
    this.age = age;
    this.name = name;
    this.getName = function() { return this.name; };
    this.setName = function(name) { this.name = name; };
}

function Child(age, name) {
    Parent.call(this, age, name);
}

let child = new Child(18, 'Peter');
child.setName('Steve');
let name = child.getName();
console.log(name);
// 执行结果:Steve

apply 事件

apply 的作用与 call 完全一样,都能用于改变函数的执行环境,两者的区别仅仅在于传入的参数

第一个参数传入的都是上下文执行环境,即函数运行时 this 的指向,参数的区别在于之后传入的参数

之后传入的参数是调用函数执行所需的参数,call 是按照顺序直接传入,而 apply 是将参数放在数组中再传入

// 判断类型
let number = 0;
let string = '';
let boolean = true;
let object = {};
let array = [];

function typeOf(value) {
    return Object.prototype.toString.apply(value).slice(8, -1);
}

console.log(typeOf(number));				// 执行结果:Number
console.log(typeOf(string));				// 执行结果:String
console.log(typeOf(boolean));				// 执行结果:Boolean
console.log(typeOf(object));				// 执行结果:Object
console.log(typeOf(array));					// 执行结果:Array
--------------------------------------------------------------------------------------------------------------------------------
// 数值求和
function addNumber() {
    let isNumber = function(value) { return typeof value === 'number'; };
    let numbers = Array.prototype.filter.apply(arguments, [isNumber]);
    let sum = numbers.reduce(function(prev, curr) {
        return prev + curr;
    });
    return sum;
}

let result = addNumber(1, 'a', 2, 'b', 3, 'c');
console.log(result);
// 执行结果:6

bind 事件

传入 bind 的参数与 call 完全相同,作用也与 call 大致一样,但它们还是有所区别的

call 在调用后马上执行函数,bind 不会,调用 bind 返回一个改变了上下文的新函数,可以在需要的时候再调用

// 借用方法
let apple = {
    color: 'red',
    getColor: function() { return this.color }
};

let banana = {
    color: 'yellow'
};

let getColorForBanana = apple.getColor.bind(banana);
console.log(getColorForBanana);					// 执行结果:ƒ () { return this.color }
let color = getColorForBanana();
console.log(color);								// 执行结果:yellow
--------------------------------------------------------------------------------------------------------------------------------
// 解决回调函数 this 指向的问题
let object = {
    value: 0,
    asyncPrint: function() {
        setTimeout(function() { console.log(this.value); }, 2000);
    },
    asyncPrintWithThat: function() {
        let that = this;
        setTimeout(function() { console.log(that.value); }, 2000);
    },
    asyncPrintWithBind: function() {
        setTimeout(function() { console.log(this.value); }.bind(this), 2000);
    }
};

object.asyncPrint();								// 执行结果:undefined
object.asyncPrintWithThat();						// 执行结果:0
object.asyncPrintWithBind();						// 执行结果:0
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值