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);  //未定义
 
__proto__
protoType

【作用域】

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原型、原型链、作用链、闭包全解的更多相关文章

  1. js系列教程1-数组操作全解

    全栈工程师开发手册 (作者:栾鹏) 快捷链接: js系列教程1-数组操作全解 js系列教程2-对象和属性全解 js系列教程3-字符串和正则全解 js系列教程4-函数与参数全解 js系列教程5-容器和算 ...

  2. js系列教程2-对象、构造函数、对象属性全解

    全栈工程师开发手册 (作者:栾鹏) 快捷链接: js系列教程1-数组操作全解 js系列教程2-对象和属性全解 js系列教程3-字符串和正则全解 js系列教程4-函数与参数全解 js系列教程5-容器和算 ...

  3. Javascript-我对作用链、闭包、原型及原型链的理解

    Javascript-基础概念总结(2) 最近学习一些javascript基础知识,也解决了很多之前的疑惑,记得第一次被问及怎样理解闭包时,我的回答是:就是类似于封装吧!现在想想是有多白痴,学习技术是 ...

  4. js javascript 原型链详解

    看了许多大神的博文,才少许明白了js 中原型链的概念,下面给大家浅谈一下,顺便也是为了巩固自己 首先看原型链之前先来了解一下new关键字的作用,在许多高级语言中,new是必不可少的关键字,其作用是为了 ...

  5. 详聊js中的原型/原型链

    先以一段简单的代码为例: function Dog(params){     this.name = param.name;     this.age = param.age;     this.ba ...

  6. 前端基本知识(二):JS的原型链的理解

    之前一直对于前端的基本知识不是了解很详细,基本功不扎实,但是前端开发中的基本知识才是以后职业发展的根基,虽然自己总是以一种实践是检验真理的唯一标准,写代码实践项目才是唯一,但是经常遇到知道怎么去解决这 ...

  7. JS中原型链的理解

    new操作符具体干了什么呢?其实很简单,就干了三件事情. var obj = {}; obj.__proto__ = Base.prototype; Base.call(obj); 第一行,我们创建了 ...

  8. js奥义:原型与原型链(1)

    要弄懂原型链,首先应先明白prototype原型对象.__proto__.对象三者之间的关系. 引入构造函数的相关定义: 构造函数是一种比较特殊的函数,用于批量实例化对象.通俗一点说,构造函数是用于生 ...

  9. 攻略前端面试官(三):JS的原型和原型链

    本文在个人主页同步更新~ 背就完事了 介绍:一些知识点相关的面试题和答案 使用姿势:看答案前先尝试回答,看完后把答案收起来检验成果~ 面试官:什么是构造函数 答:构造函数的本质是一个普通函数,他的特点 ...

随机推荐

  1. this绑定丢失的问题

    在之前的一篇文章<this绑定>中已经说过this绑定的四种情况,也谈到了this绑定丢失的问题,但是没有解释为什么会出现this绑定的丢失,最近在多次阅读关于this绑定方面的文章之后, ...

  2. gentoo eclipse swt

    最近学习使用 eclipse rcp 来做一些插件. 首先下载安装 eclipse-rcp,然后安装 swt emerge -av swt 安装完成以后, 在 /usr/portage/distfil ...

  3. redis 主从配置和集群配置

    主从配置 |  集群配置 redis主从 主从配置原因: 1.到达读写分离,读的操作和写操作比例10 : 1读数据频繁,写数据次数少,这样可以配置1个master数据库用来写数据,配置多个slave从 ...

  4. SLD Related Gateway Serivces Unavaliable

    SAP NW 7.4 default switched on the ACL (access control list) in gateway service, so only local acces ...

  5. openstack provider self-service network subnet 创建

  6. tkinter面板切换

  7. 最近玩了下linux下的lampp注意一些使用

    最近玩了下linux下的lampp注意一些使用 1 配置文件 /opt/lampp/etc 2 一些命令 mysql命令 /opt/lampp/bin/mysql php命令 /opt/lampp/b ...

  8. Linux:常用命令【转载】

    转载于:https://www.cnblogs.com/yjd_hycf_space/p/7730690.html 系统信息 arch 显示机器的处理器架构(1) uname -m 显示机器的处理器架 ...

  9. spark 运行架构

    spark 运行架构基本由三部分组成,包括SparkContext(驱动程序),ClusterManager(集群资源管理器)和Executor(任务执行过程)组成. 其中SparkContext负责 ...

  10. 2018年1月21日--2月4日 NAS

    二十号去比赛时,与同事闲聊时说起家庭服务器,后来搜到nas(网络附着存储器),找到freenas,突然觉得很有用,手机拍了大量的照片视频,存储在电脑,已经换过几次硬盘了,对于这些珍贵的资料,万一硬盘坏 ...