cxxjackie 发表于 2023-2-5 23:46:21

闲话this

很多教程对this的解释比较抽象,简单来说,在全局函数作用域中,this一般指向window,在对象函数作用域中,this指向所属对象。先说对象的,看看下面的例子:

```js
const a = {
    b: function() {
      console.log(this);
    },
    c: {
      d: function() {
            console.log(this);
      }
    }
};
```

第一个this指向a,因为b是a的函数;第二个this指向a.c,因为d是a.c的函数。看懂了吗?你在哪个对象上调用的函数,该函数内部的this就指向哪个对象。为什么全局作用域中指向window呢?因为window也是一个对象,在js设计之初,所有全局作用域下定义的变量和函数,都会被自动绑定到window上。看看这个例子:

```js
function a() {
    console.log(this);
}
a();
```

a函数在定义时就被绑定到window上,调用a()等同于调用window.a(),看到window.就懂了吧,这也可以是广义的对象函数作用域,this指向所属的对象window。看似很合理,不幸的是,这种设计存在缺陷:并非所有全局函数都在window上,特别是ES6引入块级作用域后,这种问题尤其常见,比如下面的声明方式:

```js
const a = function() {
    console.log(this);
};
```

const和let声明的变量不会绑定到window上(即使在全局作用域中声明),但这种语境下,this仍然指向window。这属于历史遗留问题,为解决这一缺陷,ES5提出了严格模式(use strict),当函数没有调用对象时,this改为指向undefined:

```js
'use strict';
const a = function() {
    console.log(this);
};
a();
```

关于箭头函数,由于箭头函数自身没有this,故this从外部继承,把this当局部变量来看就好,当前作用域没定义就往上找。这不难理解,只是在多层函数嵌套中容易混淆,因此也有重命名一个变量来存储this值的做法。

### 类中的this

在类的构造函数和原型方法中,this指向实例。什么是类和实例呢?可以简单理解为所有需要new的东西就是类,new的结果就是实例,比如:

```js
const xhr = new XMLHttpRequest();
```

XMLHttpRequest是一个类,xhr是他的实例。这种设计可以共享变量和函数,比如所有数组都有push方法,是不是每个数组都需要在内存中声明一个push函数?当然不是,push只声明在Array原型上,所有数组(或者说Array的实例)都共用这个函数。如何区分是哪个数组?这就是this指向实例的作用。你说数组不需要new?这不过是简化了的字面量语法,完整写法长这样:

```js
const arr = new Array(1, 2, 3); // const arr = ;
```

包括数字Number、字符串String、对象Object、函数Function等等,一切都可以抽象成类和实例的概念(面向对象编程)。构造函数就是类在实例化时自动执行的函数,看下面的例子就能理解了:

```js
class MyClass {
    constructor(num) { // 构造函数
      console.log('我被实例化了!');
      this.num = num;
    }
    add() { // 原型方法,MyClass.prototype.add
      this.num++;
    }
}
const obj = new MyClass(123);
console.log(obj.num);
obj.add();
console.log(obj.num);
```

### 改变this

函数的this可以被改变,包括显式和隐式的改变,其中隐式改变是最容易被忽视的地方,看看这个例子:

```js
元素2.onclick = () => {
    元素1.click();
};
```

这里你可能会认为两个函数的参数一致,因此简写成:

```js
元素2.onclick = 元素1.click;
```

这两种写法真的等价吗?第二种写法实际是以click函数替换onclick,既然onclick是元素2的方法,this自然指向元素2,也就是说,代码的实际效果是这样:

```js
元素2.onclick = () => {
    元素1.click.bind(元素2)();
}
```

由于所有元素都共用同一个click函数,函数内通过this来区分实例,因此最终效果变成了这样:

```js
元素2.onclick = () => {
    元素2.click();
}
```

这就是隐式改变this所带来的问题,不注意的话就容易犯错。看另一个例子:

```js
class MyClass {
    constructor() {
      this.num = 0;
    }
    add() {
      this.num++;
    }
}
const obj1 = new MyClass();
const obj2 = {
    num: 10,
    add: obj1.add // () => obj1.add()
};
obj2.add();
console.log(obj1.num, obj2.num);
```

分别试试两种写法,再结合前面例子,可以帮助你更好地理解这种差异性。
显式改变this,即通过apply、call、bind等方法强制指定this,比如要让上面click的例子正常工作,可以改成:

```js
元素2.onclick = 元素1.click.bind(元素1);
```

你希望用$简写document.querySelector?一样的道理,可以写成:

```js
const $ = document.querySelector.bind(document);
```

### 劫持时的this

下面是一个很典型的函数劫持写法:

```js
const oldPush = Array.prototype.push;
Array.prototype.push = function(...args) {
    // ...
    let result = oldPush.apply(this, args);
    // ...
    return result;
};
```

劫持函数内部通过oldPush调用原函数,由于oldPush没有从属对象,内部this会指向window(严格模式指向undefined),这显然是错误的,我们需要将实例传递过去,这就是为什么要用apply。当然如果原函数不访问this,或者this就是window,那不传递也没影响,但只要你不想自找麻烦,传递了总不会犯错。
还有另一种写法:

```js
Array.prototype.oldPush = Array.prototype.push;
Array.prototype.push = function(...args) {
    // ...
    let result = this.oldPush(...args);
    // ...
    return result;
};
```

这种写法将原函数保存在原型上,然后用this.的方式调用,本质上跟apply没有区别,只是提供了从外部获取原函数的途径,但也可能对原对象造成污染,各有优劣。

李恒道 发表于 2023-2-6 00:42:57

