函数高级

1. 原型链

1.1 函数的prototype属性

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

    console.log(Date.prototype, typeof Date.prototype)
    function fn() {}
    console.log(fn.prototype, typeof fn.prototype)
  • 原型对象中有一个属性constructor, 它指向函数对象

    console.log(Date.prototype.constructor===Date)
    console.log(fn.prototype.constructor===fn)

  • 给原型对象添加属性(一般都是方法),函数的所有实例对象自动拥有原型中的属性(方法)

    function F() {}
    F.prototype.age = 12 //添加属性
    F.prototype.setAge = function (age) { // 添加方法
        this.age = age
    }
    // 创建函数的实例对象
    var f = new F()
    console.log(f.age)
    f.setAge(23)
    console.log(f.age)

1.2 显式原型与隐式原型

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

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

  • 对象的隐式原型的值为其对应构造函数的显式原型的值

  • 内存结构如图:

  • 总结

    • 函数的prototype属性: 在定义函数时自动添加的, 默认值是一个空Object对象,创建函数的实例对象时使用。
    • 对象的__proto__属性: 创建对象时自动添加的, 默认值为构造函数的prototype属性值,当查找对象中的某个属性,但对象本身没有
    • 程序员能直接操作显式原型, 但不能直接操作隐式原型(ES6之前)

1.3 原型链

1.3.1 基础原型链

  • 所有的实例对象都有__proto__属性, 它指向的就是原型对象
  • 这样通过__proto__属性就形成了一个链的结构---->原型链
  • 当查找对象内部的属性/方法时, js引擎自动沿着这个原型链查找
  • 当给对象属性赋值时不会使用原型链, 而只是在当前对象中进行操作
function Fn() {
    this.test1 = function () {
        console.log('test1()')
    }
}
Fn.prototype.test2 = function () {
    console.log('test2()')
}
var fn = new Fn()

fn.test1()
fn.test2()
console.log(fn.toString())
fn.test3()

1.3.2 构造函数/原型/实例对象的关系(图解)

var o1 = new Object();
var o2 = {};

1.3.3 构造函数/原型/实例对象的关系2(图解)

  • 所有函数对象的__proto__属性值都相等
  • 所有函数都是Function的实例, 包含它自己

1.3.4 原型链属性问题

  1. 读取对象的属性值时: 会自动到原型链中查找
  2. 设置对象的属性值时: 不会查找原型链, 如果当前对象中没有此属性, 直接添加此属性并设置其值
  3. 方法一般定义在原型中, 属性一般通过构造函数定义在对象本身上
function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.setName = function (name) {
    this.name = name;
}
Person.prototype.sex = '男';

var p1 = new Person('Tom', 12)
p1.setName('Jack')
console.log(p1.name, p1.age, p1.sex)
p1.sex = '女'
console.log(p1.name, p1.age, p1.sex)

var p2 = new Person('Bob', 23)
console.log(p2.name, p2.age, p2.sex)

1.3.5 探索instanceof

  1. instanceof是如何判断的?

    • 表达式: A instanceof B

    • 如果B函数的显式原型对象在A对象的原型链上, 返回true, 否则返回false

  2. Function是通过new自己产生的实例

function Foo() {  }
var f1 = new Foo();
console.log(f1 instanceof Foo);
console.log(f1 instanceof Object);

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

console.log(Function.prototype.__proto__===Object.prototype)
console.log(Function.prototype, {})

  1. 自己实现instanceof

    function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
        var O = R.prototype;
        L = L.__proto__;
        while (true) {
            if (L === null)
                return false;
            if (O === L) // 这里重点:当 O 严格等于 L 时,返回 true
                return true;
            L = L.__proto__;
        }
    }
    // 开始测试
    var a = []
    var b = {}
    
    function Foo(){}
    var c = new Foo()
    
    function child(){}
    function father(){}
    child.prototype = new father()
    var d = new child()
    
    console.log(instance_of(a, Array)) // true
    console.log(instance_of(b, Object)) // true
    console.log(instance_of(b, Array)) // false
    console.log(instance_of(a, Object)) // true
    console.log(instance_of(c, Foo)) // true
    console.log(instance_of(d, child)) // true
    console.log(instance_of(d, father)) // true

