浅谈JavaScript原型链继承方式与闭包

来源:互联网 发布:国家电网网络大学答案 编辑:程序博客网 时间:2024/05/22 15:06

JavaScript中的继承方式有两种,一种是通过call或apply方法借用构造函数,另一种方法则是通过原型来继承。这篇文章用于梳理自己关于prototype继承的知识点,也略微涉及一点点闭包的概念。

关于原型

在JavaScript中每当我们创建一个Function类的新对象,这个对象都会自动同时获得一个名为prototype的属性,这个属性指向原型对象,它包含一个名为constructor的属性,这个属性始终指向创建当前对象的函数(构造函数)。

var obj = function(){    this.attr= 'attr';    this.method= function () {        return 'method';    }};console.log(obj.prototype);//Object {}

但是已经实例化的对象中是不存在prototype属性的:

var obj1 = new obj();console.log(obj1.prototype);//undefined

那么prototype去了哪里呢?别急,回到我们开头的代码片,我们去给prototype做个标记:

obj.prototype.property1 = 1;console.log(obj.prototype);//Object {property1: 1}

这里可以看到原型对象中已经有了我们刚才定义的属性property1: 1。
现在,我们实例化一个obj对象,然后打印它,尝试把我们做的标记找出来:

var obj2 = new obj();console.log(obj2);

是不是翻遍了所有属性也没有找到,只剩下最后一个看起来很奇怪的东西?

__proto__

展开它就会发现,我们为obj.prototype添加的属性就在里面。

这里要引入一个新的概念,我觉得应该称之为……对象的“实际原型”。将任意一个对象打印到控制台,展开它的属性列表,我们总是能看到这么一个长得很奇怪的东西,我们为我们构造的对象——此处我认为应当称之为“类型对象”——的prototype添加的属性和方法,都在这里面。

实际上这个东西在我们上面打印过的obj.prototype中也出现了,展开之后会发现在这里面它本身没有出现:因为实际上在这里obj.prototype中的__proto__所指向的就是Javascript原型链的尽头:Object.prototype

原型继承

根据前文的结论,我们可以很简单地做出推断:当构造一个新对象的时候,如果将对象2的原型(prototype属性)赋值为对象1的实例,那么对象1的任何新实例都将拥有对象1所有的属性和方法。
这相当于每当对象2被实例化,就创建一个对象1的只读副本,对实例化的对象2做出的任何修改不会影响到原本的对象1,而对原本的对象2进行修改则会影响到每一个实例化的对象2,除非该对象2的同名属性已经被修改和覆盖。
此时我倾向于将类型对象1称之为“父类对象”,类型对象2称之为“子类对象”。

初学者有可能会认为这样就实现了prototype继承,实际上这样也确实可以某种程度上达到目的:

var obj1 = function(){    this.attr1 = 'a';    this.method1 = function() {        return 'this is method1';    }}var obj2 = function(b){    this.attr2 = b;    this.method2 = function() {        return 'this is method2';    }}obj2.prototype = new obj1();var obj = new obj2('a');console.log(obj.method1());//this is method1

闭包

但是,仅仅这样还不够。来看下面的代码片:

var obj1 = function() {    this.attr1 = ['b','c'];}var obj2 = function (b) {    this.attr2 = b;}obj2.prototype = new obj1();var a1 = new obj2('d');a1.attr1.push('e');1.attr1 = 'b';onsole.log(a1.attr1);//'b'ar a2 = new obj2('c');console.log(a2.attr1);//['b','c','e']

可以看到,虽然修改实例化的子类对象无法重写原型对象obj1的属性attr1,但却可以使用push方法对obj2.prototype.attr1进行修改。
这是由于在JavaScript中,同一作用域中的引用类型复制操作,其复制的结果只是复制了对同一个对象的引用。实际上进行修改时,并不是修改了我们以为的对象的新副本,而是修改了被引用的对象,所有对其的引用都会受到影响。
在本例中,a1和a2的实际原型都是obj1的同一个实例,虽然作为原型被继承的实例本身并不会由于子类对象的实例被修改而受到影响(重写a1.attr1并不会影响到obj2.prototype.attr1),但是我们却可以访问并修改Array对象['b', 'c'],从而间接地改变了obj2.prototype的属性。
在实际的应用中这是十分危险的,原型对象的属性被修改往往会带来一连串的问题。
解决这一问题的办法就是将对象进行再次封装:

//var attr1 = ['b', 'c'];var obj2 = function () {    var _attr1 = ['b', 'c'];    var i = 0;    var obj1 = function () {        i++;        this.attr1 = _attr1;        //this.attr1 = attr1;        this.attr3 = this.attr1;        this.method1 = function () {            document.write('attr3: ' + this.attr3 + '<br />');        }    }    var OBJ2 = function (b) {        this.a = i;        this.attr2 = b;    }    OBJ2.prototype = new obj1();    OBJ2.prototype.id = i;    return new OBJ2();}var a1 = new obj2('d');a1.attr1.push('f');var a2 = new obj2('c');console.log(a1.attr1);//["c", "d", "f"]console.log(a2.attr1);//["c", "d"]console.log(a1.attr1 === a2.attr1);//FALSE

这样通过将obj1和OBJ2的构造函数放在一个函数内,将它们的赋值操作限制在函数obj2的作用域内,就保护了我们不希望被修改的obj1的私有属性。
为了保护私有属性或者不污染全局环境而将变量等放置在一个函数中,使他们处于函数作用域中,这个方法有个专有名词,就叫做闭包。

写一篇博客梳理一下,才发现自己对知识点的理解其实并没有自己以为的那样到位,欢迎各位交流、指教。

0 0