const和let声明的变量不会绑定到window上(即使在全局作用域中声明),但这种语境下,this仍然指向window。这属于历史遗留问题这部分第一次知道,长见识了...
但是关于
数组不需要new?这不过是简化了的字面量语法
这部分我个人认为还是有一定出入的
印象里new Array属于装箱类型,[]属于拆箱类型
应该在执行速度上以及一些细微的兼容性上都会有一定差异
感觉Array属于[]的上层封装更贴切一些吧
(论坛真卡...)

王一之 发表于 2023-2-6 09:38:40

李恒道 发表于 2023-2-6 00:42
const和let声明的变量不会绑定到window上(即使在全局作用域中声明),但这种语境下,this仍然指向window。 ...
难受,腾讯云香港不知道怎么了。。。。有钱了,换更好的线路

我这里是正常的(40的ms 0丢包,哥哥可以看看你那里)

cxxjackie 发表于 2023-2-7 00:31:24

李恒道 发表于 2023-2-6 00:42
const和let声明的变量不会绑定到window上(即使在全局作用域中声明),但这种语境下,this仍然指向window。 ...

确实不太一样,像是new Number(123) !== 123,以面向对象思想来说,一切都是对象,实例自然也是,因此new Number(123) instanceof Object是true,而123 instanceof Object却是false。js的字面量在实现方式上有些许不同,效率上比new实例化更快,因为每个实例都需要独立内存,而同值的字面量实际上是同一个:
new Number(123) !== new Number(123)
123 === 123
字面量具有类实例的所有特性,甚至在Object原型上添加的属性和方法,同样会反映到字面量上:
Object.prototype.__test__ = 'test';
console.log((123).__test__);
从这个角度来说,字面量实际上是一种特殊的对象,也算是类的一个特殊实例。这里只是作为类和实例之间关系的例子,可能不太贴切,但不影响理解。

李恒道 发表于 2023-2-7 09:59:19

cxxjackie 发表于 2023-2-7 00:31
确实不太一样,像是new Number(123) !== 123,以面向对象思想来说,一切都是对象,实例自然也是,因此new ...

还真是特殊的对象...
JS太乱了
卧槽

李恒道 发表于 2023-2-8 10:55:09

cxxjackie 发表于 2023-2-7 00:31
确实不太一样,像是new Number(123) !== 123,以面向对象思想来说,一切都是对象,实例自然也是,因此new ...
突然想起来
C大对settimeout和setinterval解决爆栈的问题有研究吗
我之前在rxjs的源码里看到过搞了一个队列来解决堆栈溢出的问题
https://bbs.tampermonkey.net.cn/ ... highlight=%E6%A0%88
看stackflow资料来说settimeout和setinterval也能解决
但是实际我写的代码测试堆栈在一直往上叠...
C大知道正确写法吗

cxxjackie 发表于 2023-2-8 22:41:13

李恒道 发表于 2023-2-8 10:55
突然想起来
C大对settimeout和setinterval解决爆栈的问题有研究吗
我之前在rxjs的源码里看到过搞了一个队 ...

这个我了解过一点,定时器的本质是异步,而异步能优化递归的前提是:必须是尾递归。js的尾递归优化做的不太好(主要是浏览器没实现,提案已经有了),所以通过异步来实现,如果不是尾递归,中间的内部变量什么的无法舍弃,也就无法覆盖当前栈。这块我也不擅长,之前搞这个脚本的时候有研究过:https://bbs.tampermonkey.net.cn/thread-916-1-1.html
不过没搞好,速度不太满意。队列也是一种解法,而且队列对递归形式没要求,后面我考虑把那个脚本也改成队列的试试。

李恒道 发表于 2023-2-9 10:03:05

cxxjackie 发表于 2023-2-8 22:41
这个我了解过一点,定时器的本质是异步,而异步能优化递归的前提是:必须是尾递归。js的尾递归优化做的不 ...

{:4_115:}c大有实现例子吗
我还是没触发出来...
目前的代码是
var max = 1;
function getTime() {
max = max + 1;
return setTimeout(getTime, 2000);
}

getTime();

cxxjackie 发表于 2023-2-9 23:33:40

李恒道 发表于 2023-2-9 10:03
c大有实现例子吗
我还是没触发出来...
目前的代码是


这样已经实现了啊,你把延时改成0让他运行,肯定不会溢出,只是速度非常慢,因为setTimeout有最小延迟限制(我记得是4ms),每次都setTimeout的话延迟会成倍增加,可以用这种方式优化一下:
var max = 1;
function getTime() {
max = max + 1;
try {
    getTime();
} catch (e) {
    setTimeout(getTime, 0);
}
}

getTime();
用try...catch在每次溢出时才setTimeout,不想try...catch也可以设一个固定的数字。这种方式的主要缺陷在于:速度仍然有影响,而且难以处理返回值的问题(得加Promise),所以我那个脚本最后没有采用这种方案,只是单纯改成异步并加参数限制。

steven026 发表于 2023-2-10 00:36:44

cxxjackie 发表于 2023-2-8 22:41
这个我了解过一点,定时器的本质是异步,而异步能优化递归的前提是:必须是尾递归。js的尾递归优化做的不 ...

我当时尝试过把查看全局属性从for循环改成数组形式,结果还是爆栈……也可能是我写法不对

话说哥哥有什么介绍关于js底层实现方式或者代码性能优化的资料或者教程吗?
我找了很久没找到,我现在经常能遇到性能问题无法解决,最后只能强行改代码折衷实现
页: [1] 2
查看完整版本: 闲话this