js原型、原型链、作用链、闭包全解
https://www.2cto.com/kf/201711/698876.html
【对象、变量】
一个对象就是一个类,可以理解为一个物体的标准化定义。它不是一个具体的实物,只是一个标准。而通过对象实例化得到的变量就是一个独立的实物。比如通过一个对象定义了“人”,通过“人”这个标准化定义,实例化了“小明”这个人。其中“人”就是对象,“小明”就是变量。实例化的过程就是通过构造函数,来初始化设置标准定义中是具体指。比如在创建“小明”这个变量时,同时设置了他的名称,性别等信息。在变量中包含对对象的引用,所以可以通过变量操作对象,或引用对象的函数、属性。比如“小明”有手有脚(属性),可以抬头低头(函数)。
【原型、原型链】
什么是派生?
原型对象派生另一个对象,就是创建了原型对象的副本,占有独立的内存空间,并在副本上添加一个独特的属性和方法。
在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对象。
1
2
3
4
5
|
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指向函数。
1
2
3
4
5
6
7
8
9
|
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。
1
2
3
4
|
var a1 = { 'age' :1} var a2 = Object. create (a1); console.log(a2.__proto__); //Object { age: 1 } console.log(a2.constructor); //[ Function : Object] |
案例讲解
现在我们再来看案例。是不是清晰多了。js在线测验网站https://tool.lu/coderunner/
1
2
3
4
5
6
7
8
9
|
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中都不存在,所以显示未定义。
下面的留给读者自己理解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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语句内的不存在只有他们能访问的变量。只有在函数内存在局部变量。但不要因此就定义除函数以外所有使用{ }括起来的都是块级元素。对象使用{ }定义变量,对象内是属性的定义,不是变量定义,所以不存在作用域的说法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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属性是空字符串),创建闭包的常见方式就是在一个函数内部创建另一个函数。闭包保存的是整个变量的对象。
闭包的作用:在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量,这时灵活方便的闭包就派上用场,我们知道当一个函数被调用时就会创建一个执行环境及相应的作用域链,那么闭包就会沿着作用域链向上获取到开发者想要的变量及元素。
闭包灵活方便,也可以实现封装,这样就只能通过对象的特定方法才能访问到其属性。但是,不合理的使用闭包会造成空间的浪费,内存的泄露和性能消耗。
当函数被创建,就有了作用域,当被调用时,就有了作用域链,当被继承时就有了原型链,当需要获取作用域链或原型链上的变量或值时,就有了闭包。
js原型、原型链、作用链、闭包全解的更多相关文章
- js系列教程1-数组操作全解
全栈工程师开发手册 (作者:栾鹏) 快捷链接: js系列教程1-数组操作全解 js系列教程2-对象和属性全解 js系列教程3-字符串和正则全解 js系列教程4-函数与参数全解 js系列教程5-容器和算 ...
- js系列教程2-对象、构造函数、对象属性全解
全栈工程师开发手册 (作者:栾鹏) 快捷链接: js系列教程1-数组操作全解 js系列教程2-对象和属性全解 js系列教程3-字符串和正则全解 js系列教程4-函数与参数全解 js系列教程5-容器和算 ...
- Javascript-我对作用链、闭包、原型及原型链的理解
Javascript-基础概念总结(2) 最近学习一些javascript基础知识,也解决了很多之前的疑惑,记得第一次被问及怎样理解闭包时,我的回答是:就是类似于封装吧!现在想想是有多白痴,学习技术是 ...
- js javascript 原型链详解
看了许多大神的博文,才少许明白了js 中原型链的概念,下面给大家浅谈一下,顺便也是为了巩固自己 首先看原型链之前先来了解一下new关键字的作用,在许多高级语言中,new是必不可少的关键字,其作用是为了 ...
- 详聊js中的原型/原型链
先以一段简单的代码为例: function Dog(params){ this.name = param.name; this.age = param.age; this.ba ...
- 前端基本知识(二):JS的原型链的理解
之前一直对于前端的基本知识不是了解很详细,基本功不扎实,但是前端开发中的基本知识才是以后职业发展的根基,虽然自己总是以一种实践是检验真理的唯一标准,写代码实践项目才是唯一,但是经常遇到知道怎么去解决这 ...
- JS中原型链的理解
new操作符具体干了什么呢?其实很简单,就干了三件事情. var obj = {}; obj.__proto__ = Base.prototype; Base.call(obj); 第一行,我们创建了 ...
- js奥义:原型与原型链(1)
要弄懂原型链,首先应先明白prototype原型对象.__proto__.对象三者之间的关系. 引入构造函数的相关定义: 构造函数是一种比较特殊的函数,用于批量实例化对象.通俗一点说,构造函数是用于生 ...
- 攻略前端面试官(三):JS的原型和原型链
本文在个人主页同步更新~ 背就完事了 介绍:一些知识点相关的面试题和答案 使用姿势:看答案前先尝试回答,看完后把答案收起来检验成果~ 面试官:什么是构造函数 答:构造函数的本质是一个普通函数,他的特点 ...
随机推荐
- 【Jmeter自学】jmeter实战-其他请求和总结(七)
FTP测试 步骤:其他的结果树等跟http请求一样 mysql数据库测试 JDBC请求 Jmeter-分布式性能测试 jmeter结果分析:
- TessorFlow学习 之 神经网络的构建
1.建立一个神经网络添加层 输入值.输入的大小.输出的大小和激励函数 学过神经网络的人看下面这个图就明白了,不懂的去看看我的另一篇博客 def add_layer(inputs , in_size , ...
- General error: 24374 OCIStmtFetch: ORA-24374: define not done before fetch or execute and fetch
问题 $sql='insert into "test"("id") values(4)'; $res=$this->conn->query($sql ...
- oracle 问题
OCIEnvCreate 失败,返回代码为 -1,但错误消息文本不可用. 客户端文件没复制全 ORA-01017: invalid username/password; logon denied == ...
- 轻松解决oracle11g 空表不能exp导出的问题。
解决方法: 1插入一条数据(或者再删除),浪费时间,有时几百张表会累死的.2创建数据库之前使用代码: Sql代码 alter system set deferred_segment_creation ...
- java中的byte
8 bit (位) = 1 Byte (字节) java中的byte就是Byte 1024 Byte = 1KB 1024 KB = 1MB 1:“字节”是byte,“位”是bit : 2: 1 by ...
- leetcode218
from heapq import * class Solution: def getSkyline(self, LRH): skyline = [] i, n = 0, len(LRH) liveH ...
- MBR (主引导记录)
概念 主引导记录(MBR,Main Boot Record)是位于磁盘最前边的一段引导(Loader)代码.它负责磁盘操作系统(DOS)对磁盘进行读写时分区合法性的判别.分区引导信息的定位,它由磁盘操 ...
- vue格式化显示json数据
已经是json格式数据的,直接用标签 <pre></pre>展示. 参考:https://www.jianshu.com/p/d98f58267e40
- 【JEECG技术文档】JEECG 组织机构导入V3.7
1.功能介绍 组织机构导入 提供组织机构模版导入功能,使用户更快速的创建组织机构 要使用组织机构导入功能需要完成以下步骤: 1. 下载模版excel 2. 填写组织机构信息 3. 点击导入-选择文件- ...