JavaScript复习,this指向、原型链、变量提升、作用域、闭包

文章目录

  • 函数中this指向问题
  • 原型
  • 变量/函数 提升
    • 全局执行上下文:
    • 测试题
    • var可重复声明
  • 作用域
  • 闭包
    • 更深入理解闭包的原理
    • 闭包的作用
    • 终极题目
  • ES5的继承

函数中this指向问题

任何函数本质上都是通过某个对象来调用的,如果没有直接指定就是window

  Person("red"); //this是谁? window

  var p = new Person("yello"); //this是谁? p

  p.getColor(); //this是谁? p

  var obj = {};
  p.setColor.call(obj, "black"); //this是谁? obj

  var test = p.setColor;
  test(); //this是谁? window
var name = 'window'
var person = {
    name :'Alan',
    sayName:function () {
        return function () {
            console.log(this.name)
        }
    }
}
person.sayName()()  // window

这里sayName方法的确是person调用的,但返回的匿名函数是window调用,这个匿名函数中this指向window

解决方法还是有的,我们可以把外部作用域的this传递给匿名函数。

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

函数嵌套,使用了闭包的原理

原型

函数也属于对象

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

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

每个函数function都有一个prototype,即显式原型(属性)
在定义函数时自动添加的, 默认值是一个空Object实例对象

每个实例对象都有一个__proto__,可称为隐式原型(属性)
创建实例对象时自动添加的, 默认值为构造函数的prototype属性值

实例对象的隐式原型的值===其对应构造函数的显式原型的值,即指向同一个东西

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

原型链的查找方式时沿着__proto__这条链向上查找,所以也称隐式原型链

理解该图:

  • function Foo()它是一个构造函数,但同时也可以看成是Function构造函数的一个实例对象:Foo = new Function。所以Foo实例对象也有隐式原型__proto__
  • 所有函数都是Function的实例(包含Function,Function = new Function;Object = new Function)
  • 函数的显示原型指向的对象默认是空的Object实例对象,即Foo.prototype和Funtion.prototype为Object实例对象。
    (但Object不满足,因为Object.prototype的__proto__值为null,说明它不是实例对象)
  • Object的原型对象是原型链尽头,因为它不是实例对象,没有__proto__继续往上找

JavaScript复习,this指向、原型链、变量提升、作用域、闭包_第1张图片
看视频的话,应该容易理解该图。有基础的,看看就懂,虽然有点绕。

  1. 表达式: A instanceof B 意思是A是否是B的实例对象

如果B函数的显式原型对象在A对象的隐式原型链上, 返回true, 否则返回false。
具体就是A的__proto__(或者往上找)是否和B的prototype指向同一个东西。

  1. 看着上图,判断下面题目
  /*
  案例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

1.读取对象的属性值时: 会自动到原型链中查找
2.设置对象的属性值时: 不会查找原型链, 如果当前对象中没有此属性, 直接添加此属性并设置其值

  function Fn() {

  }
  Fn.prototype.a = 'xxx'
  var fn1 = new Fn()
  console.log(fn1.a, fn1)

  var fn2 = new Fn()
  fn2.a = 'yyy'
  console.log(fn1.a, fn2.a, fn2)
  // 结果
  // xxx  yyy

JavaScript复习,this指向、原型链、变量提升、作用域、闭包_第2张图片

  function A () {

  }
  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)
  // 结果
  // 1 undefined 2 3
  // 原因是 b的proto引用是之前的原型,c的proto引用是新的原型
  function F (){}
  Object.prototype.a = function(){
    console.log('a()')
  }
  Function.prototype.b = function(){
    console.log('b()')
  }
  
  var f = new F()
  f.a()
  // f.b() b为undefined
  F.a()
  F.b()

变量/函数 提升

console.log(b) //undefined  变量提升
  fn2() //可调用  函数提升
  // fn3() //不能  因为fn3属于变量提升

  var b = 3
  function fn2() {
    console.log('fn2()')
  }

  var fn3 = function () {
    console.log('fn3()')
  }

全局执行上下文:

  • 在执行全局代码前将window确定为全局执行上下文
    JavaScript复习,this指向、原型链、变量提升、作用域、闭包_第3张图片
    函数执行上下文:
  • 在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的, 存在于栈中)
    JavaScript复习,this指向、原型链、变量提升、作用域、闭包_第4张图片

测试题

	/*
     测试题1:  先执行变量提升, 再执行函数提升
     */
    function a() { }
    var a
    console.log(typeof a) // 'function'

    /*
     测试题2: var b做变量提升,不进入if语句
     */
    if (!(b in window)) {
      var b = 1
    }
    console.log(b) // undefined

	/*
     测试题3:顺序为 var c; fun c; c=1
     */
    var c = 1
    function c(c) {
      console.log(c)
      var c = 3
    }
    c(2) // 报错