1.3.6 面试题

 /*
  测试题1
   */
  var A = function() {
  }
  A.prototype.n = 1
  var b = new A()
  A.prototype = {
    n: 2,
    m: 3
  }
  var c = new A()
  console.log(b.n, b.m, c.n, c.m)


  /*
   测试题2
   */
  var F = function(){};
  Object.prototype.a = function(){
    console.log('a()')
  };
  Function.prototype.b = function(){
    console.log('b()')
  };
  var f = new F();
  f.a()
  f.b()
  F.a()
  F.b()

2 执行上下文和执行上下文栈

2.1 变量提升与函数提升

  • 变量声明提升

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

    • 通过function声明的函数, 在之前就可以直接调用
    • 值: 函数定义(对象)
  • 问题: 变量提升和函数提升是如何产生的?

/*
   面试题: 输出什么?
*/
var a = 4
function fn () {
    console.log(a)
    var a = 5
    }
fn()

2.2 执行上下文

  1. JS引擎并不是一行行的解析和执行代码,而是一段段的去分析和执行,当执行一段代码时,先开始预处理,比如声明提升和函数提升
  2. 在执行某段js代码的时候,会进行一个准备工作,这个准备工作用专业的说法 叫“执行上下文”,其实执行上下文也是在内存中开辟的一个空间
  3. js可执行的代码分为3种类型, 全局代码 、 函数代码 、eval代码(忽略)
  4. 每执行一段代码,都会创建相对应的执行上下文,在脚本中可能存在多个执行上下文
  5. 因为有太多的执行上下文, JS创建了一个执行上下文栈(stack) 用来管理执行上下文
  6. 当js开始解析程序的时候,最先遇到的全局代码,此时向执行上下文栈中 压入一个全局执行上下文,全局的一定是在整体运行结束以后才被清空
  7. 当执行一个函数的时候 会创建一个函数的执行上下文,并压入到执行上下文栈中,只要函数执行完成,会将函数从栈里弹出
  8. 每个执行上下文 都有三个重要属性:1.变量对象(VO) 2.作用域链 3.this
function fun3() {
    console.log("fun3");
}
function fun2() {
    fun3();
}
function fun1() {
    fun2();
}
fun1();

/*
      //用伪代码来实现上边代码执行上下文的流程
      //1.js引擎创建了一个执行上下文栈
      var Stack = [];
      //2.向栈中押入 全局执行上下文
      Stack = [globalContext]
      //3.在fun1中创建执行上下文  并压入栈中
      Stack.push(<fun1>Context)
      //4.在fun2中创建执行上下文  并压入栈中
      Stack.push(<fun2>Context)
      //5.在fun3中创建执行上下文  并压入栈中
      Stack.push(<fun3>Context)
      //6.fun3执行完毕
      Stack.pop();
      //7.fun2执行完毕
      Stack.pop();
      //8.fun1执行完毕
      Stack.pop();
      //9整体代码执行结束  全局执行上下文出去
*/

var scope = "hello";
function checkscope() {
    var scope = "world";
    function f() {
        return scope;
    }
    return f();
}
checkscope();
/*
    var Stack = [globalContext];
    Stack.push(<checkscope>Context);
    Stack.push(<f>Context);
    Stack.pop()
    Stack.pop()
*/



//1. 依次输出什么?
//2. 整个过程中产生了几个执行上下文?
  console.log('global begin: '+ i)
  var i = 1
  foo(1);
  function foo(i) {
    if (i == 4) {
      return;
    }
    console.log('foo() begin:' + i);
    foo(i + 1);
    console.log('foo() end:' + i);
  }
  console.log('global end: ' + i)

2.3 变量对象

  • 变量对象是 ECMAScript规范术语。在一个执行上下文中,变量对象才被激活,只有激活的变量对象,其各种属性才能被访问

  • 变量对象是与执行上下文相关的数据作用域,储存了在上下文中定义的变量和函数声明

2.3.1 全局上下文的变量对象

  • window是预定义对象,作为JS全局函数和全局属性的占位符(全局的变量和函数就是window对象属性和方法)
  • 全局执行上下文的变量对象其实就是全局对象window

