JS-高级-05(闭包.单线程问题...)

闭包

//实际上js中所有的函数都是闭包
//闭包是由 函数体 和 函数所处的环境(函数作用域) 构成的综合体
//闭包就是函数作用域的应用

闭包的作用:
1.保护变量
2.维持变量的值

闭包的基本模型:
函数嵌套函数,并且让嵌套的函数返回

function fn() {
      var num = 123;

// 返回函数的目的:为了在外层函数fn的外部来访问到函数内部的变量num
// 为什么将函数返回以后,还能在函数外部访问到变量num的值???
// 不管函数在哪个地方调用,最终还是要回到创建它的环境中执行
// 因为这个函数是在外部函数fn中创建的,所以,返回函数的作用域链包含了外层函数fn
// 的作用域,所以,能够访问到变量num的值
      return function() {
        // console.log(num);
        console.log(++num);
      };
    }

    var foo = fn();
    foo();
    foo();
    

从内存角度分析闭包

函数调用是要占用内存的
比如:函数内部声明的变量是需要内存来进行存储
function(){
    var num = 123;
}
fn()

console.log(num);报错   函数内部无法访问到函数内部的变量

闭包的问题

闭包存在内存占用的问题,闭包占用的内存是不会被释放的
所以,如果滥用闭包,会造成内存浪费(泄露)的问题,影响程序的性能

最佳实践:只在必要的情况下使用闭包!
function fn(){
    var num = 1;
    return function(){
        console.log(++num)
    }
}
var foo = fn();
foo()

1.调用函数fn(),会占用一块内存
2.这块内存存储了变量num
3.函数fn调用结束,但是调用函数占用的内存没有被释放
4.因为我们将返回的函数赋值给了foo,变量foo现在就是返回函数的引用
5.因为返回函数foo的作用域中包含了 外层函数fn 的作用域,也就是函数foo的作用域链存在对外层函数fn的作用域的引用(占用)
6.因为占用的关系,所以,内存函数foo需要一直都能够访问到外层函数fn里的变量num
7.最终导致的结果就是,外层函数fn虽然调用结束了,但是函数fn的作用域已在内存中得到了维持,也就是函数fn调用的占用的内存没有被释放

JS垃圾回收机制:

var obj = {}; //计数1

var o = obj;//计数2

obj = null; //计数1 手动释放变量占用的内存

o = null;//计数0;

创建对象obj,也就是说,变量obj是对新创建对象的引用

这种情况下,对象{ }占用的内存是不会被回收的,因为变量obj引用了这个对象

obj = null; 手动释放变量占用的内存

将obj的值设置为null,o的值设置为null以后,就没有任何方式能够获取到上面我们创建的对象,那么,这个对象将来就会被JS的垃圾回收机制回收掉;


setTimeout的问题

var  num = 1;
setTimeout(function(){
    num = 5;
},0);
console.log(num); //1

定时器:

第一个参数:表示一个回调函数

第二参数:表示"延迟时间",必须要经过设置的时间后,才会调用回调函数

注意:调用的这个时间是不准确的!!!

JavaScript单线程的说明

单线程是相对于多线程来说的

所谓的单线程就是:同时只能处理一件事情

与JavaScript密切相关的三个线程:(浏览器提供)

1.++主线程++(执行JS代码,所有的js代码都是由主线程执行)

2.++UI渲染线程++(浏览器将css或js操作DOM的效果渲染到页面中)

3.++事件循环线程++

主线程和UI渲染线程是互斥的,也就是说同时只能有一个线程在运行

如果是主线程在执行(JS代码在执行),那么UI渲染线程就会等待

如果是UI渲染线程在执行(渲染页面效果),那么,主线程就会等待

 // 1 
    console.log('1 开始');
    setTimeout(function () {
// 2
      console.log('2 setTimeout');
    }, 10);

    var start = new Date();
    for (var i = 0; i < 10000000; i++) {

    }
// 3
    console.log(new Date() - start);

// 4
    console.log('3 结束');
    
1 主线程会从上往下解析执行JS代码
2 对于第一步,直接执行,打印出 1 开始
3 遇到一个 setTimeout,主线程知道这是一个“异步”(延迟执行)的代码
  然后,就将回调函数 function () { console.log('2 setTimeout'); } 放到事件队列中
  注意:没有调用回调函数
