原型、继承、原型链、圣杯模式

107 阅读5分钟

1、原型是什么?

原型就是对象里面一个以__proto__为键的隐式属性。这个属性的值指向一个prototype对象,这个对象也叫原型对象。prototype是构造器下的一个属性。所有被同一个构造函数创建出来的对象的原型,都指向构造器同一个prototype对象,所以这些对象具有prototype对象里面相同的属性和方法。除非在实例化一个对象obj1之后,又给这个构造器下的prototype对象指向其他的对象,然后又用这个构造函数创建出另外的实例对象obj2,此时obj1的原型对象和obj2的原型对象,是不一样的。这也说明,一个对象一旦被创建,它的原型对象也被创建。

function Car() {}
Car.prototype.brand = "Benz"
var car1 = new Car();
var car2 = new Car();
console.log(car1.__proto__ === car2.__proto__); //true
//把一个新的对象赋给构造器下的prototype属性
Car.prototype = { brand :"Benz" }
var car3 = new Car();
Car.prototype.age=2
console.log(car1.__proto__ === car3.__proto__); //false
console.log(car1.age);//undefined
console.log(car3.age);//2
car1.__proto__.length=5
console.log(car2.__proto__.length);//5

分析以上代码:

第5行代码:说明在Car.prototype没有赋予新的对象之前,所有被同一个构造函数创建出来的对象的原型,都指向同一个原型对象。

第10行代码:说明Car.prototype没有赋予新的对象之后,所创建出来的对象的原型,和Car.prototype没有赋予新的对象之前,所有创建出来的对象的原型,指向的不是同一个对象。

第11行的结果undefined和12的结果是2,进一步说明car1和car3的原型,指向的是不同的对象。

第13行:给car1的原型对象添加一个length属性值为5;

第14行打印出car2的原型对象的length属性的值为5。

综合13、14行代码,进一步说明car1和car2的原型对象是同一个对象。

2、继承

JS引擎寻找对象属性的机制:先在对象的自身上查找,如果找不到就到原型对象上找,如果还是找不到就到原型对象上的原型上找,直到Object.prototype。如果找到就立即返回,不再往下查找,否则返回undefined。对象的原型对象就是继承的桥梁,把一个obj对象,设置为构造器的prototyoe属性的值,那么该构造器创建出来的obj2对象就继承了obj对象。obj对象就是obj2对象的祖先。

Object.prototype.superSkill="All"
professor.prototype.tSkill = "Java"
function professor() {}
function Teacher() {
	this.mSkill = 'JS'
}
Teacher.prototype = new professor();
var teacher = new Teacher()
Student.prototype = teacher;
function Student() {
	this.pSkill = 'CSS'
}
var student = new Student()
console.log(student.pSkill);//CSS
console.log(student.mSkill);//JS
console.log(student.tSkill);//Java
console.log(student.superSkill);//All

继承的关系如下图:

1642410533(1).jpg

3、原型链

由于对象有原型,指向原型对象。原型对象也是对象,所以它也有自己的原型,它的原型又指向另外的原型对象。这样形成有序的链条,叫原型链。 原型链的尽头是什么,有意义的是Object.prototype,没有意义的是null。js引擎查找对象的某一个属性或者方法,只查到Object.prototype这一级就结束。

3-1:子类操作父类属性:

1、父类的原始值属性:只能访问,不能增删改。

2、父类的对象属性:可以随意操作,包括删除。

3、父类的方法:子类可以访问,可以重写(对自己生效),其他子类无效。

4、圣杯模式

有一个这样的场景,比如父类有些私有属性,不想让子类访问,公共的属性对所有的子类都可见。 主要思想是通过中间对象实现间接继承,隔离父子的原型直接访问,代码实现如下:

function inherit(Target, Origin) {
	// 1、声明一个构造函数Buffer
	function Buffer() {}
	// 2 把父级的原型对象赋给Buffer的原型对象
	Buffer.prototype = Origin.prototype
	// 3、把Buffer的实例赋给子类的原型对象
	Target.prototype = new Buffer();
	// 还原构造器
	Target.prototype.constructor = Target;
	Origin.prototype.constructor = Origin;
}
// 公共属性
Parent.prototype = {
	firstName: '姓',
	say: function() {
		console.log("hello world");
	},
	makeMoney: function() {
		console.log(`通过${this.job}赚钱`);
	}
}

// 私有属性
function Parent() {
	this.job = "种地";
	this.privateName = "个人名字";
}
function Childern() {
	this.job = "写代码"
}

