js高级(函数高级)

276 阅读13分钟

image.png

原型与原型链

image.png

1. 函数的prototype属性

  • 每个函数都有一个prototype属性,它默认指向一个Object空对象(即称为: 原型对象)

  • 原型对象中有一个属性constructor,它指向函数对象

image.png

image.png

2. 给原型对象添加属性(一般都是方法) —供实例对象访问

  • 作用:函数的所有实例对象自动拥有原型中的属性(方法)

image.png

3.显式原型与隐式原型的关系

  • 每个函数function都有一个prototype,即显式原型。

  • 每个实例对象都有一个__proto__,可称为隐式原型。

  • 对象的隐式原型的值为其对应构造函数的显式原型的值。(Fn.prototype===fn.proto)

image.png

  • 内存结构

image.png

  • 总结:

    • 函数的prototype属性:在定义函数时自动添加的,默认值是一个空Object对象(空指我们未主动添加东西)

    • 对象的__proto__属性:创建对象时自动添加的,默认值为构造函数的prototype属性值

      • prototype和__proto_** _**都是引用类型,存的都是地址值
    • 原型对象即为当前实例对象的父对象

    • 程序员能直接操作显式原型,但不能直接操作隐式原型(ES6之前)。

      • 通过显式原型添加方法:Fn.prototype.test=function(){}

4.原型链

概念: - 访问一个对象的属性时, - 先在自身属性中查找,找到返回 - 如果没有, 再沿着__proto__这条链向上查找,找到返回 - 如果最终没找到,返回undefined

image.png

  • 别名: 隐式原型链

  • 作用: 查找对象的属性(方法)

image.png

构造函数/原型/实体对象的关系

image.png

构造函数/原型/实体对象的关系2

image.png

函数都有prototype属性,只是说普通函数的prototype属性的没有意义,当做构造函数使用的函数的prototype属性才有意义。

所有的函数对象都是Function函数的实例对象,因此这些函数的隐性原型属性_proto_都指向Function原型对象。然后这些函数都有各自自己的显性原型属性prototype,和由通过他们自己创建的实例对象的隐性原型属性地址值相等。

补充:

1、所有函数的显示原型指向的对象默认是空Object的实例对象(但Object不满足)。

  • 函数的prototype —>空Object的实例对象,该空Object的实例对象的__proto__—>Object
  • Object(是一个函数)的prototype —>Object(此处的Object.proto===null)

2、所有函数都是Function的实例(包含Function本身)。

3、Object的原型对象是原型链的尽头 null

image.png

5.原型链属性问题

  1. 读取对象的属性值时: 会自动到原型链中查找
  2. 设置对象的属性值时: 不会查找原型链, 如果当前对象中没有此属性, 直接添加此属性并设置其值
  3. 方法一般定义在原型中, 属性一般通过构造函数定义在对象本身上

image.png

image.png

6.探索instanceof

    1. instanceof是如何判断的?

    • 表达式: A instanceof B(A为实例对象,B是构造函数对象)
    • 如果B函数的显式原型对象在A对象的原型链上, 返回true, 否则返回false
    1. Function是通过new自己产生的实例

案例1

//案例1
function Foo() {}
var f1 = new Foo()
console.log(f1 instanceof Foo) // true
console.log(f1 instanceof Object) // true

image.png

案例2

//案例2
console.log(Object instanceof Function) // true
console.log(Object instanceof Object) // true
console.log(Function instanceof Function) // true
console.log(Function instanceof Object) // true

function Foo() {}
console.log(Object instanceof Foo) // false

image.png

7.原型面试题

image.png

image.png

执行上下文与执行上下文栈

变量提升与函数提升

  1. 变量声明提升
    • 通过var定义(声明)的变量, 在定义语句之前就可以访问到
    • 值: undefined
  2. 函数声明提升
    • 通过function声明的函数, 在之前就可以直接调用
    • 值: 函数定义(对象)
    • 如果是通过函数表达式定义一个函数,属于变量提升,不属于函数提升,不可直接调用
//函数声明
function fun(){
}
//函数表达式
var fun = function(){
}
  1. 问题: 变量提升和函数提升是如何产生的?
    • 先有变量提升, 再有函数提升
    • 请看面试题

执行上下文

  1. 代码分类(位置)
    • 全局代码
    • 函数代码
  2. 全局执行上下文
    • 在执行全局代码前将window确定为全局执行上下文

    • 对全局数据进行预处理

      • var定义的全局变量==>undefined, 添加为window的属性
      • function声明的全局函数==>赋值(fun), 添加为window的方法
      • this==>赋值(window)
    • 开始执行全局代码

    image.png
  3. 函数执行上下文
    • 在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象

    • 对局部数据进行预处理

      • 形参变量==>赋值(实参)==>添加为执行上下文的属性
      • arguments==>赋值(实参列表), 添加为执行上下文的属性
      • var定义的局部变量==>undefined, 添加为执行上下文的属性
      • function声明的函数 ==>赋值(fun), 添加为执行上下文的方法
      • this==>赋值(调用函数的对象)
    • 开始执行函数体代码

    image.png

