频道栏目
首页 > 资讯 > JavaScript > 正文

js原型、原型链、作用链、闭包全解

17-11-18        来源:[db:作者]  
收藏   我要投稿

【对象、变量】

一个对象就是一个类,可以理解为一个物体的标准化定义。它不是一个具体的实物,只是一个标准。而通过对象实例化得到的变量就是一个独立的实物。比如通过一个对象定义了“人”,通过“人”这个标准化定义,实例化了“小明”这个人。其中“人”就是对象,“小明”就是变量。实例化的过程就是通过构造函数,来初始化设置标准定义中是具体指。比如在创建“小明”这个变量时,同时设置了他的名称,性别等信息。在变量中包含对对象的引用,所以可以通过变量操作对象,或引用对象的函数、属性。比如“小明”有手有脚(属性),可以抬头低头(函数)。

这里写图片描述

【原型、原型链】

什么是派生?

原型对象派生另一个对象,就是创建了原型对象的副本,占有独立的内存空间,并在副本上添加一个独特的属性和方法。

在js系统中,Object对象派生了Number对象、Boolean对象、String对象、Function对象、Array对象、RegExp对象、Error对象、Date对象。当然你可以通过Object对象派生自己的对象。

右键图片-在新标签页打开图片-查看清晰图片
这里写图片描述

在js系统中,Function对象又派生了Number函数、Boolean函数、String函数、Object函数、Function函数、Array函数、RegExp函数、Error函数、Date函数、自定义函数。

这也就是为什么说函数是一种特殊的对象。因为函数是通过Function对象派生的。

从上面的介绍我们知道一切对象派生于Object对象。Object对象中包含了一系列属性和方法,可以参考js系列教程2-对象、对象属性全解

这里主要介绍__proto__和constructor属性。由于所有对象都继承自Object对象,所以所有对象(包括函数)都拥有这两个属性。

每个对象的proto属性是保存当前对象的原型对象。

所以Number对象、Boolean对象、String对象、Object对象、

Function对象、Array对象、RegExp对象、Error对象、Date对象的proto都指向Object对象。

Number函数、Boolean函数、String函数、Object函数、Function函数、Array函数、RegExp函数、Error函数、Date函数、自定义函数的proto都指向Function对象。

这种派生对象使用__proto__指针保存对原型对象的链接,就形成了原型链。对象通过原型链相互连接。所有的对象都在原型链上。所有的原型链顶端都是Object对象。

我们在原型对象中的一般用来实现所有可能的派生对象或实例变量的公共方法和公共属性。

构造/实例化

上面讲了什么是派生,原型链的形成。

那什么是实例化呢?

实例化即创建一个变量的过程,是将对象浅复制一个副本,然后通过构造函数来对这个占有独立空间的变量进行初始化。

你可以用“人”派生了“男人”、“女人”。男人实例化了“小明”、“小王”。

准确说法应该是这个占有独立空间的副本也是一个对象,这个指向副本的链接才是变量,叫做引用变量。所以变量也可以进行实例化,其实实例化的是变量指向的副本对象。这个我就把副本对象叫做变量以区分实例化和派生。

右键图片-在新标签页打开图片-查看清晰图片
这里写图片描述

所以在js中要想实例化一个对象,进而创建一个变量的过程都需要有一个原型对象,和一个构造函数。我们把这个原型对象叫做函数的构造绑定对象,把函数叫做原型对象的构造函数。

要注意区分函数的原型对象是Function对象

为了表达这种对象与构造函数的紧密关系,js在在对象中使用constructor属性保存当前对象的构造函数的引用,在构造函数中使用prototype保存对对象的引用。对象实例化的变量中,constructor指向构造函数、__proto__指向这个对象。我们也可以称这个对象是这个变量的原型对象。

我们可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。

而我们使用函数当做构造函数时,并没有创建这个原型对象呀?

这是因为在定义函数时,系统除了将函数的proto属性指向Function对象外,还会自动由Object对象派生了一个对象,作为这个函数的构造绑定对象,在函数中用prototype指向这个对象。