2.3.2 函数上下文的变量对象

  • 进入执行上下文 不会立马执行代码,只进行分析。此时首先第一步,变量对象包括了函数所有的形参和实参
  • 检查所有声明的函数,由名称和对应值 组成一个变量对象的属性 被创建。如果变量对象已经有相同名字的属性,则完全替换
  • 检查所有的声明的变量,创建键值对儿
  • 变成变量对象的属性,如果变量名和已经声明的形参或函数相同,则变量声明不会干扰已经存在的这类属性
function foo(a) {
    var b = 2;
    function c() {}
    var d = function () {};
    b = 3;
}
foo(1, 2);

//模拟以上代码的变量对象:
var AO = {
    //arguments是保存所有的的实参
    arguments:{
        0:1,
        1:2,
        length:2
    },
    a:1,
    c:function c(){},
    b:undefined,
    d:undefined
}

//练习
function fn1(a) {
    console.log(a); 
    var a = 2;
    console.log(a); /
}
fn1(1);

function fn2() {
    console.log(foo); 
    function foo() {}
    var foo = 1;
}
fn2();

/*
   测试题1: 先预处理函数, 后预处理变量, 如果已经存在就会被忽略
   */
  function a() {}
  var a;
  console.log(typeof a)


  /*
  测试题2: 变量预处理, in操作符
   */
  if (!(b in window)) {
    var b = 1;
  }
  console.log(b)

  /*
  测试题3: 预处理, 顺序执行
   */
  var c = 1
  function c(c) {
    console.log(c)
    var c = 3
  }
  c(2)

2.4作用域

2.4.1作用域

  • 理解
    • 就是一块"地盘", 一个代码段所在的区域
    • 它是静态的(相对于上下文对象), 在编写代码时就确定了
  • 分类
    • 全局作用域
    • 函数作用域
    • 没有块作用域(ES6有了)
  • 作用
    • 隔离变量,不同作用域下同名变量不会有冲突
var a = 10,
    b = 20
function fn(x) {
    var a = 100,
        c = 300;
    console.log('fn()', a, b, c, x)
    function bar(x) {
        var a = 1000,
            d = 400
        console.log('bar()', a, b, c, d, x)
    }

    bar(100)
    bar(200)
}
fn(10)

2.4.2 作用域与执行上下文

  • 区别1
    • 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时
    • 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
    • 函数执行上下文环境是在调用函数时, 函数体代码执行之前创建
  • 区别2
    • 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
    • 上下文环境是动态的, 调用函数时创建, 函数调用结束时上下文环境就会被释放
  • 联系
    • 上下文环境(对象)是从属于所在的作用域
    • 全局上下文环境==>全局作用域
    • 函数上下文环境==>对应的函数使用域
var a = 10,
    b = 20
function fn(x) {
    var a = 100,
        c = 300;
    console.log('fn()', a, b, c, x)
    function bar(x) {
        var a = 1000,
            d = 400
        console.log('bar()', a, b, c, d, x)
    }

    bar(100)
    bar(200)
}
fn(10)

/*
   问题:
   1. 有几个作用域?
   2. 产生过几个上下文环境对象?
   */

2.4.3 作用域链

  • 在函数创建的时候创建一个包含全局变量对象的作用域链( scope chain),储存在内部[[Scope]]属性中。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。
  • 函数执行的时候会创建一个执行环境,通过复制[[Scope]]属性中的对象,构建执行环境的作用域链,并把自己的活动对象推入该作用域链的前端以此形成完整的作用域链。
  • 作用域链的前端,始终都是当前执行的代码所在环境的变量对象。
  • 全局执行环境的变量对象始终都是作用域链中的最后一个对象。
  • 标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)

2.4.4面试题

var x = 10;
function fn() {
    console.log(x);
}
function show(f) {
    var x = 20;
    f();
}
show(fn);



var fn = function () {
    console.log(fn)
}
fn()

var obj = {
    fn2: function () {
        console.log(fn2)
    }
}
obj.fn2()


var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
console.log(checkscope());

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
console.log(checkscope()());

3 闭包

3.1 引入

点击某个按钮, 提示"点击的是第n个按钮"

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>00_引入</title>
</head>
<body>

<button>测试1</button>
<button>测试2</button>
<button>测试3</button>
<!--
需求: 点击某个按钮, 提示"点击的是第n个按钮"
-->
<script type="text/javascript">
  var btns = document.getElementsByTagName('button')
  /* //有问题
  for(var i=0,length=btns.length;i<length;i++) {
    var btn = btns[i]
    btn.onclick = function () {
      alert('第'+(i+1)+'个')
    }
  }*/

  //解决一: 保存下标
  /*for(var i=0,length=btns.length;i<length;i++) {
    var btn = btns[i]
    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[i]
      btn.onclick = function () {
        alert('第'+(i+1)+'个')
      }
    })(i)
  }
</script>
</body>

</html>

3.2 理解闭包

  • 如何产生闭包?

    • 当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时, 就产生了闭包

      // 1. 将函数作为另一个函数的返回值
      function fn1() {
          var a = 2
          function fn2() {
              a++
              console.log(a)
          }
          return fn2
      }
      var f = fn1()
      f() // 3
      f() // 4
  • 闭包到底是什么?

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

    • 函数嵌套
    • 内部函数引用了外部函数的数据(变量/函数)
    • 执行外部函数
  • 常见闭包

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

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

      // 1. 将函数作为另一个函数的返回值
      function fn1() {
          var a = 2
          function fn2() {
              a++
              console.log(a)
          }
          return fn2
      }
      var f = fn1()
      f() // 3
      f() // 4
      
      // 2. 将函数作为实参传递给另一个函数调用
      function showMsgDelay(msg, time) {
          setTimeout(function () {
              console.log(msg)
          }, time)
      }
      showMsgDelay('hello', 1000)
  • 闭包的作用

    • 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)

    • 让函数外部可以操作(读写)到函数内部的数据(变量/函数)

      function fun1() {
          var a = 3;
      
          function fun2() {
              a++;            //引用外部函数的变量--->产生闭包
              console.log(a);
          }
      
          return fun2;
      }
      var f = fun1();  //由于f引用着内部的函数-->内部函数以及闭包都没有成为垃圾对象
      
      f();   //间接操作了函数内部的局部变量
      f();
  • 闭包的生命周期

    • 产生: 在嵌套内部函数定义执行完(创建函数对象)时就产生了(不是在调用)

    • 死亡: 在嵌套的内部函数成为垃圾对象时

      function fun1() {
          //此处闭包已经产生
          var a = 3;
      
          function fun2() {
              a++;
              console.log(a);
          }
      
          return fun2;
      }
      var f = fun1();
      
      f();
      f();
      f = null //此时闭包对象死亡

3.3 闭包的应用

定义JS模块

  • 具有特定功能的js文件
  • 将所有的数据和功能都封装在一个函数内部(私有的)
  • 只向外暴露一个包含n个方法的对象或函数
  • 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
/**
 * 自定义模块1
 */
function coolModule() {
  //私有的数据
  var msg = 'atguigu'
  var names = ['I', 'Love', 'you']

  //私有的操作数据的函数
  function doSomething() {
    console.log(msg.toUpperCase())
  }
  function doOtherthing() {
    console.log(names.join(' '))
  }

  //向外暴露包含多个方法的对象
  return {
    doSomething: doSomething,
    doOtherthing: doOtherthing
  }
}

/**
 * 自定义模块2
 */
(function (window) {
  //私有的数据
  var msg = 'atguigu'
  var names = ['I', 'Love', 'you']
  //操作数据的函数
  function a() {
    console.log(msg.toUpperCase())
  }
  function b() {
    console.log(names.join(' '))
  }

  window.coolModule2 =  {
    doSomething: a,
    doOtherthing: b
  }
})(window)

3.4 闭包的缺点及解决

  • 缺点
    • 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长
    • 容易造成内存泄露
  • 解决
    • 能不用闭包就不用
    • 及时释放

3.5 面试题

//代码片段一
var name = "The Window";
var object = {
    name: "My Object",
    getNameFunc: function () {
        return function () {
            return this.name;
        };
    }
};
console.log(object.getNameFunc()());  //?

//代码片段二
var name2 = "The Window";
var object2 = {
    name2: "My Object",
    getNameFunc: function () {
        var that = this;
        return function () {
            return that.name2;
        };
    }
};
console.log(object2.getNameFunc()()); //?





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,?,?,?

var b = fun(0).fun(1).fun(2).fun(3) //undefined,?,?,?

var c = fun(0).fun(1)
c.fun(2)
c.fun(3) //undefined,?,?,?
posted @ 2023-01-13 22:28  z_bky  阅读(24)  评论(0)    收藏  举报