inherit(Childern, Parent);
var child = new Childern();
var parent = new Parent();
console.log(child.privateName);//undefined
//打印出undefined,证明父类的私有属性,子类访问不到。
child.makeMoney();//通过写代码赚钱
parent.makeMoney();//通过种地赚钱

5、其他说明

5-1: 不是所有的对象都继承Object.prototype,比如Object.create(null)。

5-2: call、aplly、bind:他们都可以该this的指向,所以他们可以借用构造数里面的方法,并不能真正的实现继承:

call和apply都可以改变被调用函数this的指向,而且是立即执行,只是给函数传参的方式不同:

1、apply:是以数组传参。

2、call:传多个单个参数。

3、bind:也可以改变被调用函数this的指向,但是指向绑定函数,并不执行,按需执行。传参和call一样。

Animal.prototype.food="fish"
function Animal(a,b,c){
	this.species='animals'
	this.move=function(){
		console.log(`I am ${this.name},I can ${this.skill},I belong to ${this.species}${a} ${b} ${c} Go !`);
	}
}

function Cat(){
	Animal.call(this,1,2,3)
	this.name='cat'
	this.skill="runing"
}

function Bird(){
	Animal.apply(this,[1,2,3])
	this.name='bird'
	this.skill="Flying"
}

var cat=new Cat();
cat.move();
console.log(cat.eat);//undefined

var bird=new Bird();
bird.move();

通过console.log(cat.eat)打印出undefined,可以证明他们不能实现真正的继承,因为访问不到原型上的属性。

通过节流的案例说明bind的用法:

var inp = document.getElementById("inputId")
inp.oninput = throttle(function() { console.log(this.value); }, 500)

function throttle(fn, delay) {
	var timer
	return function() {
		if (timer) {
			clearTimeout(timer)
		}
		timer = setTimeout(fn.bind(this), delay)
	}
}

5-3:立即执行函数/IIFE(immediately invoke function Exprssion)

1、立即执行函数实现插件:

(function() {
	function add(a, b) {
		return a + b
	}
	
	function multiply(a, b) {
		return a * b
	}
	window.add = add;
	window.mul = multiply;
})()
console.log(window.add(2, 6));
console.log(window.mul(2, 6));

1、立即执行函数实现模块化,模块化是防止全局变量污染:

(function() {
	var a = 3
	var b = 6
	function initAdd() {
		console.log(a + b);
	}

	function initMultiply() {
		console.log( a * b);
	}
	window.init = function() {
		initAdd();
		initMultiply();
	}
})()
window.init()

5-4:链式调用

var obj={
	fn1:function(){
		console.log('fn1');
		return this
	},
	fn2:function(){
		console.log('fn2');
		return this
	},
	fn3:function(){
		console.log('fn3');
		return this
	}
}
obj.fn1().fn2().fn3()

5-5:对象访问属性

最早的JS引擎都是用方括号都是obj['key']来访问的 ,现在可以用点运算符来访问属性,再JS的底层是执行时都会转换成方括号运算符来访问,方括号运算符支持表达式,即obj['key'+1]

5-6:判断数组的3种方法

console.log(Array.isArray([]));//true
console.log([] instanceof Array);//true
console.log(Object.prototype.toString.call([])==='[object Array]');//true

5-7:对象原型链上属性操作方法 1、for in 只能遍历原型链上可枚举的属性;

2、in 判断原型链上是否存在某个属性或者方法;

3、Object.hasOwnProperty('key',obj)/obj.hasOwnProperty('key')判断key属性是不是obj的自身属性

4、Object.keys获取对象自身可枚举的属性和方法名称;

5、Object.getOwnPropertyNames,获取对象自身的属性和方法名称。

Fn1.prototype.top='top'
Fn1.prototype.say=function (){console.log("hello world");}
function Fn1(){
	this.fn1="fn1"
}
Fn2.prototype=new Fn1()
function Fn2(){
	this.fn2="fn2"
}
Fn3.prototype=new Fn2()
function Fn3(){
	this.fn3="fn3"
	this.eat=function(){}
}
var obj=new Fn3()
Object.defineProperty(obj,'name',{
	enumarable:false,
	value:'tony'
})
for (let key in obj) {
	console.log(obj[key]);//fn3、f ()、fn2、fn1、top、f ()
}
console.log(obj.name);//tony
console.log('name' in obj);//true
console.log('say' in obj)//true

console.log(Object.keys(obj));//["fn3", "eat"]
console.log(Object.getOwnPropertyNames(obj));//["fn3", "eat", "name"]