JS-FunctionAdvance

本文深入探讨JavaScript中的原型、原型链、显式原型与隐式原型的概念,以及如何查找对象属性。同时,讲解执行上下文、变量提升与函数提升,强调全局执行上下文和函数执行上下文的区别。最后,讨论作用域、作用域链和闭包,解释闭包的产生、作用及应用场景,并指出闭包可能导致的内存问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JS-Function Advance

  1. 原型和原型链

    原型

    —每个函数对象都具有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>
    
  2. 执行上下文与执行上下文栈

    变量提升与函数提升(声明提前)

    变量声明提前:通过var定义(声明)的变量,在定义语句之前就可以访问到;其值为undefined

    函数声明提升:通过function声明的函数,在之前就可以调用;其值为函数对象;

    变量先提升,函数在提升;

    —执行函数定义:创建函数对象;执行函数:调用函数;

    执行上下文

    全局执行上下文

    1. 在执行全局代码前将window确定为全局执行上下文;

    2. 对全局数据进行预处理;

      var定义的全局变量—>undefined,添加为window属性;

      function声明的全局函数—>赋值函数对象,添加为window的方法;

      this赋值为window

    3. 开始执行全局代码;

    函数执行上下文

    1. 在调用函数时,准备执行函数体之前,创建对应的函数执行上下文对象

    2. 对局部数据进行预处理

      —形参变量—>赋值实参—>添加为函数执行上下文的属性;

      arguments—>赋值实参列表,添加为函数执行上下文的属性;

      var定义的全局变量—>undefined,添加为函数执行上下文属性;

      function声明的全局函数—>赋值函数对象,添加为函数执行上下文的方法;

      this赋值调用函数的对象;

    3. 开始执行函数体代码;

    函数执行上下文对象不是真实存在的,虚拟的,存在于栈中;全局变量和局部变量全部保存在栈内存中,在栈内存中也分为全局区域和局部区域,函数执行完毕后,局部区域会自动释放

    —函数只有在调用时才会产生执行上下文对象,声明不会产生执行上下文对象;

    执行上下文栈

    —在全局代码执行前,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>
    
  3. 作用域和作用域链

    —作用域为一个代码段所在的区域,它是静态的,相对于上下文对象,在编写代码时就确定作用域了;一般变量都在作用域链中遍历

    —分类:全局作用域和函数作用域,没有块作用域(ES6有了);

    作用域可以隔离变量,不同作用域下同名变量就不会有冲突;

    作用域数量为定义函数个数+1

    作用域和执行上下文对象

    1. 区别1

      —全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了,而不是函数调用时

      —全局执行上下文是在全局作用域确定之后,js代码马上执行之前创建的;

      —函数执行上下文是在调用函数时,函数体代码执行之前创建;

    2. 区别2

      作用域是静态的,只要函数定义好了就一直存在,且不会再变化;

      执行上下文是动态的,调用函数时创建,函数调用结束时就会自动释放

    3. 两者联系

      执行上下文对象是从属所在的作用域

      —全局上下文执行对象环境:全局作用域;

      —函数上下文执行对象环境:对应的函数作用域;

    作用域链

    —多个上下级关系的作用域形成的链,它的方向是由下向上,从内向外;

    查找一个变量的查找规则

    在当前作用域的执行上下文中查找对应的属性,如果有则直接返回,否则进入上一级作用域的执行上下文对象中查找对应的属性,如果有则直接返回,否则继续进入上一级,直到全局作用域,如果找不到就抛出异常;

    <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>
    
  4. 闭包

    参考网址: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调试工具查看闭包,绝大部分人认为闭包是嵌套的内部函数,少数人认为闭包是包含被引用变量(函数)的对象,这样闭包是存在于嵌套的内部函数中;

    —产生闭包的条件:函数嵌套、内部函数引用了外部函数的数据(变量/函数)、调用外部函数

    执行内部函数定义就会产生闭包,不用调用内部函数;当使用函数声明创建函数时,会函数提升,则真正函数对象的创建会比定义时早;而使用函数表达式创建函数,不会实现函数提升,则只有函数执行到表达式才会执行函数定义;

    —闭包个数,为含有嵌套函数的外部函数调用的次数;

    常见的闭包

    1. 将函数作为另一个函数的返回值
    <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>
    
    1. 将函数作为实参传递给另一个函数调用
    <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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值