JS-Function Advance
-
原型和原型链
原型
—每个函数对象都具有
prototype
属性,它默认指向一个Object空对象(即为原型对象);—原型对象中有一个属性
constructor
,它指向函数对象本身;简单理解为相互引用关系,可以通过prototype
指向constructor
,然后constructor
又可以指向函数本身对象;—给原型对象添加属性(一般都是方法),这样函数的所有实例对象自动拥有原型中的属性(方法);
显式原型与隐式原型
—显式原型(属性):每个函数都有一个
prototype
,默认指向一个空的Object
对象(它的内存内容是地址值数据);—隐式原型(属性):每个实例对象(包括
Function
的实例对象-函数对象)都有一个__proto__
;—实例对象的隐式原型的值为其对应的构造函数的显式原型的值;
—一个实例对象的原型对象可能不止一个;
—函数的
prototype
属性在定义函数时自动添加的;可以简单的理解为,创建函数的时候,内部语句为:this.prototype={}
;—对象的
__proto__
属性在创建对象时自动添加的,默认值为构造函数的prototype
属性值;可以简单地理解为在创建对象的时候,内部语句:this.__proto__=构造函数.prototype
;—程序员可以直接操作显式原型,但不能直接操作隐式原型(ES6之前)
—prototype
是函数才有的属性,__proto__
是每个对象都有的属性原型链
—访问一个对象的属性时,现在自身属性中查找,找到返回;一般函数对象都在原型链中遍历
—如果在自身属性中查找不到,就沿着
__proto__
这条原型链向上查找,找到返回;—
Object
的原型对象为null
,即Object.__proto__==null
,即其为原型链的尽头,如果最终在原型链尽头都没有找到一个对象,则返回undefined
;—隐式原型链:查找对象的属性或者方法;如果赋值不会进行原型链的遍历;
—所有函数的
__proto__
都是相同的,因为它们都等于显式原型的prototype
;—函数相当于
Function
的实例,当Function = new Function()
时,其自身Function
的隐式原型__proto__
等于Function
的显式原型prototype
;—函数的显示原型指向对象,默认是空Object实例对象,但是Object不满足以上;
—所有函数都是
Function的实例
,包括Function
是它自身的实例;即Function.__proto__===Function.prototype
—
Object
的原型对象是原型链的尽头;即Object.prototype .__proto__=null
原型链属性问题
—读取对象属性值时,会自动到原型链中查找;
—设置对象属性值时,不会查找原型链,如果当前对象中没有此属性值,直接添加此属性并设置此值;
—一般情况下,方法一般定义在原型中,而属性通过构造函数定义在对象本身上;
instanceof
—判断表达式:
A instanceof B
;A为实例对象,B为构造函数;—如果B函数的显示原型对象在A实例的隐式原型链上,就返回
true
,否则返回false
<script> //Object.__proto__=Function.prototype console.log(Object instanceof Function);//返回true //Object.__proto__——>Function.prototype——>Function.__proto__——>Object.prototype console.log(Object instanceof Object);//返回true //Function.__proto__——>Object.prototype console.log(Function instanceof Object);//返回true //Function.__proto__==Function console.log(Function instanceof Function);//返回true function Foo(){} //Object.__proto__——>Function.prototype——>Object.prototype——>null console.log(Object instanceof Foo);//返回false </script>
原型面试题
<script> function A(){} A.prototype.n=1; var b = new A(); A.prototype = { n:2; m:3; }; var c = new A(); //分别返回1,undefined,2,3 console.log(b.n,b.m,c.n,c.m); </script>
<script> var F = function(){}; Object.prototype.a = function(){ console.log('a()'); }; Function.prototype.b = function(){ console.log('b()'); } var f = new F(); //返回`a()` f.a(); //报错 b() is not a function f.b(); //返回`a()` F.a(); //返回`b()` F.b(); </script>
-
执行上下文与执行上下文栈
变量提升与函数提升(声明提前)
—变量声明提前:通过
var
定义(声明)的变量,在定义语句之前就可以访问到;其值为undefined
;—函数声明提升:通过
function
声明的函数,在之前就可以调用;其值为函数对象;—变量先提升,函数在提升;
—执行函数定义:创建函数对象;执行函数:调用函数;
执行上下文
—全局执行上下文
-
在执行全局代码前将
window
确定为全局执行上下文; -
对全局数据进行预处理;
—
var
定义的全局变量—>undefined
,添加为window
属性;—
function
声明的全局函数—>赋值函数对象,添加为window
的方法;—
this
赋值为window
; -
开始执行全局代码;
—函数执行上下文
-
在调用函数时,准备执行函数体之前,创建对应的函数执行上下文对象;
-
对局部数据进行预处理
—形参变量—>赋值实参—>添加为函数执行上下文的属性;
—
arguments
—>赋值实参列表,添加为函数执行上下文的属性;—
var
定义的全局变量—>undefined
,添加为函数执行上下文属性;—
function
声明的全局函数—>赋值函数对象,添加为函数执行上下文的方法;—
this
赋值调用函数的对象; -
开始执行函数体代码;
—函数执行上下文对象不是真实存在的,虚拟的,存在于栈中;全局变量和局部变量全部保存在栈内存中,在栈内存中也分为全局区域和局部区域,函数执行完毕后,局部区域会自动释放;
—函数只有在调用时才会产生执行上下文对象,声明不会产生执行上下文对象;
执行上下文栈
—在全局代码执行前,JS引擎就会创建一个栈来存储管理所有的执行上下文对象;
—队列是先进先出,栈是后进先出;
—在全局执行上下文(
window
)确定后,将其添加到栈中(压栈);—在函数执行上下文创建后,将其添加到栈中(压栈);
—在当前所有函数执行完后,将栈顶的对象移除(出栈);
—当所有代码执行完后,栈中只剩下
window
;栈底永远并且只能是window
;—全局执行上下文只有一个,总共执行上下文数量为函数调用个数+1,注意函数调用可能会嵌套函数;
执行上下文面试题
<script> //输出undefined console.log('global begin:'+i) var i = 1; foo(1); function foo(){ if(i==4){ return; } //输出1 console.log(`foo() begin:`+i); //递归调用:函数内部调用自己 //必须有一个终止条件,否则陷入死循环 foo(i+1) console.log(`foo() end:`+i); } //输出1 console.log(`global end:`+i); //依次输出: //gb:undefined //fb:1 //fb:2 //fb:3 //fe:3 //fe:2 //fe:1 //ge:1 //一共执行了五个上下文对象 </script>
<script> //先执行变量提升,在执行函数提升 function a(){} var a; console.log(typeof a)//返回`function` // 如果返回1,则说明if语句执行了 //如果返回undefined,说明没有执行 if(!(b in window)){ var b = 1; } console.log(b);//返回undefined var c = 1 function c(c){ console.log(c) var c = 3 } c(2)//报错 c is not a function //以上代码可以写成 var c function c(c){ console.log(c) var c = 3 } //变量赋值覆盖了函数执行 c=1 c(2) </script>
-
-
作用域和作用域链
—作用域为一个代码段所在的区域,它是静态的,相对于上下文对象,在编写代码时就确定作用域了;一般变量都在作用域链中遍历
—分类:全局作用域和函数作用域,没有块作用域(ES6有了);
—作用域可以隔离变量,不同作用域下同名变量就不会有冲突;
—作用域数量为定义函数个数+1;
作用域和执行上下文对象
-
区别1
—全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了,而不是函数调用时;
—全局执行上下文是在全局作用域确定之后,js代码马上执行之前创建的;
—函数执行上下文是在调用函数时,函数体代码执行之前创建;
-
区别2
—作用域是静态的,只要函数定义好了就一直存在,且不会再变化;
—执行上下文是动态的,调用函数时创建,函数调用结束时就会自动释放;
-
两者联系
—执行上下文对象是从属所在的作用域;
—全局上下文执行对象环境:全局作用域;
—函数上下文执行对象环境:对应的函数作用域;
作用域链
—多个上下级关系的作用域形成的链,它的方向是由下向上,从内向外;
—查找一个变量的查找规则:
在当前作用域的执行上下文中查找对应的属性,如果有则直接返回,否则进入上一级作用域的执行上下文对象中查找对应的属性,如果有则直接返回,否则继续进入上一级,直到全局作用域,如果找不到就抛出异常;
<script> var x = 10 function fn(){ console.log(x) } function show(fn){ var x = 20 fn() } show(fn);//返回10 </script>
<script> var fn = function(){ console.log(fn) } fn()//返回函数对象 var obj={ fn2:function(){ //通过作用域寻找变量 console.log(fn2) //首先通过作用域查找变量 //如果要想输出fn2这个函数对象 console.log(this.fn2); } } obj.fn2()//报错 </script>
-
-
闭包
参考网址:https://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
<script> //点击按钮,提示“点击的是第n个按钮” //遍历加监听 var btns = document.getElementsByTagName('button'); for(var i = 0,length = btns.length;i<length;i++){ var btn = btn[i] //将btn所对应的下标保存再btn上 btn.index = i btn.onclick = function(){ alert('第'+(this.index+1)+'个') } } //利用闭包 for(var i = 0,length = btns.length;i<length;i++){ ( function(i){ var btn = btns[j] //内部函数引用了外部函数形参i btn.onclick = function(){ alert('第'+(i+1)+'个') } } )(i) } </script>
—当一个嵌套的内部子函数引用了嵌套的外部父函数时,就产生了闭包;
—可以通过chrome调试工具查看闭包,绝大部分人认为闭包是嵌套的内部函数,少数人认为闭包是包含被引用变量(函数)的对象,这样闭包是存在于嵌套的内部函数中;
—产生闭包的条件:函数嵌套、内部函数引用了外部函数的数据(变量/函数)、调用外部函数;
—执行内部函数定义就会产生闭包,不用调用内部函数;当使用函数声明创建函数时,会函数提升,则真正函数对象的创建会比定义时早;而使用函数表达式创建函数,不会实现函数提升,则只有函数执行到表达式才会执行函数定义;
—闭包个数,为含有嵌套函数的外部函数调用的次数;
常见的闭包
- 将函数作为另一个函数的返回值;
<script> function fn1(){ var a = 2 function fn2(){ a++ console.log(a) } return fn2 } //fn1()返回fn2函数对象 //这里外部函数只调用了一次,所以只产生了一个闭包 //闭包存在的原因是因为f赋值了fn2对象的地址值 //栈中的fn2变量释放了,但是堆中的函数对象由于f的引用没有释放 var f = fn1() //f是调用内部函数,不是外部函数 f()//返回3 f()//返回4 </script>
- 将函数作为实参传递给另一个函数调用
<script> function showDelay(msg,time){ setTimeout(function(){ //内部函数引用了外部函数的变量,因此有闭包存在 alert(msg) },time) } showDelay('atguigu',2000) </script>
闭包作用
—延长了局部变量的生命周期,使用函数内部的变量在函数执行后,仍然存活在内存中;
—让函数外部可以操作(读写)到函数内部的数据(变量/函数);
—闭包的生命周期:在嵌套内部函数声明定义执行(创建了函数对象)完就产生闭包(不在调用时),在嵌套的内部函数成为垃圾对象(指向
null
)时就死亡了;—函数执行完毕后,函数内部声明的局部变量一般不存在,但是存在于闭包中的变量才能存在;
—一般情况下,函数外部不能直接访问函数内部的局部变量,但是可以通过闭包让外部来操作函数内部局部变量;
闭包应用-自定义JS模块
—具有特定功能的JS文件;
—将所有的数据和功能都封装在一个函数内部(私有的);
—只向外暴露一个包含n个方法的对象或函数,可以在JS模块中使用
return
返回对象或方法,如果返回n个的话,需可以使用对象容器进行存储;例如下面一个自定义JS模块:<script> //需要在外部执行函数 function myModule(){ //私有数据 var msg = `My data` function doSomething(){ console.log(msg.toUpperCase()) } function doOtherthing(){ console.log(msg.toLowerCase) } //向外暴露的对象(给外部使用的方法) return{ doSomething:doSomething, doOtherthing:doOtherthing } } </script>
<script> //立即执行函数表达式 (function myModule(window){ //私有数据 var msg = `My data` function doSomething(){ console.log(msg.toUpperCase()) } function doOtherthing(){ console.log(msg.toLowerCase) } //添加至window对象中,向外暴露方法,外部不需要调用,直接调用方法即可 window.myModule2{ doSomething:doSomething, doOtherthing:doOtherthing } })(window) </script>
—模块的使用者,只需要通过模块暴露的对象调用方法来实现对应的功能;
闭包的缺点:内存溢出与内存泄漏
—内存溢出:一种程序运行出现的错误,当程序运行需要的内存超过了剩余的内存时,就抛出了内存溢出的错误;
—内存泄漏:占用的内存没有及时释放,会导致可用内存空间不足;内存泄漏积累多了就容易导致内存溢出;常见的内存泄漏:(1)意外的全局变量,例如在函数中未用
var
定义的变量;(2)未及时清理循环定时器setInterval
;(3)闭包;—函数执行完后,函数内的局部变量没有释放,占用内存时间会变长;影响网页的性能;
—容易造成内存泄漏,占用内存不释放;
—解决方法:让内部函数成为垃圾对象,垃圾回收机制回收闭包;尽量减少闭包的使用;
闭包面试题
<script> //这个里面没有产生闭包,虽然有函数嵌套,但是内部函数未引用外部函数的变量 var name = "The Window" var object = { name:"My Object", getNameFunc:function(){ return function(){ return this.name } } } //object.getNameFunc()返回的一个函数,直接调用这个函数,this指向window alert(object.getNameFunc()())//返回The Window //这里面产生了闭包,因为内部函数引用了外部函数的变量 var name2 = "The Window" var object2={ name2:"my Obejct", getNameFunc:function(){ var that = this return function(){ return that.name2 } } } //that保存的是调用getNameFunc方法下的this,因此this指向object2 alert(object2.getNameFunc()())//返回My Object </script>
<script> function fun(n,o){ console.log(o) return{ fun:function(m){ return fun(m,n) } } } var a = fun(0) //返回undefined a.fun(1)//返回0 a.fun(2)//返回0 a.fun(3)//返回0 var b = fun(0).fun(1).fun(2).fun(3)//返回undefined、0、1、2 var c = fun(0).fun(1) c.fun(2) c.fun(3)//返回undefined、0、1、1 </script>
如果查找变量找不到会报错,查找对象属性如果找不到返回
undefined