JS 原型,原型链,作用域,闭包全解

对象,变量

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

js中数据的类型

underfined,null,boolean,string,number为基本数值类型,其他的为引用类型

所以在浅赋值对象时,基本数值类型的赋值就是数值赋值,而对象的赋值是引用赋值。修改副本的基本数值类型,不影响源对象。

原型,原型链

什么是派生?

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

在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对象中包含了一系列属性和方法。

这里只要介绍__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对象。

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

构造/实例化

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

那什么是实例化呢?

实例化即创建一个实例对象的过程,是先为实例对象开辟一个内存,然后添加对原型对象的引用到__proto__属性,然后通过构造函数来对这个占有独立空间的对象进行初始化

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

实例化时的内存操作如下

实例化对象在查询变量时是先查询本身是否具有该属性,再沿原型链向上查询,例如

console.log(实例对象,数值变量1)

不能通过实例对象属性的形式修改原型对象的属性,这样只会在实例对象中新增同名属性例如

实例对象.数值变量1 = 12;   //这是为实例对象增加“数值变量1”属性
实例对象.引用变量1 = [1,2];    //这是为实例对象增加“引用变量1”属性

但是可以通过实例对象属性的形式修改原型对象引用变量指向的数据,例如

实例对象.引用变量2.append(1);     //修改对象2空间的数据

可以通过实例对象__proto__属性获取原型对象的引用再修改属性。或者通过构造函数的prototype属性获取原型对象的引用再修改属性

实例对象.__proto__.数值变量1 = 12;   //修改原型对象的属性

所以构造函数中用this来获取原型对象的属性

function{
  this.aa = 1;
  this.bb = 'a';
  this.cc = ['aa0'];
}

上面的函数作为构造函数,就是新增或修改了原型对象的三个属性。对于构造函数中新增的属性,这些属性存储在实例化的对象中,而不是在存储在原型对象的属性当中,对于修改的属性,属性保留原来的存储位置。

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

所以在js中要想实例化一个对象,进而创建一个变量的过程都需要有一个原型对象,和一个构造函数的引用,在构造函数中使用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.protytpe);    //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.creat(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.__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);	//未定义

作用域

在C++,java,c#中,在变量声明的代码段之外,变量是不可见的,我们通常称为块级作用域。而在JS中没有块级作用域,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

函数内不使用var声明的变量是全局变量,函数内声明的所有变量在函数体内始终是可见的。并且,变量在声明之前就可以使用了,这种情况就叫做声明提前(hoisting),tip:声明提前是在js引擎预编译时就进行了,在代码被执行之前已经有声明提前的现象产生了。

var name = "one";
function test(){
	console.log(name);	//undefined
	var name = "two";
	console.log(name);
}
test();

上面的图就是作用域链的全部内容。

每个函数都有一个作用域(window有一个全局作用域,可以把window想象成全局函数),每个函数对应有一个活动对象,函数作用域链,函数环境对象作用域链。他们并不是一直存在,而是动态创建的。

在定义新的函数时(不是执行到新函数时),就会为这个函数生成一个指针列表就是作用域链,它每一项都指向一个外部作用域的活动对象。作用域链实现以后,它的地址存储在window或函数的[[Scope]]属性中。作用域是栈的结构,它由上到下,依次排列着由内到外的作用域对应的活动对象。全局活动对象始终都是作用域链中的最后一个对象。函数的作用域链的第一项是定义函数时所处的作用域对应的活动对象,而不是函数自己的活动对象。

每个函数的活动对象中存放了当前作用域中定义的属性和方法。函数的活动对象在执行到作用域时才创建的,这也就是为什么定义函数时创建的函数作用域链不包含函数自己的活动对象,因为定义函数时,函数的活动对象还没有创建。

创建一个活动对象后,首先将该函数的每个形参和实参,都添加为该活动对象的属性和值,将该函数体内显示声明的变量和函数,也添加为该活动的属性(在刚进入该函数执行环境时,未赋值,所以值为undefined,这个是JS的提前声明机制),函数一边执行一边对活动对象中的属性进行赋值。

