史上最全的 js中this详解

前言

转载于 https://zhuanlan.zhihu.com/p/90833853

我现在依然坚信一些js开发者虽然可以熟练的使用js内置函数和对象,也能够封装出出色函数,但是 问题来了,你们在使用非箭头函数的时候this的创建和this判断是不是模糊不清,哈哈我也是,不过我最近总结了一些this的创建和this判断的资料,也包含了自己一些观点,作为分享,文章比较长,请耐心看看,如果有什么不足的地方或者理解错误的地方请留言一下,到时候咱们一起探讨 。

目录

  • 为什么要用this
  • 对this的误解
  • this到底是什么
  • 全面解析this

为什么要用this

来直接上代码

function foo(){
     console.log(`我本身属性a是   ${this.a}`)
 }
 
 var bar ={
     a:2,
     foo:foo
 }
 var baz={
     a:4,
     foo:foo
 }
 bar.foo();//我本身属性a是  2
 baz.foo()://我本身属性a是  4

小伙伴们是不是已经在这个简单的代码中发现了 刚发foo只定义了一次,去可以被不同的对象引用,实现了代码共享

对this的误解

一般对this的误解分为两个方面

  • 1 this是指向当前函数的本身
  • 2 this 指向的是当前函数的 作用域

接下来咱们看看代码中的是调试

this是指向当前函数的本身

下面代码中大家要理解函数的多面性,多个身份

  • 普通的函数
  • 普通的对象
  • 构造函数

接下来讲用到函数的是两个身份普通函数、普通对象, 看代码()

function foo(){
    this.count++
}
var count=0;
foo.count=0;
for(var i=0;i<5;i++){
    
    foo()
}
console.log(foo.count)//0
console.log(count)//5

从打印的结果上来看显然,this指向的不是本身函数,当然咱们一般看到这类的问题咱们就会绕道而行,看代码

function foo(){
    this.count++
}
var bar={
    count:0
}
foo.count=0;
for(var i=0;i<5;i++){
    
    foo.call(bar)
}
console.log(bar.count)//5
console.log(count)//0

虽然这种解决方案很好,也会有其他的解决方案,但是我们还是不理解this的问题,心里还是有种不安之感

this 指向的是当前函数的 作用域

接下来讲用到函数的是两个身份普通函数、普通对象, 看代码()

function foo(){
     var num=2;
     console.log(this.num)
 }
 var num=0;
 foo()//0

咱们看到代码的执行结果后,发现this指向的并不是该函数的作用域。

this到底是什么

this是在函数调用的时候绑定,不是在函数定义的时候绑定。它的上下文取决于函数调用时的各种条件,函数执行的时候会创建一个活动记录,这个记录里面包含了该函数中定义的参数和参数,包含函数在哪里被调用(调用栈)…,this就是其中的一个属性。 来看图

图中咱们看到this是在函数执行的时候创建的。

全面解析this

前面几步咱们已经确定的this的创建和this的指向的误区,接下啦咱们要看看this的绑定的规则,分为4个规则。

  • 默认绑定
  • 隐式绑定(上下文绑定)
  • 显式绑定
  • new 绑定

默认绑定

默认绑定的字面意思就是,不满足其他的绑定方式,而执行的绑定规则。默认绑定会把this绑定到全局对象(是一个危险的操作,文章后面会说为什么) 看代码

function foo(){
     var num=2;
     this.num++
     console.log(this.num)
 }
 var num=0;
 foo()//1

上面代码中就实现了默认绑定,在foo方法的代码块中操作的是window.num++。

隐式绑定(上下文绑定)

定义:

函数被调用的位置有上下文,或者是该函数的引用地址是不是被某个对象的属性引用,并通过对象的属性直接运行该函数。如果出现上述的情况,就会触发this的隐式绑定,this就会被绑定成当前对象 看代码

function foo(){
    console.log(this.name)
}
var bar={
    name:'shiny',
    foo:foo
}
bar.foo()//shiny

