Javascript-this/作用域/闭包

this & 作用域链 & 闭包

this上下文context

this的核心,在大多数情况下,可以简单地理解为谁调用了函数,this就指向谁。但请注意,这里不包括通过callapplybindnew操作符或箭头函数进行调用的特殊情况。在这些特殊情况下,this的指向会有所不同。

this的值是在函数运行时根据调用方式和上下文来确定的。和作用域不同,作用域在代码写出来的那一刻就已经决定好了。

const o1 = {
  text: "o1",
  fn: function(){
    console.log("o1 this",this);
    return this.text;
  }
};

const o2 = {
  text: "o2",
  fn: function(){
    console.log("o2 this",this);
    return o1.fn()
  }
};

const o3 = {
  text: "o3",
  fn: function(){
    // 当通过 let fn = o1.fn; 将 o1.fn 赋值给局部变量 fn 时,已经丢失了与 o1 的任何关联。
    let fn = o1.fn;
    return fn();
  }
}

console.log("o1fn",o1.fn());
// o1调用fn,所以先打印o1对象,再打印 "o1"
console.log("o2fn",o2.fn());
// o2调用fn,所以先打印o2对象,再打印 o1对象,最后打印 "o1"
console.log("o3fn",o3.fn());
// o3调用fn,此时fn没有调用对象,所以this指向默认的window对象,window没有text属性,所以this.text返回undefined

你看明白了吗?

image-20240628002235481

那提问o2.fn执行时怎么让最终的结果改成"o2"呢?

// 1. 可以直接借用函数,在o2本地去调用函数
const o1 = {
  text: "o1",
  fn: function(){
    console.log("o1 this",this);
    return this.text;
  }
};

const o2 = {
  text: "o2",
  fn: o1.fn
};
console.log("o2fn",o2.fn())

image-20240628003040224

// 2. 通过call apply bind去显示指定this
const o1 = {
  text: "o1",
  fn: function(){
    console.log("o1 this",this);
    return this.text;
  }
};

const o2 = {
  text: "o2",
  fn: function(){
    console.log("o2 this",this);
    return o1.fn.call(o2) // o1.fn.call(this) o2调用fn时,this就是指向o2
  }
};
console.log("o2fn",o2.fn());

image-20240628003313572

call & apply & bind

  • 相同点:三者都是用来改变函数调用时的this指向
  • 不同:
    • call functionName.call(thisArg, arg1, arg2, ...) 参数需要一个一个传递
    • apply functionName.apply(thisArg, [argsArray]) 参数可以用数组传递
    • bind
      • functionName.bind(thisArg[, arg1[, arg2[, ...]]])
      • 返回一个新函数,在bind()被调用时,这个新函数this被指定为bind()的第一个参数,而其余参数将作为新函数的参数供调用时使用。
  • callapply 都会调用函数,并返回函数调用的结果(如果有的话)。
  • bind 不会调用函数,而是返回一个新的函数,这个新的函数被调用时,才会执行原始函数,并且具有指定的this值和预置的参数。
  • 使用场景
    • 如果知道要传递的参数,并且想要立即调用函数,那么可以使用callapply
    • 如果想要创建一个新函数,这个函数在被调用时具有特定的this值和预置的参数,那么可以使用bind

有时我们会遇到要手写这三个函数的情况,我们可以先写一个大致的框架,列出函数的输入输出,然后再向框架里填充内容。

call

function hello(start, end) {  
  return start + ', ' + this.name + end;  
}  
  
const person = { name: 'Alice' };  
  
// 使用 call 调用 hello 函数,并将 this 绑定到 person 对象  
const message = hello.call(person, 'Hello', '!');  
console.log(message); // 输出 "Hello, Alice!"

手写call

输入:一个上下文,可选参数(一个一个传递)

输出:函数执行的结果

/*
function speak() {
    console.log(this.name,'can speak!'); // Alice can speak
}

const obj1 = {
    name: 'Alice'
}
speak.myCall(obj1);
*/
Function.prototype.myCall = function (context,...args) {
    // 边界检测,如果context没传则将上下文替换成全局对象window||global
    context = context || window;
    // 将调用myCall的函数(这里指的是speak)作为新的上下文(调用myCall时传入的obj1)的属性值添加到上下文中
    // 注意:这里我们用context.fn作为中间变量来调用函数
    context.fn = this; // // 将this赋值给context的一个属性
    const result = context.fn(...args); // 使用context作为上下文调用函数
    delete context.fn; // 清理环境,避免内存泄漏
    return result;
}

在这个例子中,speak 是被调用的函数,所以 thismyCall 内部指向 speak 函数。而 context 是我们传递给 myCallobj1 对象,我们希望在 speak 函数内部使用 obj1 作为 this 上下文。因此,我们将 speak 函数作为 obj1 的一个方法(临时)来调用它,实现了改变 this 上下文的效果。

中途打断点可以看到context的值如图

image-20240627234537458

apply

// Math.max() 函数不接受数组作为参数。它接受任意数量的数字参数,并返回这些参数中的最大值。所以这是最适合用于演示apply的函数
function max(numbers) {  
  return Math.max.apply(null, numbers);  
}  
  
