原型链闭包面试题,原型链的理解,原型链和
再探原型和闭包
最近看见一篇关于原型和闭包的博文,十分精彩,传送门:深入理解javascript原型和闭包(完结)-王福朋。看完有种茅舍顿开的感觉,之前自己总结的原型链和闭包,实在是有失偏颇,因此在参考这篇文章,重新整理了原型链和作用域链的知识,加以巩固。
原型链: . _ _ proto _ _ 属性 首先,我们需要牢记两点:①__proto__和constructor属性是对象所独有的;② prototype属性是函数所独有的。但是由于JS中函数也是一种对象,所以函数也拥有__proto__和constructor属性,这点是致使我们产生困惑的很大原因之一。上图有点复杂,我们把它按照属性分别拆开,然后进行分析: 第一,这里我们仅留下 proto 属性,它是对象所独有的,可以看到__proto__属性都是由一个对象指向一个对象,即指向它们的原型对象(也可以理解为父对象),那么这个属性的作用是什么呢?它的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象(可以理解为父对象)里找,如果父对象也不存在这个属性,则继续往父对象的__proto__属性所指向的那个对象(可以理解为爷爷对象)里找,如果还没找到,则继续往上找…直到原型链顶端null(可以理解为原始人。。。),再往上找就相当于在null上取值,会报错(可以理解为,再往上就已经不是“人”的范畴了,找不到了,到此结束,null为原型链的终点),由以上这种通过__proto__属性来连接对象直到null的一条链即为我们所谓的原型链。
prototype属性 prototype属性,它是函数所独有的,它是从一个函数指向一个对象。它的含义是函数的原型对象,也就是这个函数(其实所有函数都可以作为构造函数)所创建的实例的原型对象,由此可知:f1.proto === Foo.prototype,它们两个完全一样。那prototype属性的作用又是什么呢?它的作用就是包含可以由特定类型的所有实例共享的属性和方法,也就是让该函数所实例化的对象们都可以找到公用的属性和方法。任何函数在创建的时候,其实会默认同时创建该函数的prototype对象。
constructor属性 constructor属性也是对象才拥有的,它是从一个对象指向一个函数,含义就是指向该对象的构造函数,每个对象都有构造函数(本身拥有或继承而来,继承而来的要结合__proto__属性查看会更清楚点,如下图所示),从上图中可以看出Function这个对象比较特殊,它的构造函数就是它自己(因为Function可以看成是一个函数,也可以是一个对象),所有函数和对象最终都是由Function构造函数得来,所以constructor属性的终点就是Function这个函数。
1. 原型链
1.1. 原型
所有的对象都是由其构造函数创建,基础对象最基本的构造函数是Obecjt()。每个对象都有一个" __ proto__ "的属性,指向该对象构造函数的原型,该属性也被称为隐式原型。每个函数都有一个"prototype"的属性,表示这个构造函数的原型,函数的原型实际上是一个对象,并且该对象有一个construct的属性,指向构造函数本身。
同时,由于函数也是对象,所有函数的构造函数都是Function()。函数的隐式原型就是Function()的显式原型。 甚至连Object()函数的构造函数都是Function(),假设存在构造函数Aoo()和其实例化对象a,存在下面的关系:
a.__proto__ == Foo.prototype; // 对象的隐式原型指向其构造函数的显式原型
Foo.__proto__ == Function.prototype; // 函数的构造函数是Function()
Foo.prototype.__proto__ == Object.prototype; // 原型也是一个对象,对象的基本构造函数是Object()
Object.__proto__ == Function.prototype; // Object()函数的构造函数也是Function()
Function.__proto__ == Function.prototype; // Function()函数是被自身创建的-_-
Function.prototype.__proto__ == Object.prototype; // Function()的原型也是对象
Object.prototype.__proto__ == null; // Object.prototype 是第一个对象,所有对象的基础属性和方法都源于此,所以他的隐式原型是null。
1.2. 继承
JS中的继承是通过原型链来体现的:访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着proto这条链向上找,这就是原型链。 如果将构造函数A的原型是构造函数B的实例对象,则A的实例对象就可以通过A.prototype获取到b的属性和方法(因为A.prototype == b)。 可以使用hasOwnProperty方法来判断一个属性到底是实例对象的还是其原型的。
可以通过使用instanceof来判断两个类是否存在继承关系,其规则为:沿着左操作数的proto这条线来找,同时沿着右操作数的prototype这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回true。如果找到终点还未重合,则返回false。通过上面的关系,就可以解释下面的问题了:
console.log(Object instanceof Function); // true
console.log(Function instanceof Function); // true
console.log(Function instanceof Object); // true
2. 闭包
2.1. 环境上下文
环境上下文,分为全局上下文和函数上下文:
- 全局上下文中的数据内容包括
- 普通变量声明
- 函数声明
- this
- 函数上下文中的数据内容除了上面三种情形,还包括
- 参数
- arguments
- 自由变量(指不在该上下文中声明的变量)
所谓的执行环境上下文,指的是:在执行代码之前,解析器会进行变量声明提前,也就是说会把这段代码将要用到的所有变量都事先拿出来,有的直接赋值了,有的先用undefined占个空。
- 变量、函数表达式声明,默认赋值为undefined占位;
- this——赋值;
- 函数语句声明——赋值;
2.2. 上下文栈
下面是代码执行的大致过程:
- 在加载程序时,已经确定了全局上下文环境以及执行上下文(变量声明提前等准备工作),并随着程序的执行对变量进行赋值。
- 在函数体的语句执行之前,arguments变量和函数的参数都已经被赋值。因此,函数每被调用一次,都会产生一个新的执行上下文环(即使是同一个函数(甚至是相同参数),在不同的调用下产生的上下文环境也是不一样的)。
- 当程序运行到将要调用函数的时候,会生成该函数的上下文环境,然后将此上下文压入上下文环境栈并设置为活动状态。
- 然后执行函数内部的代码,如果遇见函数调用则重复上述步骤:产生新的上下文->入栈并设置为活动状态->执行函数代码;
- 当这个函数调用完毕,则其上下文环境被销毁,从上下文环境栈弹出,此时程序又回到了其父作用域下的上下文环境,并在栈中将其置为活动状态
- 按照此过程进行,到最后上下文环境栈中只剩下了全局上下文环境(当然这是在没有闭包的情况下)
2.3. 函数内部的数据内容
上面提到,函数上下文中的数据内容,除了普通变量声明,函数声明和this之外,还多了参数,arguments对象和自由变量。
2.3.1. 自由变量
自由变量的定义:在fn函数中使用的变量x,却没有在A作用域中声明,对于fn函数作用域来说,x就是一个自由变量。函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。
var a = 10;
function fn(){
console.log(a);
}
function foo(f){
var a = 20;
f();
}
foo(fn); // 10
2.3.2. this
跟自由变量不同的是,函数内部的this取何值,是在函数真正被调用执行的时候确定的,因为函数定义的时候根本确定不了。
- 如果函数作为构造函数用,那么其中的this就代表它即将new出来的对象,在原型链中,this代表的也都是当前对象的值;但是如果直接把构造函数当作普通函数调用,则其的this会变成window
- 如果函数作为对象的一个属性时,并且作为对象的一个属性被调用时,函数中的this指向该对象;但是如果方法函数被赋值到了另一个变量中,并没有作为obj的一个属性被调用,那么this的值就是window,此时就无法在函数中使用this获取原对象的属性
- 当一个函数被call和apply调用时,this的值就取传入的对象的值
- 全局环境下,this永远是window,普通函数在调用时,其中的this也都是window
- 闭包函数中的this也是window。
2.4. 作用域
JS中只有全局作用域和函数作用域,并没有“块级作用域”的概念,函数作用域是在函数定义时生成的。作用域只是一个抽象的概念,其中没有变量。要通过作用域对应的执行上下文环境来获取变量的值。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。所以,如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中寻找变量的值。牢牢记住,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。
作用域内部声明的变量(包括参数)会覆盖掉外部的同名变量,这正是我们需要的。那么,程序是如何确定作用域下的某个上下文中所使用的自由变量呢?
前面提到过:函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。在fn函数中,取自由变量x的值时,要到创建fn函数的那个作用域中取,无论fn函数将在哪里调用。 如果跨了一步,还没找到呢?接着跨!一直跨到全局作用域为止。要是在全局作用域中都没有找到,那就是真的没有了。
这个一步一步“跨”来寻找自由变量的路线,就是我们常说的“作用域链”。
2.5. 闭包函数
了解了作用域链,理解闭包就十分轻松了。关于闭包,有一个不那么精准的定义:一个作为函数返回值或者函数参数的函数。
// 作为函数返回值
function fn(){
var a = 100;
return function foo(x){
if (x > a){
console.log("more");
}else {
console.log("less or equal");
}
}
}
var a = 10;
var f = fn();
f(50); // "less or equal"
// 作为参数
var a = 10;
var fn = function(x){
if (x > a){
console.log("more");
}else {
console.log("less or equal");
}
}
!(function(f){
// 这里的闭包是形参f而不是实参fn,fn只是一个普通的函数表达式声明的函数
var a = 100;
f(50); // "more"
})(fn);
在前面的上下文栈中提到:当函数调用结束,其执行上下文将从上下文栈中弹出并被销毁,其中的变量也随之被销毁,这里的例外就是闭包。
由于闭包函数是在函数内部定义的函数(函数可以创建一个独立的作用域),因此如果在闭包中使用其定义函数上下文中的自由变量,或者在其他地方调用该函数,则必须保证其定义函数的执行上下文仍然存在(如果跟普通的函数一样调用结束就被销毁,则就无法找到其中的数据内容了。),所以如果函数上下文中存在闭包,则在调用结束之后不会被销毁,而是保存在上下文栈中(尽管活动状态已经被切换为上一层),而之所以说使用闭包会增加内存开销,就是这个原因。
总之,理解闭包,弄清楚环境上下文和作用域链是十分必要的,此外,牢记“函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域 ”。
3. 最后
这个世界上有无数优秀且勤奋的人,然而我并不是其中的一个。感谢这么多前辈愿意在互联网上分享自己的学习笔记和心得,我现在能做的就是抓紧学习,希望有朝一日也能为社区的发展贡献绵薄之力。加油吧!
[SCSS进阶](/article/SCSS进阶)[初识Vue](/article/初识Vue)