要需要补充一点,不管你的对象嵌套多深,this只会绑定为直接引用该函数的地址属性的对象,看代码

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny',
    foo:foo
}
var red={
    name:'red',
    obj:shiny
    
}
red.obj.foo()//shiny

隐式绑定的丢失

先看代码

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny',
    foo:foo
}
function doFoo(fn){
    fn()
}
doFoo(shiny.foo)//undefind

大家知道函数参数在函数执行的时候,其实有一个赋值的操作,我来解释一下上面的,当函数doFoo执行的时候会开辟一个新的栈并被推入到全局栈中执行,在执行的过程中会创建一个活动对象,这个活动对象会被赋值传入的参数以及在函数中定义的变量函数,在函数执行时用到的变量和函数直接从该活动对象上面取值使用。 看图 doFoo的执行栈

fn的执行栈

看下面原理和上面一样通过赋值,导致隐式绑定的丢失,看代码

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny',
    foo:foo
}
var bar = shiny.foo
bar()//undefined

大家是不是已经明白了为什么是undefined,来解释一波,其实shiny的foo属性是引用了foo函数的引用内存地址,那么有把foo的引用地址赋值给了 bar 那么现在的bar的引用地址个shiny.foo的引用地址是一个,那么执行bar的时候也会触发默认绑定规则因为没有其他规则可以匹配,bar函数执行时,函数内部的this绑定的是全局变量。

看下满的引用地址赋值是出现的,奇葩 隐式绑定丢失,看代码

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny',
    foo:foo
}
var red={
    name:'red'
}
(red.foo=shiny.foo)()//undefined

赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是 foo() 而不是 p.foo() 或者 o.foo()。根据我们之前说过的,这里会应用默认绑定。

显式绑定

call、apply绑定

javascript,在Function的porpertype上提供了3个方法来强行修改this,分别是 call、apply、bind,大家经常用的莫过于call和apply了,这两个函数的第一个参数,都是需要执行函数绑定的this,对于apply只有连个参数,第二个参数是一个数组,这个数组是要传入执行函数的参数,而call可以跟很多参数,从第二个参数起都会被传入到要执行函数的参数中

看代码

function foo(){
   console.log(this.age)
}
var shiny={
   age:20
}
foo.call(shiny)//20

function bar(){
console.log(this.age)
}
var red={
age:18
}
bar.apply(red)//18

这两个方法都是显式的绑定了tihs

硬绑定:

类似与 bind方法行为,是显式绑定的一种方式

function foo(b){
  return this.a+b
}
var obj={
  a:2
}
function bind(fn,obj){
  return function(){
     return fn.apply(obj,arguments)
  }
}
bind(foo,obj)(3)//5

语言解释: 通过apply + 闭包机制 实现bind方法,实现强行绑定规则

API调用的“上下文” 第三方库或者寄生在环境,以及js内置的一些方法都提供了一下 content 上下文参数,他的作用和 bind一样,就是确保回调函数的this被绑定

function foo (el){
  console.log(el,this.id)
}
var obj ={
 id:'some one'
};
[1,2,4].forEach(foo,obj)
// 1 some one 2 some one 4 some one

new 绑定

说道new 大家都会想到js的构造函数,咱们想不用着急new 绑定this的问题,咱们先看看咱们对js的构造函数的误解,传统面向类的语言中的构函数和js的构造函数时不一样

  • 传统面向类的语言中的构函数,是在使用new操作符实例化类的时候,会调用类中的一些特殊方法(构造函数)

  • 很多人认为js中的new操作符和传统面向类语言的构造函数是一样的,其实有很大的差别

  • 从新认识一下js中的构造函数,js中的构造函数 在被new操作符调用时,这个构造函数不属于每个类,也不会创造一个类,它就是一个函数,只是被new操作符调用。

  • 使用new操作符调用 构造函数时会执行4步

  • 创建一个全新的对象

  • 对全新的对象的__proto__属性地址进行修改成构造函数的原型(prototype)的引用地址

  • 构造函数的this被绑定为这个全新的对象

  • 如果构造函数有返回值并且这个返回值是一个对象,则返回该对象,否则返回当前新对象

咱们了解了js new 操作符调用构造函数时都做了些什么,哪么咱们就知道构造函数里面的this是谁了

代码实现

function Foo(a){
  this.a=a
}
var F = new Foo(2)
console.log(F.a)//2

绑定规则的顺序

咱们在上面了解this绑定的4大规则,那么咱们就看看这4大绑定规则的优先级。

默认绑定

咱们根据字面意思,都能理解只有其余的3个绑定规则无法触发的时候就会触发默认绑定,没有比较意义

显式绑定 VS 隐式绑定

看代码

function foo(){
    console.log(this.name)
}
var  shiny={
    name:'shiny',
    foo:foo
}
var red={
    name:'red'
}

shiny.foo()//shiny
shiny.foo.call(red)// red
shiny.foo.apply(red)// red
shiny.foo.bind(red)()//red

显然在这场绑定this比赛中,显式绑定赢了隐式绑定

隐式绑定 VS new 操作符绑定

看代码

function  foo(name){
    this.name=name
}
var shiny={
    foo:foo
}
shiny.foo('shiny')
console.log(shiny.name)//shiny

var red = new shiny.foo('red')
console.log(red.name)//red

显然在这场绑定this比赛中new 操作符绑定赢了隐式绑定

显式绑定(硬绑定) VS new 操作符绑定

使用call、apply方法不能结合new操作符会报错误

但是咱们可以是bind绑定this来比较 显式绑定和new操作符的绑定this优先级。 看代码

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny'
}

var bar = foo.bind(shiny)
var obj = new bar();
console.log(obj.name)// undefind

显然 new操作符绑定 战胜了 显式绑定

this的判断

咱们在上面已经了解 4个绑定this的优先级。咱们可以列举出来

  • 1 判断该函数是不是被new操作符调用,有的话 this就是 构造函数运行时创建的新对象 var f = new foo()
  • 2 判断 函数是不是使用显式绑定 call、apply、bind,如果有,那么该函数的this就是 这个三个方法的第一个参数 foo.call(window)
  • 3 判断该函数是不是被一个对象的属性引用了地址,该函数有上下文(隐式绑定),在函数执行的时候是通过该对象属性的引用触发,这个函数的this就是当前对象的。 obj.foo();
  • 4 上面的三种都没有的话,就是默认绑定,该函数的this就是全局对象或undefined(严格模式下)

绑定例外

规则总是会有意外的,this绑定也是会有的,某些场面的绑定也是会出乎意料的,有可能触发了默认绑定 看代码

function foo(){
    console.log(name)
}
var name ='shiny'
foo.call(null)//shiny
foo.call(undefined)//shiny
var bar = foo.bind(null)
var baz = foo.bind(undefined)
bar()//siny
baz()//siny

把 null、undefined通过 apply、call、bind 显式绑定,虽然实现可默认绑定,但是建议这么做因为在非严格的模式下会给全局对象添加属性,有时候会造成不可必要的bug。

更安全的this

咱们从上面知道在非严格模式下 默认绑定是并操作this的话会该全局对象添加属性,这样的操作是有风险性的

function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 我们的空对象
var ø = Object.create( null );
// 把数组展开成参数
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3

es6中的this

在es5及一下版本,我们被this深深的困惑,但是看完了上面的文章,应该判断this没有关系,但是 重点来了 es6的this可以通过箭头函数直接绑定在该函数的执行的作用域上。 看代码

function foo(){
     return ()=>{
          console.log(this.name)
     }
 }
 var obj ={
     name:'obj'
 }
  var shiny ={
     name:'shiny'
 }
 var bar = foo.call(obj);
 bar.call(shiny)// foo

我们看到箭头函数的this被绑定到该函数执行的作用域上。

咱们在看看 js内部提供内置函数使用箭头函数

function foo() {
    setTimeout(() => {
    // 这里的 this 在此法上继承自 foo()
    console.log( this.a );
    },100);
}
var obj = {
    a:2
};
foo.call( obj ); // 2

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oGcI8woD-1693201398501)(data:image/svg+xml;utf8, )]

