Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

js-this指向 #7

Open
ahaow opened this issue May 5, 2022 · 0 comments
Open

js-this指向 #7

ahaow opened this issue May 5, 2022 · 0 comments

Comments

@ahaow
Copy link
Owner

ahaow commented May 5, 2022

this指向

五种绑定方法

  1. 默认绑定(严格/非严格)
  2. 隐式绑定
  3. 显示绑定
  4. new绑定
  5. 箭头函数

绑定规则

默认绑定

  • 非严格模式下,this会指向全局对象
  • 严格模式下,this指向undefined
// 1. 非严格模式
function f() {
  console.log(this.name)
}
var name = 'carpe'
f() // carpe

// 2. 严格模式
'use strict'
function f() {
  console.log(this.name)
}
var name = 'carpe'
f() //  Cannot read properties of undefined (reading 'name')

隐式绑定

当函数引用有上下文对象时, 隐式绑定规则会把函数中的this绑定到这个上下文对象

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

隐式丢失

被隐式绑定的函数特定情况下会丢失绑定对象,应用默认绑定,把this绑定到全局对象或者undefined上。

function foo() {
  console.log(this.name)
}
var obj = {
  name: 'carpe',
  foo: foo
}
var bar = obj.foo // this指向现在是bar 

/**
	类似于 var bar = function foo() {}
*/
var name = 'diem'
bar() // diem

显式绑定

使用 apply ,call,bind

function foo() {
  console.log(this.name)
}
let obj = {
  name: 'carpe',
  age: 24,
}
foo.apply(obj, [1,2,3]) // carpe
foo.call(obj, 1,2,3) // carpe
let fn = foo.bind(obj, 1,2,3)
fn() // carpe

new绑定

function foo(name) {
  this.name = name
}
lat bar = new foo('carpe')
console.log(bar.name) // carpe

箭头函数

箭头函数无法使用上述四条规则,而是根据外层(函数或者全局)作用域(词法作用域)来决定this

function foo() {
  return (a) => {
    console.log(this.a)
  }
}
let obj = {
  a: 2,
}
let obj2 = {
  a: 3
}
var bar = foo.apply(obj)
bar.apply(obj2)

总结

this总是指向调用该函数的对象

箭头函数