var可重复声明

function c(c) {
      console.log(c) // 2
      var c = 5
      console.log(c) // 5
    }
    c(2)
var a = 100;
function fn() {
alert(a); //undefined
var a = 200;
alert(a); //200
}
fn();
alert(a); //100
var a;
alert(a); //100
var a = 300;
alert(a); //300

如果重复使用的一个声明有一个初始值,那么它担当的不过是一个赋值语句的角色.
如果重复使用的一个声明没有一个初始值,那么它不会对原来存在的变量有任何的影响.

1.使用var声明变量,在方法内部是局部变量,在方法外部是全局变量
2.没有使用var声明的变量,在方法内部或外部都是全局变量,但如果是在方
法内部声明,在方法外部使用之前 需要先调用方法,告知系统声明了全局变量 后方可在方法外部使用。

// 代码报错。可见没有var关键字,变量的声明不会提前
console.log(d) //Uncaught ReferenceError: d is not defined
d = 1
	// var val,使函数里面的val都为局部变量
	function TestClass() {
		val = 1;
		alert(val);//1  
		alert(window.val);//undefined  
 
		var val = 10;
		alert(val);//10  
		alert(window.val);//undefined  
	}
 
	var test = new TestClass();

	var n1 = 1;
	n2 = 2;
 
	function f1() {
		var n3 = 3;
		n4 = 4;
	}
	// 	console.log("n4="+n4) //Error: n4 is not defined
	f1();
	console.log("n1=" + n1);
	console.log("n2=" + n2);
	// 	console.log("n3="+n3); //Error: n3 is not defined
	console.log("n4=" + n4);

作用域

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

输出结果:10

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

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

fn2首先在函数作用域找不到,然后全局作用域也找不到。
JavaScript复习,this指向、原型链、变量提升、作用域、闭包_第5张图片

闭包

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

//利用闭包
    for (var i = 0, length = btns.length; i < length; i++) {
      (function (j) {
        var btn = btns[j]
        btn.onclick = function () {
          alert('第' + (j + 1) + '个')
        }
      })(i)
    }

看完下面知道怎么解释了,之前我只是理解为像let关键字一样,每次循环的j都保留起来。

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

function (j)属于父函数变量,(j + 1)引用了父函数的变量。闭包到底是什么?

  1. 闭包是嵌套的内部函数(绝大部分人)
    或者
    包含被引用变量(函数)的对象(极少数人)
    闭包存在于嵌套的内部函数中

  2. 产生闭包的条件?

前提是function (j) 要先执行,然后btn.onclick = function ()执行函数定义就会产生闭包(不用调用内部函数)。比如上述,点击事件并未触发,但已经产生闭包了。

常见闭包

  // 2. 将函数作为实参传递给另一个函数调用
  function showDelay(msg, time) {
    setTimeout(function () {
      alert(msg)
    }, time)
  }
  showDelay('atguigu', 2000)

更深入理解闭包的原理

