上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
12下一页
返回列表 发新帖

闲话this

[复制链接]
  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    857

    回帖

    1356

    积分

    荣誉开发者

    积分
    1356

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

    发表于 2023-2-5 23:46:21 | 显示全部楼层 | 阅读模式

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

    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上。看看这个例子:

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

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

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

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

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

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

    类中的this

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

    const xhr = new XMLHttpRequest();

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

    const arr = new Array(1, 2, 3); // const arr = [1, 2, 3];

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

    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可以被改变,包括显式和隐式的改变,其中隐式改变是最容易被忽视的地方,看看这个例子:

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

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

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

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

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

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

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

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

    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的例子正常工作,可以改成:

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

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

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

    劫持时的this

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

    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,那不传递也没影响,但只要你不想自找麻烦,传递了总不会犯错。
    还有另一种写法:

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

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

  • TA的每日心情
    开心
    2023-2-28 23:59
  • 签到天数: 191 天

    [LV.7]常住居民III

    620

    主题

    5084

    回帖

    5958

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    5958

    荣誉开发者管理员油中2周年生态建设者喜迎中秋

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

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情
    开心
    2024-3-13 10:14
  • 签到天数: 211 天

    [LV.7]常住居民III

    284

    主题

    3810

    回帖

    3739

    积分

    管理员

    积分
    3739

    管理员荣誉开发者油中2周年生态建设者喜迎中秋油中3周年挑战者 lv2

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

    难受,腾讯云香港不知道怎么了。。。。有钱了,换更好的线路

    我这里是正常的(40的ms 0丢包,哥哥可以看看你那里)
    上不慕古,下不肖俗。为疏为懒,不敢为狂。为拙为愚,不敢为恶。/ 微信公众号:一之哥哥
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    857

    回帖

    1356

    积分

    荣誉开发者

    积分
    1356

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

    发表于 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实例化更快,因为每个实例都需要独立内存,而同值的字面量实际上是同一个:
    1. new Number(123) !== new Number(123)
    2. 123 === 123
    复制代码

    字面量具有类实例的所有特性,甚至在Object原型上添加的属性和方法,同样会反映到字面量上:
    1. Object.prototype.__test__ = 'test';
    2. console.log((123).__test__);
    复制代码

    从这个角度来说,字面量实际上是一种特殊的对象,也算是类的一个特殊实例。这里只是作为类和实例之间关系的例子,可能不太贴切,但不影响理解。
    回复

    使用道具 举报

  • TA的每日心情
    开心
    2023-2-28 23:59
  • 签到天数: 191 天

    [LV.7]常住居民III

    620

    主题

    5084

    回帖

    5958

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    5958

    荣誉开发者管理员油中2周年生态建设者喜迎中秋

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

    还真是特殊的对象...
    JS太乱了
    卧槽
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情
    开心
    2023-2-28 23:59
  • 签到天数: 191 天

    [LV.7]常住居民III

    620

    主题

    5084

    回帖

    5958

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    5958

    荣誉开发者管理员油中2周年生态建设者喜迎中秋

    发表于 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大知道正确写法吗
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    857

    回帖

    1356

    积分

    荣誉开发者

    积分
    1356

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

    发表于 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
    不过没搞好,速度不太满意。队列也是一种解法,而且队列对递归形式没要求,后面我考虑把那个脚本也改成队列的试试。
    回复

    使用道具 举报

  • TA的每日心情
    开心
    2023-2-28 23:59
  • 签到天数: 191 天

    [LV.7]常住居民III

    620

    主题

    5084

    回帖

    5958

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    5958

    荣誉开发者管理员油中2周年生态建设者喜迎中秋

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

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

    getTime();
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    857

    回帖

    1356

    积分

    荣誉开发者

    积分
    1356

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

    发表于 2023-2-9 23:33:40 | 显示全部楼层
    李恒道 发表于 2023-2-9 10:03
    c大有实现例子吗
    我还是没触发出来...
    目前的代码是

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

    10. getTime();
    复制代码

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

    使用道具 举报

  • TA的每日心情
    慵懒
    15 小时前
  • 签到天数: 603 天

    [LV.9]以坛为家II

    30

    主题

    523

    回帖

    1379

    积分

    荣誉开发者

    积分
    1379

    荣誉开发者新人进步奖油中2周年生态建设者新人报道挑战者 lv2油中3周年喜迎中秋

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

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

    话说哥哥有什么介绍关于js底层实现方式或者代码性能优化的资料或者教程吗?
    我找了很久没找到,我现在经常能遇到性能问题无法解决,最后只能强行改代码折衷实现
    回复

    使用道具 举报

    发表回复

    本版积分规则

    快速回复 返回顶部 返回列表