title: 原型链、闭包与继承
tags:
- javaScript
- 原型链与闭包的深入理解
categories:
- web前端
- javaScript
闭包
什么是闭包?
《JavaScript高级程序设计》这样描述:
闭包:是指有权访问其他函数中局部变量的函数。
《JavaScript权威指南》这样描述:
从技术的角度讲,所有的JavaScript函数都是闭包:它们都是对象,它们都关联到作用域链。
《你不知道的JavaScript》这样描述:
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
闭包作用
先看下面一段代码:
function A(){
var a = 10; // 局部变量a
function B(){
console.log(a); // 10
}
return B; // 最后返回函数B,函数B就是我们的闭包
}
var result = A(); // result就是整个B函数
result(); //输出10
解释:
由于函数A内部的局部变量a不能被A以外的函数访问到,只能被A内部的子函数B访问到,这是由于JavaScript的链式作用域
结构导致的,既然只有内部函数B才可以访问到函数A中的局部变量a,那么我们只需要把函数B作为函数A的返回值,就可以在函数A外部访问它的局部变量a! 其中函数B就是闭包
。
由上可知:
- 闭包就是一个函数。
- 闭包作用:访问其他函数内部的局部变量。
所以,在函数内部创建子函数,最后返回子函数,这是平常开发创建闭包最常见的方式。
面向对象中使用闭包读取设置私有属性
//使用构造函数创建一个Person class Person{}
function Person(name,age){
this.name = name; // this公有属性
var age = age; // var 私有属性
//通过一个公有的函数来访问其私有属性
this.getAge = function(){
return "age:"+age;
}
//通过一个闭包函数来修改其私有属性
this.setAge = function(newage){
age = newage;
}
}
//实例化一个实例对象
var p1 = new Person('大锤',18);
console.log(p1.age); // undefined 不能直接读取私有属性
console.log( p1.getAge() ); // 18
p1.setAge('38');
console.log( p1.getAge() ); // 38
由于var定义的属性是私有属性(局部变量),因此只能通过闭包函数去读取。
注意,由于闭包可以访问函数内的局部变量,所以此变量是不会被垃圾回收机制回收的,使用不当还可能造成会内存泄漏(未能释放已经不再使用的内存)。
继承
回顾创建对象的方式
- 通过new Object()或{},两种本质是一样的,后者是前者的语法糖形式(简写形式)。
- 工厂方式创建对象
- 构造函数和原型对象(定理)
- 构造函数有个prototype属性,指向原型对象
- 原型对象有个constructor属性,指向构造函数本身。
- 通过构造函数new出来的对象,有个隐形的属性proto属性,指向原型对象。
call和apply
1、call和apply作用
执行函数并改变函数中的this指向。
2、call和apply的区别
语法如下所示:
fuName.call(obj,实参数1,实参数2...)
fuName.apply(obj, [ 参数1,参数2..... ] )
说明:使用对象obj伪造函数fuName中的this,于是函数fuName中的this都被obj对象伪造了。
- 相同点:都是改变函数内this的指向。
- 不同点:参数传递的形式不一样。
- call参数是一个一个传,需要传递几个就需要看函数需要几个形参。
- apply是传递一个参数数组,如果apply是使用在某个函数内,则参数数组可以使用类数组arguments进行代替。
call和apply继承代码实现
1、通过call或者apply继承的思想:
在子类的构造函数中,使用call或apply去伪造父类构造函数中的this,把父类构造函数中的属性添加到子类对象的自身空间中。
2、 代码实现
//构造出父类
function Parent() {
this.email = "qq.com",
this.color = ["red","green"]
}
// 给父类的原型中添加方法
Parent.prototype.getEmail = function() {
return "my email is" + this.email;
}
// 实现伪造继承,把父类的属性继承到子类中
function Child(name) {
Parent.call(this); // 使用call
this.name = name; // 子类自身的属性
}
3、 call或apply继承带来的缺点
通过call或apply只能会把父类构造函数中定义的属性复制一份到子类对象自身空间中,但是无法继承到父类原型对象上面的属性。
解决办法: 通过原型继承
来实现,可以继承父类原型对象中的属性。
通过原型对象继承
1、原型对象继承的核心思想
用父类对象重写子类原型对象。核心代码如下
子类.prototype = new 父类()
2、代码实现即内存空间图解
3、通过原型继承带来的问题
当子类的某个对象对引用类型属性如数组添加一个值的时候,实质上,我们是把此值添加在子类原型对象的属性中,这样会造成所有子类对象对此属性的读取,这是我们不允许的。
我们希望,每个子类自身空间中都独有一份引用属性,则当子类对象修改的时候只会影响当前对象,对其他对象没有任何影响。
解决办法:通过混合继承。 通过伪造继承+原型继承
混合起来,就可以解决伪造继承和原型继承单独使用带来的问题。
混合继承(伪造+原型对象)
1、伪造继承和原型继承分别带来的问题
伪造继承带来的问题:只能将父类构造函数中的属性放到子类对象的自身空间中,但是无法继承父类原型对象上面的属性。
原型继承带来的问题:子类对象只能继承父类原型对象上面的属性,但是无法把父类构造函数中的属性放到自身空间中。
2、通过伪造和原型对象混合继承的核心思想:
通过伪造继承,把父类构造函数中的属性添加到子类对象的自身空间中
通过原型继承,继承到父类原型上面的属性
最后把他们两者组合起来即可(形成互补)
如:
伪造继承
:它会前端,但不会后台
原型继承
:不会前端,但会后台
组合起来,可实现既能写后台又能写前端。 (ps:年轻人都加油)
3、代码实现
function Parent(){
this.email = 'qq.com'
this.color = ['red','green']
}
Parent.prototype.getEmail = function(){
return "my email is " + this.email;
}
function Child(name){
Parent.call(this); //伪造继承
this.name = name;
}
//原型继承的思想: 子类.prototype = new 父类();
Child.prototype = new Parent();
//给子类原型也加方法
Child.prototype.getName = function(){
return "my name is " + this.name;
}
var c = new Child('zs');
var c2 = new Child('zs');
console.log(c);
通过es6创建类和继承
- class 定义类
- extends 继承
//创建父类Parent
class Parent{
//构造函数(不要加关键字function)
constructor(name,age){
this.name = name;
this.age = age;
}
getName(){
return this.name;
}
}
//创建子类Child继承Parent
class Child extends Parent{
//构造函数(不要加关键字function)
constructor(name,age,email){
//执行父类的构造函数,继承name和age属性,到子类对象自身空间中
super(name,age);
this.email = email;
}
getAge(){
return this.age;
}
}
//实例化子类对象
var c = new Child('zs',33,'zs@qq.com')
console.log( c.getAge() ); // 33
console.log( c.getName() ); // zs
class和extends底层还是通过原型对象来实现的,其是原型对象的一种语法糖形式(简化写法)
继承小结
-
使用混合继承(伪造+原型对象)要点:
- 利用伪造call或apply继承父类构造函数中的属性到子类的自身空间中
- 利用原型对象继承父类原型对象上的属性
最终将a、b两种组合起来即可实现完整的继承。
继承原型链终极图解
在js继承体系中,最终都是通过对象的proto找到其对应的原型对象,最终实现继承。
注意:
- 函数没有返回值,默认返回undefined
- 函数名.length 查看最少传递的实参个数。