4 直接打印 3 打印4 结束,到此未知,JS主线程中的代码就执行完毕了
5 此时,JS主线程会通知事件循环线程,让事件循环线程到事件队列中看下有没有正在等待执行的回调函数
6 如果有,事件循环线程就会把这个回调函数取出来交给 主线程
7 最终,这些回调函数,还是由 主线程 来调用

代码执行循序:1>3>4>2

需要被放到事件队列中延迟执行的代码有:

1.setTimeout

2.setInterval

主线程中的代码优先级最高,只有等到主线程中所有的代码都执行完成以后,事件队列中的回调函数才会被执行!!!


为什么要使用缓存???

储存一些已经计算过的结果,方便以后再次使用(避免重复计算)

为什么要使用闭包???

为了保护变量(缓存对象)

因为我们使用缓存,也就是我们完全信任缓存中的数据,因为缓存是需要被保护的

任何人都不能修改缓存中的数据

缓存使用的步骤:

1.先判断缓存中有没有我们需要的数据

2.如果有就直接使用,如果没有就先计算然后将计算的结果存储到缓存

函数的四种调用模式:

1.函数调用模式 this--->window
2.方法调用模式 this--->调用方法的对象
3.构造函数调用模式 this--->指向创建出来的实例对象
4.上下文调用模式 (call/apply)

实际上是根据函数中的this的指向,来区分不同的调用模式

this具有动态性,也就是这个函数以不同的方式来调用,函数中的this会指向不同的对象

分析this的问题

1.分析this属于哪一个函数
2.再看这个函数是以什么模式来调用的
对于this的问题,只看函数是怎么被调用的,而不管函数是怎么来的!!!

    // 函数
    // function fn() {
    //   console.log(this);
    // }
    // // 函数调用模式
    // fn();

    // var obj = {
    //   name: 'test',
    //   // 方法
    //   say: function() {
    //     console.log(this);
    //   }
    // };
    // // 方法调用模式
    // obj.say();

    // function Person() {
    //   this.name = 'jack'
    // }
    // // 构造函数调用模式
    // var p = new Person();
    // console.log(p);

函数四种调用模式面试题

1.
 var num = 333;
    function fn(){
        var num = 11;
        console.log(this.num)//333 函数调用模式 this.指向window 
    }
    fn()
    console.log(num)//333

2.
 var age = 38;
 var obj = {
  age: 18,
  getAge: function () {
    console.log(this.age);// this 指向 obj
  }
};
obj.getAge();//方法调用模式

3.
 var age = 38;
 var obj = {
  age: 18,
  getAge: function () {
    function foo() {
      // this --> window
      console.log(this.age); // 38
    }
    foo(); // 函数调用模式
  }
};
obj.getAge();

4.
var age = 38;
var obj = {
  age: 18,
  getAge: function () {
    // this -> window
    alert(this.age);
  }
};
var f = obj.getAge;
f(); // 函数调用模式
// obj.getAge();//方法调用模式

5.
  var length = 10;
    function fn() {
      console.log(this.length);
    }
    var obj = {
      length: 5,
      method: function (fn) {
        // 形参 fn 是上面创建的函数fn
        fn();   // 10

        // arguments是实参,arguments[0] 就是第一个实参
        // 这是 方法调用模式,此处是,arguments对象调用的方法
        // 所以,方法内部的this指向了 arguments,实际上访问的就是 arguments.length
        // 也就是 实参的个数
        arguments[0](); 
      }
    };

    obj.method(fn, 123,12,15,13);

var obj = {
      age: 19,
      getAge: function() {
        console.log(this.age);
      }
    };

    // 访问对象属性的两种形式:
    // 1 点运算符
    // 2 []
    obj.getAge(); // 方法调用模式
    obj['getAge']();// 方法调用模式 */

上下文调用模式_说明 练习

上下文调用模式,也叫做:借用方法模式
上下文,即:环境(哪个对象调用的这个方法)

两个方法:

1:apply()
2:call()

特点:可以指定函数内部中的this指向

1.apply()

语法和参数说明:
apply()
语法:函数.apply(this.Arg,数组或伪数组)
这个方法是由Function.prototype提供的!
apply()方法的参数说明:
第一个参数:用于指定由哪个对象来调用这个方法,也就是改变了函数中的this指向
第二个参数:是一个数组或者伪数组,数组或伪数组中每一项都将作为被调用函数的参数