const maxNum = max([1, 2, 3, 4, 5]);  
console.log(maxNum); // 输出 5

手写apply

经过上面的例子手写了call之后,手写apply就没什么难的了,因为它俩就接受参数的方式不同而已。apply接受的是一个数组

Function.prototype.myApply = function (context,argumentsArr) {
    context = context || window;
    context.fn = this;
    // 如果argumentsArr存在则将其内容作为参数传递
    const result = argumentsArr ? context.fn(...argumentsArr) : context.fn();
    delete context.fn;
    return result
}

// 将上面的Math.max.apply改成myApply是一样的效果

bind

比如当我们想要确保某个函数总是以特定的上下文来执行时。例如,在事件处理器、回调函数和定时器中,我们需要绑定 this 上下文,确保函数总是能够正确地访问和操作我们期望的对象。

1. 事件处理器中的 this 绑定

在事件处理器中,this 通常指向触发事件的元素,而不是我们期望的对象。使用 bind 可以确保 this 指向正确的对象。

function Button() {
    this.name = 'My Button';
    this.handleClick = function(event) {
        console.log(this.name + ' was clicked!'); // 'this' 指向Button实例
    };
    const buttonElement = document.getElementById('btn');
    buttonElement.addEventListener('click', this.handleClick.bind(this));
}
const myButton = new Button();
// 输出:My Button was clicked!

// 如果改成下面这个则不会输出name,只会输出 was clicked!
// buttonElement.addEventListener('click', this.handleClick);

2. 回调函数中的 this 绑定

在异步操作或回调函数中,this 的值可能会变化。使用 bind 可以确保 this 的值在回调函数执行时保持不变。

function User(firstName) {
    this.firstName = firstName;
    this.fetchData = function(url) {
        // 假设fetch是一个模拟的异步函数
        fetch(url)
            .then(response => response.json())
            .then(data => {
                console.log(this.firstName + ' fetched data: ', data); // 'this' 指向User实例
            }).bind(this) // 使用bind确保this指向User实例
            .catch(error => console.error('Error:', error));
    };
}

const user = new User('Alice');
user.fetchData('https://api.example.com/data');

注意:在现代JavaScript中,通常使用箭头函数来自动绑定 this,因为箭头函数不绑定自己的 this,而是捕获其所在上下文的 this 值。

3. 预设参数

使用 bind 可以预设函数的参数。这在创建可复用的函数时非常有用。

function list() {
    return Array.prototype.slice.call(arguments);
}

const list1 = list(1, 2, 3); // [1, 2, 3]

// 创建一个新的函数,预设第一个参数为'boys'
const listWithItems = list.bind(null, 'boys');
const list2 = listWithItems(1, 2, 3); // ['boys', 1, 2, 3]

在这个例子中,listWithItemslist 函数的一个新版本,将 'boys' 作为第一个参数。当我们调用 listWithItems(1, 2, 3) 时,它实际上是在调用 list('items', 1, 2, 3)

4. 绑定到特定的上下文

有时我们可能希望将函数绑定到特定的对象,以便在其他地方调用它时,它总是以该对象为上下文。

const obj = {
    age: 10,
    getAge: function() {
        return this.age;
    }
};

const unboundGetAge = obj.getAge;
console.log(unboundGetAge()); // undefined,因为this没有绑定到obj

const boundGetAge = obj.getAge.bind(obj);
console.log(boundGetAge()); // 10,因为this被绑定到obj

在这个例子中,unboundGetAge 在调用时没有绑定 this,所以它的 this 值是 window。而 boundGetAge 则被绑定到 obj 对象,因此它总是返回 obj.age 的值。

手写bind函数

// 手写bind
// 输入:一个新的this上下文,以及可选的参数列表
// 输出:一个新的函数,这个新函数被调用时会将this设置为指定的值,并且将参数列表与bind调用时提供的参数合并
Function.prototype.myBind = function (context,...initialArgs){
  // 1.保存调用 myBind 方法的原始函数。
  const self = this;
  // 2.返回一个新函数
  return function F(...boundArgs) {
    // 3.判断函数是否以构造函数的方式调用 这一段我还没理解
    if (this instanceof F) {
      // 如果是,那么 this 会指向一个新创建的对象,而不是我们提供的 context。
      // 我们就使用new和原始函数self来调用
      return new self(...initialArgs,...boundArgs);
    }
    
    // 否则,直接调用原始函数self,并传入context作为this,以及合并后的参数
    return self.apply(context,[...initialArgs,...boundArgs]);
  }
}


/*
function hello(start, end) {  
    return start + ', ' + this.name + end;  
}  
  
const obj = { name: 'Alice' };  
  
const boundHello = hello.myBind(obj, 'Hello');  
  
console.log(boundHello('!')); // 输出 "Hello, Alice!"
*/

上面这段代码执行栈如下图,context就是我们传入的obj对象,initialArgs是我们在调用bind是传入的'Hello'boundArgs是我们调用返回的新函数boundHello时传入的参数。