执行上下文栈

  1. 在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象

  2. 在全局执行上下文(window)确定后, 将其添加到栈中(压栈)

  3. 在函数执行上下文创建后, 将其添加到栈中(压栈)

  4. 在当前函数执行完后,将栈顶的对象移除(出栈)

  5. 当所有的代码执行完后, 栈中只剩下window

image.png

image.png

image.png

面试题(妙啊)

/*

  测试题1: 先变量提升,再函数提升

  */

  function a() {}

  var a;

  console.log(typeof a)   //’function‘

  /*

  测试题2: 

   */

  if (!(b in window)) {

    var b = 1;

  }

  console.log(b)  //undefined

  /*

  测试题3: 

   */

  var c = 1

  function c(c) {

    console.log(c)

    var c = 3

  }

  c(2)  //报错!!! 秒啊!!!
  
  //以上代码相当于
  var c 
  function c(c) {
    console.log(c)
    var c = 3
  }
  c=1
  c(2)  
  //先变量提升,再函数提升,再赋值!!!

复习

  • 理解
    • 执行上下文: 由js引擎自动创建的对象, 包含对应作用域中的所有变量属性
    • 执行上下文栈: 用来管理产生的多个执行上下文
  • 分类:
    • 全局: window
    • 函数: 对程序员来说是透明的
  • 生命周期
    • 全局 : 准备执行全局代码前产生, 当页面刷新/关闭页面时死亡
    • 函数 : 调用函数时产生, 函数执行完时死亡
  • 包含哪些属性:
    • 全局 :
      • 用var定义的全局变量 ==>undefined
      • 使用function声明的函数 ===>function
      • this ===>window
    • 函数
      • 用var定义的局部变量 ==>undefined
      • 使用function声明的函数 ===>function
      • this ===> 调用函数的对象, 如果没有指定就是window
      • 形参变量 ===>对应实参值
      • arguments ===>实参列表的伪数组
  • 执行上下文创建和初始化的过程
    • 全局:
      • 在全局代码执行前最先创建一个全局执行上下文(window)
      • 收集一些全局变量, 并初始化
      • 将这些变量设置为window的属性
    • 函数:
      • 在调用函数时, 在执行函数体之前先创建一个函数执行上下文
      • 收集一些局部变量, 并初始化
      • 将这些变量设置为执行上下文的属性

作用域与作用域链

作用域

  1. 理解
    • 就是一块"地盘", 一个代码段所在的区域
    • 它是静态的(相对于上下文对象), 在编写代码时就确定了
  2. 分类
    • 全局作用域

    • 函数作用域

    • 没有块作用域(ES6有了)

image.png

  1. 作用
  • 隔离变量,不同作用域下同名变量不会有冲突

作用域与执行上下文

  1. 区别1
    • 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时
    • 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
    • 函数执行上下文环境是在调用函数时, 函数体代码执行之前创建
  2. 区别2
    • 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
    • 执行上下文环境是动态的, 调用函数时创建, 函数调用结束时上下文环境就会被释放
  3. 联系
    • 执行上下文环境(对象)是从属于所在的作用域

    • 全局上下文环境==>全局作用域

    • 函数上下文环境==>对应的函数使用域

image.png

作用域链

  1. 理解
    • 多个上下级关系的作用域形成的链, 它的方向是从下向上的(从内到外)
    • 查找变量时就是沿着作用域链来查找的
  2. 查找一个变量的查找规则
    1. 在当前作用域下的执行上下文中查找对应的属性, 如果有直接返回, 否则进入2

    2. 在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入3

    3. 再次执行2的相同操作, 直到全局作用域, 如果还找不到就抛出找不到的异常

image.png

面试题(妙啊)

  //面试题1
  var x = 10;
  function fn() {
    console.log(x);    //10   因为作用域在函数定义时就已经确定了,fn的上级是window
  }
  function show(f) {
    var x = 20;
    f();
  }
  show(fn);
  
  //面试题2
  var fn = function () {
    console.log(fn)   //function () {console.log(fn) }
  }
  fn()
  var obj = {
    fn2: function () {
      console.log(fn2)   //报错,找不到fn2
      console.log(fn2)   //function () {console.log(fn2);console.log(fn2);}
    }
  }
  obj.fn2()

复习

  • 理解:
    • 作用域: 一块代码区域, 在编码时就确定了, 不会再变化
    • 作用域链: 多个嵌套的作用域形成的由内向外的结构, 用于查找变量
  • 分类:
    • 全局
    • 函数
    • js没有块作用域(在ES6之前)
  • 作用
    • 作用域: 隔离变量, 可以在不同作用域定义同名的变量不冲突
    • 作用域链: 查找变量
  • 区别作用域与执行上下文
    • 作用域: 静态的, 编码时就确定了(不是在运行时), 一旦确定就不会变化了
    • 执行上下文: 动态的, 执行代码时动态创建, 当执行结束消失
    • 联系: 执行上下文环境是在对应的作用域中的

闭包

理解闭包