箭头函数可以像 bind(…) 一样确保函数的 this 被绑定到指定对象,此外,其重要性还体 现在它用更常见的词法作用域取代了传统的 this 机制。实际上,在 ES6 之前我们就已经 在使用一种几乎和箭头函数完全一样的模式。

function foo() {
var self = this; // lexical capture of this
setTimeout( function(){
    console.log( self.a );
    }, 100 );
}
var obj = {
    a: 2
};
foo.call( obj ); // 2

虽然 self = this 和箭头函数看起来都可以取代 bind(…),但是从本质上来说,它们想替 代的是 this 机制。 如果你经常编写 this 风格的代码,但是绝大部分时候都会使用 self = this 或者箭头函数。 如果完全采用 this 风格,在必要时使用 bind(…),尽量避免使用 self = this 和箭头函数。

如有不足,在评论中提出,咱们一起学习

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Linux内核是一个开源的操作系统内核,拥有非常丰富的配置选项。以下是对Linux内核配置选项的详细解释: 1. 进程管理:通过配置选项,可以选择支持多进程、多线程、多任务等特性。可以设置进程调度策略、锁定内存区域等。 2. 文件系统支持:Linux内核支持多种文件系统,包括Ext2、Ext3、Ext4、XFS等。配置选项可以选择需要支持的文件系统类型。 3. 设备驱动支持:通过配置选项可以选择支持的硬件设备驱动,比如网卡驱动、声卡驱动、USB驱动等。 4. 内存管理:可以配置页面大小、内存映射方式、虚拟内存管理等相关选项,以提高内存的利用效率。 5. 网络支持:可以选择支持不同的网络协议栈,比如TCP/IP、UDP等。还可以通过配置选项设置网络参数,如MTU大小、网络连接数等。 6. 安全性配置:可以选择开启不同的安全特性,如SELinux、AppArmor等。还可以对访问控制进行细粒度的配置。 7. 调试支持:通过配置选项可以选择是否开启调试信息和调试功能,以便于开发和排查问题。 8. 电源管理:可以选择支持电源管理功能,以延长电池寿命或节约电能。 9. 定时器支持:可以配置内核定时器的精度和分辨率,以满足不同应用场景的要求。 10. 文件系统特性:可以选择开启各种文件系统的特性,如日志、快照、压缩等。 总而言之,Linux内核配置选项非常丰富,可以根据不同的需求和环境进行灵活配置,以获得最佳的性能和功能。 ### 回答2: Linux内核是一个自由开源的操作系统内核,可运行在各种计算机硬件平台上。内核配置是指根据特定需求对内核进行定制和编译,以满足用户对系统功能和性能的要求。 史上最全的Linux内核配置详解包括了众多的选项和参数,可以根据用户的需求进行选择。其包括了文件系统支持、设备驱动、网络协议、性能优化等方面的配置。 在文件系统支持方面,内核提供了多个选项,如EXT4、XFS、Btrfs等,用户可以根据需要选择合适的文件系统。此外,还可以选择支持的文件系统功能,如日志系统、快照、压缩等。 设备驱动是Linux内核的一个重要组成部分,内核提供了大量的设备驱动选项,包括网络设备、声卡、USB设备、磁盘控制器等。用户可以根据自己的硬件配置选择相应的驱动。 网络协议是支持网络通信的关键,内核提供了TCP/IP、IPv6、IPSec等多种网络协议的支持。用户可以根据网络环境的需求选择启用相应的协议。 内核配置还包括了一些性能优化的选项,如预排定、缓存管理、断处理等。用户可以根据系统的性能需求选择相应的优化选项。 另外,内核配置还包括了调试和跟踪选项,可以帮助开发人员定位和解决问题。 总之,史上最全的Linux内核配置详解提供了众多选项和参数供用户选择和定制,以满足各种不同的需求。用户可以根据自己的需求选择适合自己的内核配置,以获得更好的系统性能和功能支持。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值