原型链的向上搜索

派生对象或实例化对象,都要为新对象分配一个独占的空间。并且把原型对象的属性和方法复制一份给新对象,而这个复制仅仅是引用复制(即浅复制)

(其实在js中有很多种构造方式,每种构造方式都有不同的实例过程,在java、c++、c#中,实例化对象的过程是固定的,这也就造成了js的功能复杂性。这里讨论大家常用的实例化方法,即使用new来创建对象的方法)

当然我们也可以在修改原型对象的属性或替换原型对象。

在查询属性或方法时,当前对象没有查询到时,会自动在原型对象中查询,依次沿原型链向上。

由于在派生和实例化的过程中,新对象和新变量都会保留对原型对象的引用。当函数调用时,需查找和获取的变量和元素都会通过原型链机制一层层的往上搜索在原型对象或继承来的对象中获得。

实例化对象产生新变量的三种方式

1、字面量方式

通过Object函数创建D变量。

var D={}

Object对象通过Object构造函数,实例化获得变量D。变量D的__proto__指向Object对象。

var a = {};
console.log(a.prototype);  //undefined,未定义
console.log(a.__proto__);  //{},对象Object
console.log(a.constructor); //[Function: Object],函数Object
console.log(a.__proto__.constructor);  //[Function: Object],函数Object

2、构造函数方式

通过构造函数B创建对象C

function B(){}
var C=new B()

B函数定义时,系统会自动由Object对象派生一个中间对象作为函数的构造绑定对象Temp。通过函数B实例化变量时,就是对Temp对象进行实例化得到变量C。变量C就拥有Temp对象的属性方法(就是原始Object对象的属性和方法)+构造函数中的属性方法。变量的__proto__ 指向这个Temp对象,变量的Constructor指向函数。

var A = function(){};
console.log(A.prototype);  //A {},A函数的构造绑定对象
console.log(A.__proto__);  //[Function],Function对象
var a = new A();
console.log(a.__proto__); //A {},A函数的构造绑定对象
console.log(a.constructor); //[Function: A],函数A
console.log(a.constructor.prototype); //A {},A函数的构造绑定对象
console.log(a.__proto__.__proto__); // {},(Object对象)
console.log(a.__proto__.__proto__.__proto__); //null

3、通过Object.creat创建对象

如图中通过对象D创建对象E

var E=Object.creat(D)

E变量的原型链指向对象D。

var a1 = {'age':1}
var a2 = Object.create(a1);
console.log(a2.__proto__); //Object { age: 1 }
console.log(a2.constructor); //[Function: Object]

案例讲解

现在我们再来看案例。是不是清晰多了。js在线测验网站http://tool.lu/coderunner/

function Person () {
}
var person1 = new Person();       
Person.prototype.age= 18;
Person.__proto__.name= "小明";
var person2 = new Person(); 
console.log(person1.age);//18
console.log(person2.age); //18
console.log(person2.name);  //未定义

var person1 = new Person(); 这条语句。通过函数实例化了一个变量,系统自动创建一个Object对象派生的中间对象Temp作为与构造函数绑定的原型对象。Person.prototype就指向这个中间对象Temp。
Person.prototype.age修改了Temp对象。
Person.__proto__.name,我们知道函数都是由Function对象派生的,这句话就是修改的Function对象对象。
var person2 = new Person(); 这个语句同样通过函数实例化一个对象。一个构造函数只能绑定一个原型对象,所以这个原型对象就是Temp对象
person1.age访问了age属性,先在当前空间中查找,没有找到,于是沿原型链向上查找这个原型对象Temp。查找成功。
person2.name在变量和原型对象Temp中都不存在,所以显示未定义。

下面的留给读者自己理解

var a1 = {'age':1}
console.log(a1.prototype);  //undefined,未定义
console.log(a1.__proto__);  //{},对象Object
console.log(a1.constructor); //[Function: Object],函数Object
console.log(a1.__proto__.constructor);  //[Function: Object],函数Object

var a2 = Object.create(a1);
console.log(a2.__proto__); //{ age: 1 },对象a1
console.log(a2.constructor); //[Function: Object],对象Function

var Person = function(){};
console.log(Person.prototype);  //Person {},函数Person的构造绑定对象
console.log(Person.__proto__);  //[Function],对象Function
var person1 = new Person();
console.log(person1.__proto__); //Person {},函数Person的构造绑定对象
console.log(person1.constructor); //[Function: Person],函数Person
console.log(person1.constructor.prototype); //Person {},函数Person的构造绑定对象
console.log(person1.__proto__.__proto__); // {},(Object对象)
console.log(person1.__proto__.__proto__.__proto__); //null


Person.prototype.age= 18;
Person.__proto__.name= "小明";
var person2 = new Person(); 
console.log(person1.age);//18
console.log(person2.age); //18
console.log(person2.name);  //未定义

【作用域】

javascript中的作用域可以理解为一个语句执行的环境大小,有全局的作用域,函数作用域和eval作用域。在JS中没有块级作用域。所以在js中if语句,for语句内的不存在只有他们能访问的变量。只有在函数内存在局部变量。但不要因此就定义除函数以外所有使用{ }括起来的都是块级元素。对象使用{ }定义变量,对象内是属性的定义,不是变量定义,所以不存在作用域的说法。

if(1){
  var name2='lp2'
}
console.log(name2)   //lp2
for(var i=0;i<10;i++){
    var name3='lp3'
}
console.log(name3)  //lp3
Person={
    name4:'lp4'
}
console.log(name4)  //undefined
function person(){
  var name1='lp1'
}
console.log(name1)  //undefined

讲到作用域,不得不讲执行环境,执行环境在JS中是最为重要的概念。执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为。每个执行环境都有一个与之关联的环境对象,环境中定义的所有变量和函数都保存在这个环境对象中。在web浏览器中全局环境被认为是window对象,某个执行环境中的所有代码执行完毕后就被该环境销毁,保存在其中的所有变量和函数定义也随即销毁。

每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。

当代码在一个环境中执行时,会创建环境对象的一个作用域链。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端始终是当前执行的代码所在外部环境的环境对象。作用域链中的下一个环境对象来自包含(外部)环境,再下一个环境对象则来自于再下一个包含环境,这样一直延续到全局执行环境,全局执行环境的环境对象始终都是作用域链中的最后一个对象。

需注意的是:在局部作用域中定义的变量可以在全局环境和局部环境中交互使用。内部环境可以通过作用域链访问所有的外部环境,但外部环境不可以访问内部环境中的任何变量和函数。每个环境都可以向上搜索作用域链,以查询变量和函数名,但任何环境都不可以通过向下搜索作用域链而进入另一个执行环境。

作用域链本质上是一个指向环境对象的指针列表,他只引用但不包含环境对象。

【闭包】

闭包是指有权访问另一个函数作用域中的变量的函数,这里要把它与匿名函数区分开(匿名函数:创建一个函数并将它赋值给一个变量,这种情况下创建的函数叫匿名函数,匿名函数的name属性是空字符串),创建闭包的常见方式就是在一个函数内部创建另一个函数。闭包保存的是整个变量的对象。

闭包的作用:在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量,这时灵活方便的闭包就派上用场,我们知道当一个函数被调用时就会创建一个执行环境及相应的作用域链,那么闭包就会沿着作用域链向上获取到开发者想要的变量及元素。

闭包灵活方便,也可以实现封装,这样就只能通过对象的特定方法才能访问到其属性。但是,不合理的使用闭包会造成空间的浪费,内存的泄露和性能消耗。

当函数被创建,就有了作用域,当被调用时,就有了作用域链,当被继承时就有了原型链,当需要获取作用域链或原型链上的变量或值时,就有了闭包。

相关TAG标签
上一篇:Python初级学习教程
下一篇:编程开发洛谷p1307求解
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站