记住5要点

  1. 箭头函数不绑定this, 箭头函数的this相当于普通变量
  2. 箭头函数的this寻值行为和普遍变了相同,在作用域逐级寻找
  3. 箭头函数无法通过apply, call, bind 来直接修改(可以间接修改)
  4. 改变作用域的this的指向可以改变箭头函数的this
  5. eg. function closure(){()=>{//code }},在此例中,我们通过改变封包环境closure.bind(another)(),来改变箭头函数this的指向
<!DOCTYPE html>
<html lang="en">
<head>
    <style>
        body {
            height: 3000px;
        }
    </style>
</head>
<body>
    <script>
        var name = 'window'
        
        var personObj1 = {
            name: 'person1',
            show1: function() {
                console.log(this.name)
            },
            show2: () => {
                console.log(this.name)
            },
            show3: function() {
                return function() {
                    console.log(this.name)
                }
            },
            show4: function() {
                return () => {
                    console.log(this.name)
                }
            }
        }
        var personObj2 = {
            name: 'person2'
        }
        personObj1.show1()
        personObj1.show1.call(personObj2)
        personObj1.show2()
        personObj1.show2.call(personObj2)
        personObj1.show3()()
        personObj1.show3().call(personObj2)
        personObj1.show3.call(personObj2)()
        personObj1.show4()()
        personObj1.show4().call(personObj2)
        personObj1.show4.call(personObj2)()
      
      /**
      	1. personObj1.show1() // 隐式绑定 this指向调用者personObj1 结果:person1 
      	2. personObj1.show1.call(personObj2) // personObj2.show1() = 结果:person2
      	3. personObj1.show2() // 箭头函数绑定,this指向外层作用域 结果:window 
      	4. personObj1.show2.call(personObj2) // 箭头函数不支持call 约等于 personObj1.show2() 结果:window
      	5. personObj1.show3()() // const fn = personObj1.show3() fn() fn有自己独立的作用域 结果:window
      	6. personObj1.show3().call(personObj2) // const fn = personObj1.show3() fn.call(personObj2) 结果: person2
      	7. personObj1.show3.call(personObj2)() // const fn = personObj2.show3() fn() fn有自己独立的作用域 结果:window
      	8. personObj1.show4()() // const fn = personObj1.show4() fn() // 箭头函数 向上查找 person1
      	9. personObj1.show4().call(personObj2) // 箭头函数call不行,类似于personObj1.show4()() // person1
      	10. personObj1.show4.call(personObj2)() // personObj2.show4.()() // person2
      
      
      */
    </script>
</body>

</html>
<!DOCTYPE html>
<html lang="en">

<head>
    <style>
        body {
            height: 3000px;
        }
    </style>
</head>

<body>
    <script>
        var name = 'window'

        function Person(name) {
            this.name = name;
            this.show1 = function () {
                console.log(this.name)
            }
            this.show2 = () => console.log(this.name)
            this.show3 = function () {
                return function () {
                    console.log(this.name)
                }
            }
            this.show4 = function () {
                return () => console.log(this.name)
            }
        }

        var personA = new Person('personA')
        var personB = new Person('personB')

        personA.show1() // personA
        personA.show1.call(personB) // personB

        personA.show2() // personA
        personA.show2.call(personB) // personA

        personA.show3()()
        personA.show3().call(personB) // personB
        /**
         * let fn = personA.show3()
         * fn.call(personB)
        */

        personA.show3.call(personB)() // window

        personA.show4()() // personA
        personA.show4().call(personB) // personA
        personA.show4.call(personB)() // personB

    </script>
</body>

</html>

apply , call 使用场景

// 1. 取数组最大最小值
let arr = [1,2,3,4,5]
console.log(Math.max.apply(Math, arr))
console.log(Math.min.apply(Math, arr))

// 验证是否为数组
function isArray(arr) {
  return Object.prototype.toString.apply(arr) === '[object Array]'
}
// 可以通过toString() 来获取每个对象的类型,但是不同对象的 toString()有不同的实现,所以通过 Object.prototype.toString() 来检测

实现call

引子

var value = 1
var foo = {
  value: 2
}
function bar() {
  console.log(this.value)
}
bar.call(foo) // 2

call在此做了两点:

1. `call`改变了bar的指向,将它指给了`foo`
1. 函数执行了
var foo = {
  value: 2,
  bar: function() {
    console.log(this.value)
  }
}
foo.bar()

相当于:

1. 给`foo`增加了一个属性 `bar `  `foo.fn = bar`
2. 然后将`bar`执行 `foo.fn()`
3. 额外的`bar`属性,执行完后就删掉 `delete foo.fn`

第一步 绑定this, 立即执行,并删掉

Function.prototype.myCall = function(context) {
  context.fn = this
  context.fn()
  delete context.fn
}

var foo = {
  value: 2
}
function bar() {
  console.log(this.value)
}
bar.myCall(foo) // 2

第二步 处理参数

Function.prototype.myCall = function(context) {
  context.fn = this
  let args = []
  // 第一个是this, 所以从索引1开始遍历
  for (let i = 1; i < arguments.length; i++) {
    args.push(`arguments[${i}]`)
    // args [arguments[1], arguments[2]]
  }
  eval(`context.fn(${args})`)
 	delete context.fn
}

注意细节

  1. 如果this传入进来的是null 或者 undefined,将用window代替
  2. 如果 this 传入 基本类型,原生的call会自动用 Object()转换
  3. 函数可以有返回值

完整版

// es3
Function.prototype.myCall = function (context) {
  context = context ? Object(context) : window // 实现1 和 2
  context.fn = this
  let args = []
  for (let i = 1; i < arguments.length; i++) {
    args.push(`arguments[${i}]`)
  }
  var result = eval(`context.fn(${args})`)
  delete context.fn
  return result
}

// es6
Function.prototype.myCall = function(context) {
  context = context ? Object(context) : window
  context.fn = this
  let args = [...arguments].slice(1)
  let result = context.fn(...args)
  delete context.fn
  return result
}

手写apply

// es3
Function.prototype.myApply = function(context, arr) {
  context = context ? Object(context) : window
  context.fn = this
  let result
  if (!arr) {
    result = context.fn()
  } else {
    let args = []
    for (let i = 0; i < arr.length; i++) {
      args.push(`arr[${i}]`)
    }
    result = eval(`context.fn(${args})`)
  }
  delete context.fn
  return result
}
// es6
Function.prototype.myApply = function(context, arr) {
  context = context ? Object(context) : window
  context.fn = this
  let result
  if (!arr) {
    result = context.fn()
  } else {
    result = context.fn(...arr)
  }
  delete context.fn
  return result
}

补充

这里假设 context 对象本身没有 fn 属性,这样肯定不行,我们必须保证 fn属性的唯一性

function fnFactory(context) {
	var unique_fn = "fn";
    while (context.hasOwnProperty(unique_fn)) {
    	unique_fn = "fn" + Math.random(); // 循环判断并重新赋值
    }
    
    return unique_fn;
}

Function.prototype.myCall = function (context) {
  context = context ? Object(context) : window // 实现1 和 2
  let fn = fnFactory(context)
  context[fn] = this
  let args = []
  for (let i = 1; i < arguments.length; i++) {
    args.push(`arguments[${i}]`)
  }
  var result = eval(`context[fn](${args})`)
  delete context[fn]
  return result
}

Function.prototype.myCall = function(context) {
  context = context ? Object(context) : window
  let fn = Symbol()
  context[fn] = this
  let args = [...arguments].slice(1)
  let result = context[fn](...args)
  delete context[fn]
  return result
}

Function.prototype.myApply = function(context, arr) {
  context = context ? Object(context) : window
  let fn = Symbol()
  context[fn] = this
  let result
  if (!arr) {
    result = context[fn]()
  } else {
    result = context[fn](...arr)
  }
  delete context[fn]
  return result
}

bind

bind()方法会创建一个新函数,当这个新函数被调用的时候,它的this值是传递给bind()的第一个函数,传入bind方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数调用原函数

bind返回的绑定函数也能使用 new操作符创建对象:这种行为就像把原函数当成构造器,提供的this 值被忽略,同时调用时的参数被提供给模拟函数。

bindapplycall 的区别是前者返回一个绑定上下文的函数, 后则是直接执行函数

var value = 'value'
var foo = {
  value: 'foo'
}

function bar(name, age) {
  return {
    value: this.value,
    name: name,
    age: age,
  }
}

console.log(bar('carpe', 24)) // value, carpe, 24
console.log(bar.apply(foo, ['carpe', 24])) // foo, carpe, 24
console.log(bar.call(foo, 'carpe', 24)) // foo, carpe, 24

let bindFn1 = bar.bind(foo, 'hanzo') 
console.log(bindFn1()) // {value: 1, name: 'hanzo', age: undefined}
console.log(bindFn1(20)) // {value: 1, name: 'hanzo', age: 20}

上面的例子表示bind的特性

  1. 可以指定this
  2. 返回一个函数
  3. 可以传入参数
  4. 柯里化

场景1

var nickname = 'carpe'

function Person(name) {
  this.nickname = name
  this.distractedGreeting = function() {
    setTimeout(function() {
      console.log("Hello, my name is " + this.nickname)
    }, 500)
  }
}
var person = new Person('hanzo')
person.distractedGreeting() // carpe

// 解析
setTimeout是全局环境下执行,所以this指向window

// 方法

1. 缓存this
this.distractedGreeting = function() {
  let self = this
  setTimeout(function() {
    console.log("Hello, my name is " + self.nickname)
  }, 500)
}

2. 箭头函数
this.distractedGreeting = function() {
  let self = this
  setTimeout(() => {
    console.log("Hello, my name is " + self.nickname)
  }, 500)
}

3. bind
this.distractedGreeting = function() {
  let self = this
  setTimeout(function() {
    console.log("Hello, my name is " + self.nickname)
  }.bind(this), 500)
}

bind的柯里化

bind有柯里化,所以bind本身也是闭包的一种使用场景

模拟实现

bind四个特性

  1. 可以指定this
  2. 返回一个函数
  3. 可以传入参数
  4. 柯里化
// 第一点 可以使用 apply 或者 call
Function.prototype.myBind = function(context) {
  let self = this
  // 实现第3点,因为第1个参数是指定的this,所以只截取第1个之后的参数
  let args = Array.prototype.slice.call(arguments, 1)
  return function() { // 2. 返回一个函数
    // 实现第4点,这时的arguments是指bind返回的函数传入的参数
    var bindArgs = Array.prototype.slice.call(arguments)
    return self.apply(context, args.concat(bindArgs)) // 1. 可以指定`this`
  }
}

上述代码已完成四点功能,但bind还有一个特性

一个绑定函数也能使用new操作符创建对象,这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

案例

var value = 2
var foo = {
  value: 1,
}
function bar(name, age) {
  this.habit = 'shoping'
  console.log(this.value)
  console.log(name)
  console.log(age)
}
bar.prototype.friend = 'genji'
var bindFoo = bar.bind(foo, 'hanzo')
bindFoo()

var obj = new bindFoo()
console.log(obj) // this.value = undefined

在new的实现中会生成一个新的对象,这时候的this指向obj

Function.prototype.myBind = function(context) {
  let self = this
  let args = Array.prototype.slice.call(arguments, 1)
  // 注释3
  let fNOP = function () {};
  let fBound = function() {
    let bindArgs = Array.prototype.slice.call(arguments)
    // 注释1
    return self.apply(
    	this instanceof fNOP ? this : context,
      args.concat(bindArgs)
    )
  }
  // 注释2
  fNOP.prototype = this.prototype
  // 注释3
  fBound.prototype = new fNOP();
  return fBound
}

注释1: 当作为构造函数时,this 指向实例,此时 this instanceof fNOP结果为true,可以让实例获得来自绑定函数的值,即上例中实例会具有 habit 属性。

注释2: 修改返回函数的 prototype为绑定函数的prototype,实例就可以继承绑定函数的原型中的值,即上例中 obj可以获取到bar原型上的friend

注释3:

  • 上面实现中fNOP.prototype = this.prototype有一个缺点,直接修改 fNOP 的时候,也会直接修改 `this.prototype,
  • 解决方案是用一个空对象作为中介,把 fBound.prototype赋值为空对象的实例(原型式继承).
  • 这边可以直接使用ES5的 Object.create()方法生成一个新对象,不过 bind 和 Object.create()都是ES5方法

注意事项

如果调用bind的不是函数,这时候要抛出异常

// 完整代码
Function.prototype.bind2 = function(context) {
  if (typeof this !== 'function') {
    throw new Error("Function.prototype.bind - what is trying to be bound is not callable")
  }
  let self = this
  let args = Array.prototype.slice.call(arguments, 1)
  let fNOP = function() {}
  
  let fBound = function() {
    let bindArgs = Array.prototype.slice.call(arguments)
    return self.apply(
    	this instanceof fNOP ? this : context,
      args.concat(bindArgs)
    )
  }
  fNOP.prototype = this.prototype
  fBound.prototype = new fNOP()
  return fBound
}

new原理及实现

new运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例

function Car(color) {
  this.color = color
}
Car.prototype.start = function() {
	console.log(this.color + 'car start')
}
var car = new Car('red')
car.color // red 访问构造函数属性
car.start() // red car start 访问原型的属性

new创建实例的2个特性

  1. 访问到构造函数的属性
  2. 访问到原型里面的属性

模拟实现

当代码new Car()的时候

1. 一个继承自`Car.prototype`的新对象被创建
1. 使用指定的参数调用构造函数`Car` , 并将 `this` 绑定到新创建的对象
1. `new Car` 等同于 `new Car()`  也就是没有指定参数列表,`Car` 不带任何参数调用的情况
1. 由构造函数返回的对象就是`new`表达式的结果,如果构造函数没有显式返回一个对象,则使用步骤1创建的对象

实现第一步

new返回一个新对象,通过obj._proto_ = Con.prototype 继承构造函数的原型,同时通过Cona.apply(obj, arguments)调用父构造函数实现继承,获取构造函数上的属性

function Car(color) {
  this.color = color
}
Car.prototype.start = function() {
	console.log(this.color + 'car start')
}
function create() {
  // 1. 创建一个空对象
  let obj = {}
  // 2. 获取构造函数,arguments中去掉第一个参数
  let Con = [].shift.call(arguments) // Con = Car
  // 3. 链接到原型,obj 可以访问到构造函数的属性
  obj._proto_ = Con.prototype // 这一步,就可以访问 Car.prototype里面的属性了
  // 4. 绑定this实现继承,obj可以访问到构造函数中的属性
  Con.apply(obj, arguments)
  // 5. 返回对象
  return obj
}
var car = creaet(Car, 'red')
car.color // red
car.start() // red car start

构造函数返回值

构造函数返回值有如下三种情况:

  1. 返回一个对象
  2. 没有return, 即返回undefined
  3. 返回undefined以外的基本类型
// 1.返回一个对象
function Car(color, name) {
  this.color = color
  return {
    name: name
  }
}
var car = new Car('black', 'benzi')
car.color // undefined
car.name // benzi

// 2.没有`return`, 即返回`undefined`
function Car(color, name) {
  this.color = color
}
var car = new Car('black', 'benzi')
car.color // black
car.name // undefined

// 3.返回`undefined`以外的基本类型
function Car(color, name) {
  this.color = color
  return 'new Car'
}
var car = new Car('black', 'benzi')
car.color // black
car.name // undefined

完整版

function create() {
  // 1. 创建空对象
  let obj = {}
  // 2. 获取到传入进来的构造函数
  let Con = [].shift.call(argument)
  // 3. 将获取到传入进来的构造函数的原型 挂到 obj
  obj._proto_ = Con.prototype
  // 4. 绑定this继承 使得obj能够获取到构造函数的构造属性
  let result = Con.apply(obj, arguments)
  // 5. 返回obj
  // 6. 优先返回构造函数返回的对象
  return result instanceof Object : result : obj
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant