this指针、作用域理解

369 阅读6分钟

this理解

1.全局代码眼中的this

  • 全局范围内的this会指向全局对象,浏览器中的window
alert(this); // window

2.函数代码中的this(默认绑定)

  • this指向全局对象,即window,如果声明了严格模式,则是undefined
function foo(a) {
    this.a = a;
}
foo(2);
alert(x); // 全局变量2

// 严格模式下
var a = 1
function fn() {
  var a = 2
  console.log(this.a) // console what ?
}
fn();// undefined

3.对象中的函数调用this

  • this指向person对象,即当前对象
var name = ‘hello’;
var person = {
    name: 'word',
    test: function(a) {
        var saytest = function(a) {
             console.log(this.name+ 'nihao' + a);
        };
        saytest(a);
    }
}
person.test('hello, word');// word nihao hello word

4.隐式绑定(属性访问调用)

1. 如果函数调用时,前面存在调用它的对象,那么this就会隐式绑定到这个对象上,调用堆栈的上一级
// 第一种情况
function fn () {
  console.log(this.a)
}

const obj = {
  a: 1
}

obj.fn = fn
obj.fn() // 1

// 第二种情况
function fn () {
  console.log(this.a)
}

const obj1 = {
  a: 1,
  fn
}

const obj2 = {
  a: 2,
  obj1
}

obj2.obj1.fn() // 1
2.隐式绑定失效例子
// 第一种 是前面提过的情况
const obj1 = {
  a: 1,
  fn: function() {
    console.log(this.a)
  }
}

const fn1 = obj1.fn // 将引用给了 fn1,等同于写了 function fn1() { console.log(this.a) }
fn1() // 所以这里其实已经变成了默认绑定规则了,该函数 `fn1` 执行的环境就是全局环境

// 第二种 setTimeout
setTimeout(obj1.fn, 1000) // 这里执行的环境同样是全局

// 第三种 函数作为参数传递
function run(fn) {
  fn()
}
run(obj1.fn) // 这里传进去的是一个引用

// 第四种 一般匿名函数也是会指向全局的
var name = 'The Window';
var obj = {
    name: 'My obj',
    getName: function() {
        return function() { // 这是一个匿名函数
            console.log(this.name)
        };
    }
}
obj.getName()()

// 第五种 函数赋值也会改变 this 指向
// 第六种 IIFE

5.this显式绑定(call,bind,apply)

1.通过显式绑定强行绑定this上下文
let obj1 = {
    name: '你在说什么'
};
let obj2 = {
    name: '我不能说吗'
};
let obj3 = {
    name: 'echo'
}
var name = '哈哈哈哈';

function fn() {
    console.log(this.name);
};
fn(); //哈哈哈哈
fn.call(obj1); //你在说什么
fn.apply(obj2); //我不能说吗
fn.bind(obj3)(); //echo

6.new

  • 实现一个new函数
// new 关键字会进行如下操作
// 1. 创建一个空的javascript对象
// 2. 链接该对象到令一个对象,如果函数 constructor 里没有返回对象的话,this 指向的是 new 之后得到的实例
// 3. 将新创建的对象作为this上下文
// 4. 如果改函数没又返回对象,这返回this

function mynew() {
    if(typeof fn !== 'function') throw new Error('fn must be a function.');
    mynew.tatget = fn;
    const temp = Object.create(fn.prototype);
    const res = fn.apply(temp,...args);
    retunrn _.isObject(res) ? res : temp;
}

7.箭头函数

  • 箭头函数情况比较特殊,编译期间确定的上下文,不会被改变,哪怕你new,指向的就是上一层的上下文,this本身是没有this的,继承的是最外层的
function fn() {
    return {
        b: () => {
            console.log(this);
          }
    }
}
fn().b() // window 全局对象
fn().b.bind(1)() // window 全局对象
fn.bind(2)().b.bind(3)()  // 2

call、apply与bind有什么区别?

1.call、apply与bind都用于改变this绑定,但call、apply在改变this指向的同时还会执行函数,而bind在改变this后是返回一个全新的boundFcuntion绑定函数,这也是为什么上方例子中bind后还加了一对括号 ()的原因。

2.bind属于硬绑定,返回的 boundFunction 的 this 指向无法再次通过bind、apply或 call 修改;call与apply的绑定只适用当前调用,调用完就没了,下次要用还得再次绑。

3.call与apply功能完全相同,唯一不同的是call方法传递函数调用形参是以散列形式,而apply方法的形参是一个数组。在传参的情况下,call的性能要高于apply,因为apply在执行时还要多一步解析数组

8.优先级

// 隐式 vs 默认 -> 结论:隐式 > 默认
function fn() {
  console.log(this)
}

const obj = {
  fn
}

obj.fn() //  fn

// 显式 vs 隐式 -> 结论:显式 > 隐式
obj.fn.bind(5)() // 5

// new vs 显式 -> 结论:new > 显式
function foo (a) {
    this.a = a
}

const obj1 = {}

var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a) // 2