// 1. 将函数作为另一个函数的返回值
    function fn1() {
      var a = 2
      function fn2() {
        a++
        console.log(a)
      }
      return fn2
    }
    var f = fn1()
    f() // 3
    f() // 4
    var f2 = fn1()
    f2()
    f2()
    // 结果
    // 3 4 3 4

说明,每次调用父函数,都生成新的闭包。也可以认为不同的区域,所以f的闭包与f2的闭包无关
JavaScript复习,this指向、原型链、变量提升、作用域、闭包_第6张图片

    // 1. 将函数作为另一个函数的返回值
    function fn1() {
      var a = 2
      function fn2() {
        a++
        console.log(a)
      }
      function fn3() {
        a--
        console.log(a)
      }
      return fn3
    }
    var f = fn1()
    f() // 1
    f() // 0

这次重点不是在输出,而是理解下面问题:

  1. 在函数外部能直接访问函数内部的局部变量吗?
    答:不能, 但我们可以通过闭包让外部操作它

  2. 函数执行完后, 函数内部声明的局部变量是否还存在? (较难理解)
    答:一般是不存在, 存在于闭包中的变量才可能存在

因为var f = fn1()调用了父函数,毋庸置疑产生了闭包,闭包中的变量为var a

但是变量名fn2,甚至是fn3都不在闭包中。所以函数执行完都被清除。而由于var f = fn1(),将原本fn3引用的函数对象赋值给了var f,所以该函数对象没有被当作垃圾对象,fn2指向的函数对象则当作垃圾对象在内存中清除。

如果 fn1()的返回值没有用变量接收,则函数执行完,fn1函数的局部变量都不存在,自然闭包也被清除了。

那么你会问到为什么又说只要满足产生闭包的条件,闭包就存在。

  1. 闭包的生命周期
    产生: 在嵌套内部函数定义执行完时就产生了(不是在调用)
    死亡: 在嵌套的内部函数成为垃圾对象时,即:var f = fn1()改为 fn1()
  function fn1() {
    //此时闭包就已经产生了(函数提升, 内部函数对象已经创建了)
    var a = 2
    function fn2 () {
      a++
      console.log(a)
    }
    return fn2
  }
  var f = fn1()
  f() // 3
  f() // 4
  f = null //闭包死亡(包含闭包的函数对象成为垃圾对象)

我感觉之前黑马的pink老师确实没这么细,导致看完视频很多都忘记了。一定要明白原理,才会印象深刻。

闭包的作用

八股文

  • 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
  • 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
  • 具有特定功能的js文件,将所有的数据和功能都封装在一个函数内部(私有的),只向外暴露一个包信n个方法的对象或函数

缺点:

  • 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长
    JavaScript复习,this指向、原型链、变量提升、作用域、闭包_第7张图片

终极题目

贴士:
不要认为它是一个类,每次调用父函数都会产生新的闭包。
如果你不用变量去接收内部函数,那么内部函数就会清除掉,新产生的闭包也会清除。

  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

ES5的继承

<!--
方式3: 原型链+借用构造函数的组合继承
1. 利用原型链实现对父类型对象的方法继承
2. 利用call()借用父类型构建函数初始化相同属性
-->
<script type="text/javascript">
  function Person(name, age) {
    this.name = name
    this.age = age
  }
  Person.prototype.setName = function (name) {
    this.name = name
  }

  function Student(name, age, price) {
    Person.call(this, name, age)  // 为了得到属性
    this.price = price
  }
  Student.prototype = new Person() // 为了能看到父类型的方法
  Student.prototype.constructor = Student //修正constructor属性
  Student.prototype.setPrice = function (price) {
    this.price = price
  }

  var s = new Student('Tom', 24, 15000)
  s.setName('Bob')
  s.setPrice(16000)
  console.log(s.name, s.age, s.price)

你可能感兴趣的:(前端项目,javascript,原型模式,前端)