我想实现a++的功能,但是我不想让外部直接操作a(避免外部随意修改a),所以我封装了一个内部函数来实现a++的功能,并把这个内部函数暴露出去,外部可以调用我这个内部函数来实现a++的功能,但是无法直接接触到a

闭包=「函数」+「函数内部能访问到的变量」

  1. 如何产生闭包?

    • 当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时, 就产生了闭包
  2. 闭包到底是什么?

    • 使用chrome调试查看
    • 理解一: 闭包是嵌套的内部函数(绝大部分人)
    • 理解二: 包含被引用变量(函数)的对象(极少数人)
    • 注意: 闭包存在于嵌套的内部函数中

    image.png

  3. 产生闭包的条件?

    • 函数嵌套
    • 内部函数引用了外部函数的数据(变量/函数)

常见的闭包

  1. 将函数作为另一个函数的返回值

  2. 将函数作为实参传递给另一个函数调用

    外部函数调用几次,就产生几个闭包

    image.png

闭包的作用

  1. 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
  2. 函数外部可以操作(读写)到函数内部的数据(变量/函数)

问题: 1. 函数执行完后, 函数内部声明的局部变量是否还存在? - 一般不存在,存在于闭包中的变量才可能存在 - 如果定义了一个变量来保存,则闭包中的局部变量会存在 var f=fun1(); f();,直到f成为垃圾对象即f=null时,才不存在 - 如果直接调用,则不存在了fun1(); 1. 在函数外部能直接访问函数内部的局部变量吗? - 不能,但是可以通过闭包让外部操作局部变量(暴露函数)

image.png

闭包的生命周期

  1. 产生: 在嵌套内部函数定义完时就产生了(不是在调用)
  2. 死亡: 在嵌套的内部函数成为垃圾对象时
<script type="text/javascript">
  function fun1() {
    //问题2: 此时闭包产生了吗? --产生了(函数提升,内部函数对象已经创建了)
    var a = 3;
    function fun2() {
      a++;
      console.log(a);
    }
    return fun2;
  }
  //问题1: 此时闭包产生了吗? --产生了
  var f = fun1();
  //问题3: 此时闭包释放了吗? --没有
  f();
  f();
  //问题4: 此时闭包释放回收了吗? --没有
  //问题5: 如何让闭包释放回收呢?
  f=null; //闭包死亡(包含闭包的函数对象成为垃圾对象)
</script>

闭包的应用

  • 定义JS模块

    • 具有特定功能的js文件

    • 将所有的数据和功能都封装在一个函数内部(私有的)

    • 只向外暴露一个包含n个方法的对象或函数

    • 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能

    • 方法一:

    image.png

    image.png

    • 方法二:更佳

    image.png

    image.png

  • 模块化: 封装一些数据以及操作数据的函数, 向外暴露一些行为

  • 循环遍历加监听

  • JS框架(jQuery)大量使用了闭包

闭包的缺点及解决

  1. 缺点

    • 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长
    • 容易造成内存泄露
  2. 解决

    • 能不用闭包就不用
    • 及时释放 : f = null; //让内部函数对象成为垃圾对象

面试题(难)

//代码片段一----有闭包吗?--没有

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        return function(){
            return this.name;
        };
    }
};
alert(object.getNameFunc()());  //The Window


//代码片段二----有闭包吗?--有

var name2 = "The Window";
var object2 = {
    name2 : "My Object",
    getNameFunc : function(){
        var that = this;
        return function(){
            return that.name2;
        };
    }
};
alert(object2.getNameFunc()()); //My Object
  function fun(n,o) {
        console.log(o)
        return {
            fun:function(m){
                return fun(m,n);
            }
        };
    }
    var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);//undefined,0,0,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
    
    //外部函数调用几次,就产生几个闭包
    /*
      var a = fun(0);---返回内部函数,此时闭包里的变量n=0
      a.fun(1);---会产生一个新的闭包(因为调用了外部函数),但是该闭包立马消失了(无保存引用),所以用的还是a中的闭包的变量n
      a.fun(2); a.fun(3);--同理
    */
    /*
      var b = fun(0).fun(1).fun(2).fun(3);
      首先var b = fun(0)---返回内部函数,此时闭包里的变量n=0
      var b = fun(0).fun(1)---产生一个新的闭包,并保存引用到b上,所以b上的闭包的变量从0更新为1
      var b = fun(0).fun(1).fun(2).fun(3)---同理
    */
    /*
      var c = fun(0).fun(1); ---产生一个新的闭包,并保存引用到c上,所以c上的闭包的变量从0更新为1
      c.fun(2); ---会产生一个新的闭包,但是该闭包立马消失了(无保存引用),所以用的还是c中的闭包的变量1
      c.fun(3); ---同理
    */

内存溢出与内存泄露

  1. 内存溢出

    • 一种程序运行出现的错误
    • 当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误
  2. 内存泄露

    • 占用的内存没有及时释放

    • 内存泄露积累多了就容易导致内存溢出

    • 常见的内存泄露:

      • 意外的全局变量 (不用var定义的变量)
      • 没有及时清理的计时器或回调函数
      • 闭包

参考:blog.csdn.net/weixin_4494…