// new
var baz = new bar(3)
console.log( obj1.a ) // 2
console.log( baz.a ) // 3

// 所以得出结论 「new 绑」 > 「显绑」 > 「隐绑」 > 「默认绑定」


作用域

执行上下文,主要分为

1. 变量对象(VO)/活动对象(AO)👆

var a = '1';
function sum() {
    if(a=== '1') {
        a = 2;
    }else {
        a = 3;
    }
}
sum();
// 函数sum的作用域链包含了2个对象,一个是自己的变量对象,另一个是全局上下文的变量对象

//局部作用域中定义的变量可以用于局部上下文中替换全局变量

var test = ‘hello’;

function changeTest() {
    let antoTest = ‘hello1’;
    
    funciton swapTest() {
        let tempTest = antoTest;
        antoTest = test;
        test = tempTest;
        // 可以访问到color,antoTest、tempTest;
    }
    swapTest(); // 可以访问到test和antoTest
}
changeTest(); //只能访问到test

2.作用域链👆

  • 每一个执行上下文都与一个作用域链相关联。作用域链是一个对象组成的链表,求值标识符的时候会搜索它。当控制进入执行上下文时,就根据代码类型创建一个作用域链,并用初始化对象(VO/AO)填充。执行一个上下文的时候,其作用域链只会被 with 声明和 catch 语句所影响
var a = 20;
function foo(){
    var b = 100;
    alert( a + b );
}
foo();

// 两个阶段:创建 - 执行

// --------------------------- 创建 ------------------------------

// 模拟 VO/AO 对象
AO(foo) {
  b: void 0
}

// [[scope]] 不是作用域链,只是函数的一个属性(规范里的,不是实际实现)
// 在函数创建时被存储,静态(不变的),永远永远,直到函数被销毁
foo.[[scope]]: [VO(global)]

VO(global) {
  a: void 0,
  foo: Reference<'foo'>
}
  
// --------------------------- 调用 ------------------------------
  
// 可以这么去理解,近似的用一个 concant 模拟,就是将当前的活动对象放作用域链最前边
Scope = [AO|VO].concat([[Scope]])
  
  
// ---------------------------- 执行时 EC --------------------------------
EC(global) {
  VO(global) {
    a: void 0,
    foo: Reference<'foo'>
  },
  Scope: [VO(global)],
  // this
}
  
EC(foo) {
  AO(foo) { // 声明的变量,参数
    b: void 0
  },
  Scope: [AO(foo), VO(global)] // 查找顺序 -> RHS LHS  
}

3. 变量声明 👆

1.使用var的函数作用域声明

在var声明变量的时候,变量会被自动添加到上下文,在函数中醉接近的上下文就是函数的局部上下文

function sum(num1,num2) {
    var sum = num1 + num2;
    return sum;
}

let res = sum(1,2); // 3
console.log(sum);报错: sum在这里不是有效的变量 

var name = ‘hello’;
//等价于
name = ‘hello’;
var name;

// 在声明之前打印变量,可以验证变量会提升
console.log(name); // undefined
var name = 'hello';

function() {
    console.log(name); // undefined
    var name = 'hello';
}
2.使用let的会计作用域声明
  • let 关键字和var相似,但是它的作用域是块级的,if块,while块、function块
// let
if(true){
    let a;
} 
console.log(a); // ReferenceError: a没有定义

while (true) {
    let b;
}
console.log(b); // ReferenceError: a没有定义

function foo() {
    let c;
}
console.log(c); // ReferenceError: a没有定义



// var
var a;
var a; // 不会报错

{
    let b;
    let b;
}
// SyntaxError: 标识符b 已经声明过了

总结: 1.let 的行为非常适合在循环引用中声明迭代变量,使用var 声明的迭代变量会泄漏到循环外部, 2. let 在js运行中也会被提升,但是由于暂时性死区的缘故,实际上不能在声明之前使用let变量

for(var i=0;i<10;i++) {}
console.log(i);// 10

for(var j=0;j<10;j++) {}
console.log(j);// j没有定义
3.使用const的常量声明
  • 使用const 声明的变量必须同时初始化伟某个值.一旦被声明,都不能在重新赋值;
// const
if(true){
    const a = 1;
} 
console.log(a); // ReferenceError: a没有定义

while (true) {
    const b = 1;
}
console.log(b); // ReferenceError: a没有定义

function foo() {
    const c = 1;
}
console.log(c); // ReferenceError: a没有定义
  • const 声明只能应用到顶级的原语或者对象;另一个意思说,赋值为对象的const 变量不能在被重新赋值为其他引用值,当对象的键泽不受限制;
const obj = {};
obj = {}; // TypeError:给常量赋值

const obj2 ={};
obj2.name ='hello';
console.log(obj2.name); // hello

// 让整个对象不能修改,可以使用Object.freeze(),这样在给属性赋值时候虽然不会报错,但会静默失败


```js
const obj = Object.freeze({});
obj.name = 'hello';
console.log(obj.name); // undefined