特点:利用第二个参数为数组,能够将数组中的元素取出来作为被调用方法的参数
例如:Math.max.apply(null,arr);此处就利用了apply方法第二个参数为数组的特点
练习:!!!!!!
function fn(num1, num2, num3) {
      console.log(this);
      console.log(num1);
      console.log(num2, num3);
    }
var obj = {
      name: '这是对象obj'
    };
    // 作用:实际上这还是在调用函数fn
    fn.apply(obj, [1, 3, 99]);

2.call()

call方法的作用与apply方法的作用相同:
区别:第二参数的形式不一样
对于apply来说,第二个参数是一个数组或者位数

对于call来说,除第一个参数以外,其他参数都将作为被调用方法的参数

两者都具备的特点:借用方法,改变借用方法内部的this的指向

练习:
 function fn(name,age){
         this.name = name;
         this.age = age;
          
     }
     function Obj(name,age){
         fn.call(this,name,age)
     }
     var a = new Obj("小明",12);
     console.log(a) 
-----------------------------------------------------------------------------------------------------------------------------------------------
 // 人 构造函数
    function Person(name, age, gender) {
      this.name = name;
      this.age = age;
      this.gender = gender;
    }

    // 学生 构造函数
    function Student(name, age, gender, score, id) {
      // this.name = name;
      // this.age = age;
      // this.gender = gender;

      // Person(); // 这是函数调用模式,函数内部的this指向window
      // this 是谁??? 因为这个函数是以构造函数模式调用的,所以,函数内部的this指向 创建出来的实例对象!
      Person.call(this, name, age, gender);
      this.score = score;
      this.id = id;
    }

    var stu = new Student('jack', 19, 'male', 100, '10010');

    // 老师 构造函数
    function Teacher(name, age, gender, salary) {
      // 1 当前的 this 是谁??? 因为这个函数是以构造函数模式来调用的,所以,
      //                         this 是 实例对象
      // 2 让实例对象借用 Person函数,所以,Person中的this指向了 当前的实例对象
      Person.call(this, name, age, gender);

      this.salary = salary;
    }

    var t = new Teacher('小明', 20, 'male', 100000);

    // 以上代码可以实现功能,但是 name、age、gender 是一些公共的属性
    // 我们可以将其处理之后,实现复用!!!

其它情况中的this指向问题

 // 1 setTimeout中的this是谁??? window
 //   setInterval中的this是谁??? window
    setTimeout(function () {
      // window
      console.log(this);
    }, 100);

 // 2 事件中的this
    document.getElementById('btn').onclick = function() {
      // 当前的DOM对象
      console.log(this);
    };

  // 3 原型对象方法中的this 是谁???
    function Person() {}
    Person.prototype.say = function() {
      console.log(this);
    };

    var p = new Person();
    p.say(); // 实例对象

bind()方法

语法和使用说明:
作用:与call和apply方法的作用相似,也可以改变函数内部this的指向,但是用法不同
由函数调用bind方法,bind方法只用一个参数,参数为一个对象,bind方法会返回一个新的函数
这个  新返回函数中的this 就与 bind方法的参数对象  绑定到一起了

语法:var newFn = fn.bind(this.Arg);
1.bind方法会返回一个新的函数返回值(),单数不会调用函数
2.新返回函数中的this与bind方法的参数对象绑定到一起了,也就是说,将来调用新返回的函数,新返回函数中的this就是bind方法的参数了

返回函数newFn和fn的关系:
newFn是函数fn的副本(代码逻辑是完全一样的),只是函数中的this指向不同
经典案例
创建绑定函数
bind() 最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的 this 值。JavaScript新手经常犯的一个错误是将一个方法从对象中拿出来,然后再调用,希望方法中的 this 是原来的对象。(比如在回调中传入这个方法。)如果不做特殊处理的话,一般会丢失原来的对象。从原来的函数和原来的对象创建一个绑定函数,则能很漂亮地解决这个问题:

this.x = 9; 
var module = {
  x: 81,
  getX: function() { return this.x; }
};

module.getX(); // 返回 81

var retrieveX = module.getX;
retrieveX(); // 返回 9, 在这种情况下,"this"指向全局作用域

// 创建一个新函数,将"this"绑定到module对象
// 新手可能会被全局的x变量和module里的属性x所迷惑
var boundGetX = retrieveX.bind(module);
boundGetX(); // 返回 81

你可能感兴趣的:(JS-高级-05(闭包.单线程问题...))