image-20240628010304262

当我们使用 bind 方法(无论是原生的 Function.prototype.bind 还是手写实现的 myBind)时,我们实际上是在创建一个新的函数,这个函数被“绑定”到了特定的 this 上下文(在这个例子中是 obj 对象)以及一些预先设定的参数(在这个例子中是 'Hello')。

这个新函数(我们称之为 boundHello)现在可以独立使用,并且每次调用它时,都会以我们指定的 this 上下文(obj)和预先设定的参数('Hello')来调用原始的 hello 函数。

作用域与作用域链

作用域:变量和函数在代码中可以访问和可见的范围。

function test() {
  var x = '内部变量'
}
test()
console.log(x) // x is not defined

x只在test函数的作用域内,所以在全局作用域下去访问x就会报错

总结: 作用域是一个独立的区域,它定义了在该区域内声明的变量和函数的可访问性范围。

目的: 主要目的是隔离变量,确保它们不会无意中泄露到外部作用域,从而避免命名冲突和数据污染。

作用:

  1. JavaScript能够控制哪些代码块可以访问哪些变量和函数,提高了代码的安全性、模块性和可维护性。
  2. 同时,作用域也是实现闭包等高级特性的基础,它允许函数记住并访问其外部作用域中的变量,即使外部函数已经执行完毕。
  • 全局作用域:在代码的最外层定义的变量和函数具有全局作用域,这意味着它们可以在代码的任何地方被访问。在浏览器环境中,全局作用域中的变量和函数会成为window对象的属性和方法。
    image-20240831013731847

  • 函数作用域:在函数内部定义的变量和函数(不使用let或const)具有函数作用域,也称为局部作用域。这些变量和函数只能在定义它们的函数内部被访问。

    image-20240831014347350

    同理,我们直接访问内部的Play函数也会报错

    image-20240831014520608

    1. var:它声明的变量具有函数作用域或全局作用域(如果变量是在函数外部声明的)。此外,var声明的变量存在变量提升(hoisting)现象,即无论变量在何处声明,都会被视为在函数或全局作用域的顶部声明(声明可以在使用后)。
    2. let:ES6引入的let关键字用于声明块级作用域的变量。与var不同,let声明的变量仅在它们被声明的块(如{}块、if语句、for循环等)内部有效。此外,let声明的变量不会被提升(先声明再使用)。
    3. const:与let类似,const也是ES6引入的,但它用于声明常量。一旦一个常量被赋值,它的值就不能被重新赋值(如果常量是一个对象,则可以修改对象的属性,但不能将常量重新指向另一个对象)。const声明的常量也具有块级作用域,并且不会被提升。

    变量提升:如下图所示,可以将var test = 's'拆解成先声明,再赋值,所以第一次console.log时是undefined而不是下面红色报错

    image-20240831021143852

  • 块级作用域:ES6引入了let和const关键字,使得变量可以在块级作用域(如if语句、for循环或{}块)中定义。这些变量只能在定义它们的块内部被访问。

    块级作用域很容易理解,一对花括号{}包裹的就是一个块

    image-20240831014916468

  • 模块作用域:ES6还引入了模块的概念,允许将相关的代码组织在一起,并通过export和import语句在模块之间共享变量、函数、类等。每个模块都有自己的作用域,模块内部定义的变量和函数默认只能在该模块内部被访问,除非它们被显式导出。

    // math.js
    // 使用 export 关键字导出函数  
    export function add(x, y) {
        return x + y;
    }
    
    // 注意:这个函数没有被导出,因此只能在 math.js 内部访问
    function privateFunction() {
        console.log('这是一个私有函数,只能在 math.js 内部调用');
    }
    
    // 直接在模块中使用的变量也是模块作用域的  
    let privateVar = '我是一个私有变量';
    
    privateFunction()
    console.log(privateVar); // 可以在模块内部访问
    
    // index.js
    import {add} from './math.js'
    // 使用导入的函数
    console.log(add(2, 3)); // 输出: 5
    
    // 尝试访问 math.js 中的私有函数和变量会导致错误
    // console.log(privateFunction()); // Uncaught ReferenceError: privateFunction is not defined
    // console.log(privateVar); // Uncaught ReferenceError: privateVar is not defined
    

作用域理解之后再看看作用域链

let a = 'global'
console.log(a) // global

function outFunc() {
  let b = 'hello'
  console.log(b) // hello
  midFunc() 
  console.log(c) // c is not defined
  function midFunc() {
    let c = 'world'
    console.log(c) // world
    innerFunc()
    function innerFunc() {
      d = '!' // 直接赋值 此时d被放到了全局作用域
      console.log(d) // !
      console.log('test',b) // test hello
    }
  }
}
outFunc()
console.log(d) // 输出! 注意这里是可以取到d的

作用域向上查找,内部可以拿到外部的变量,反之则行不通 => 作用域链:作用域的集合,子集可以访问父集,父集不能访问子集


闭包

posted @ 2024-06-28 01:29  _Bourbon  阅读(54)  评论(0)    收藏  举报