在执行到一个函数时,除了创建函数的活动对象,还会创建函数的环境对象,并为这个环境对象也创建一个作用域链。环境对象的作用域链是通过复制函数的作用域链再添加上函数的活动对象地址而形成。

在执行函数中查找变量,就是在这个函数的环境对象的作用域链中查找的,所以在执行过程中我们只是根据环境对象来确定变量的使用

在执行完函数后,如果没有变量再指向这个函数的环境对象,则这个环境对象就会销毁。而对象的销毁是由垃圾回收机制决定的,没有指向的对象都会被销毁。

** 闭包**

javascript的垃圾回收机制

在JavaScript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收,函数a被b引用,b又被c引用,那么abc都不会被消除

理解闭包要先理解下面的执行过程

从上面关于作用域链的内容,我们知道函数的定义与函数的作用域链有关,函数的执行与函数环境对象的作用域链有关,变量的查找,是在执行时沿着函数环境对象的作用域链进行的。

代码的执行,是在一个执行栈中进行的。执行到哪个作用域链将该作用域的环境对象压入栈顶。作用域执行完毕,若没有其他作用域中的变量指向该环境中的属性,就将这个环境对象弹出执行栈。否则将该环境对象保存在执行栈中。

栈中有一个指针负责指向代码执行处当前作用域的环境对象。

看下面的代码,最后输出10.

function A(){
	var max = 10;
	function B(){
		console.log(max);
	}
	return B;
}
var f1 = A();
var max = 100;
f1();

我们来看一下过程。

由于在代码运行中涉及对象,函数的声明,定义,运行,具体的过程在作用域链中已经讲解。

函数执行中变量的查找,是在执行时沿着函数作用域链进行的。而函数环境对象是在函数运行时创建的。所以为了简化理解,我们只关心函数的运行(不关心函数的定义)。在整个代码的运行中,先进入全局函数运行,然后运行A函数,然后返回全局函数,然后运行B函数,然后返回全局函数。代码运行结束。

所以创建环境对象的过程也是创建全局环境对象,然后创建函数A环境对象,由于运行完函数A后,函数A的环境对象中的属性被函数B的作用域链引用(因为函数的作用域链是在函数定义时创建的)。所以函数A的环境对象没有被弹出销毁。但是当前环境已经进入全局环境对象。然后调用B函数,又在栈顶创建了函数B的环境对象。函数B执行完后,函数B的环境对象弹出销毁,但是函数B的作用域链并没有销毁,所以此时仍然有变量指向函数A的环境对象。但是当前环境已经进入全局环境对象,继续运行。

在上面的执行过程中,变量的查找时沿着环境对象的作用域链,环境对象的作用域链是环境对象的一个属性。不是查找了别的环境对象。

闭包的定义

闭包是指有权访问另一个函数作用域中查找变量,这时灵活方便的闭包就派上用场,我们知道当一个函数被调用时就会创建一个环境对象(也叫上下文对象,执行环境对象)及环境对象的作用域链,那么闭包就会沿着环境对象的作用域链向上获取到开发者想要的变量及元素。

闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

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

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

使用闭包的注意点

1)由于闭包会使得函数中的变量被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

闭包的应用场景

保护函数内的变量安全。以最开始的例子为例,函数a中i只有函数b才能访问,而无法通过其他途径访问到,因此保护了i的安全性。

在内存中维持一个变量。依然如前例,由于闭包,函数a中i的一直存在于内存中,因此每次执行c(),都会给i自加1.

通过保护变量的安全实现JS私有属性和私有方法(不能被外部访问)

私有属性和方法在Constructor外是无法被访问的

function Constructor(...){
	var that = this;
	var membername = value;
	function membername(...){
		....
	}
}

转载于:https://my.oschina.net/u/3428115/blog/2251825

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值