【JS读代码】几个例子带你搞定所有console输出

38 阅读5分钟

基础融合-JS读代码

全局对象(Global Object, GO)和变量提升(Hoisting)机制

var func = 1
function func() {}
console.log(func + func)

这题考察的 GO,也就是全局的预编译:

  1. 创建 GO 对象
  2. 找变量声明,将变量声明作为 key,值赋为 undefined
  3. 找函数声明,将函数名作为 GO 对象的key,值赋为函数体

编译阶段:创建 GO 对象后,func 作为 key,值为 undefined,然后 func 变成了 函数体,所以在编译结束时,func 还是一个 function

运行阶段:func 被赋值为 1,所以 func + func 就是 2

// 阶段1:声明提升(编译阶段)
function func() {} // 函数声明优先提升
var func;          // var 声明被忽略(因为 func 已由函数声明定义)

// 阶段2:执行阶段
func = 1;          // var 的赋值覆盖了函数
console.log(func + func); // 1 + 1 = 2

this 的指向

let obj1 = { x: 1 }
let obj2 = obj1
obj2.y = 2
obj2 = { y: 20 }
console.log('obj1', obj1)//obj1 { x: 1, y: 2 }
['1', '2', '3'].map(parseInt) //[1, NaN, NaN]
/*
等价于==>
['1','2','3'].map((item,index)=>{
  parseInt(item,index)
})
*/

parseInt的第一个参数的每一位,都不能超过第二参数。按照x进制还原成十进制,按以下顺序执行

  1. parseInt('1', 0) radix 假如指定 0 或未指定,基数将会根据字符串的值进行推算。所以结果为 1。
  2. parseInt('2', 1) radix 应该是 2-36 之间的整数,此时为 1,无法解析,返回 NaN
  3. parseInt('3', 2) radix 是 2,表示 2 进制,注意这里不是指将其解析为 2 进制,而是按照 2 进制进行解析,但 3 不是 2 进制里的数字,所以无法解析,返回 NaN
const User = {
  count: 1,
  getCount: function () {
    return this.count;
  },
  getCountArrow: () => {
    return this.count; // 箭头函数的 this 指向外层作用域
  },
};

console.log(User.getCount()); // 输出: 1
console.log(User.getCountArrow()); // 输出: undefined(箭头函数的 this 指向全局对象)
const func = User.getCount;
console.log(func.call(User)); // 输出: 1
console.log(func.apply(User)); // 输出: 1

this 是一个指向对象的指针,this 的指向与所在方法的调用位置有关,而与方法的声明位置无关。

作为方法调用时,this 指向调用它所在方法的对象;所以 a 为 1

作为函数调用时,this 指向 window。所以 b 为 undefined.将 User.getCount 赋值给 func 后,直接调用 func()this 指向全局对象

补充一点小知识:callapply是立即调用函数,bind会生成一个新的函数,不立即调用。call参数逐个传递,apply的参数以数组形式传递,bind的参数可以分批传递。 适用场景来说:call适合已知参数个数的情况,apply适合参数个数不确定的情况,bind适合需要延迟执行或者部分参数预填充的情况。

call、apply、bind区别

特性callapplybind
调用方式立即调用函数立即调用函数返回一个新函数,不立即调用
参数传递参数逐个传递,如 fn.call(obj, arg1, arg2)参数以数组形式传递,如 fn.apply(obj, [arg1, arg2])参数可以分批传递,如 fn.bind(obj, arg1)``(arg2)
使用场景适合已知参数个数的情况适合参数个数不确定或动态的情况适合需要延迟执行或部分参数预填充的情况
const obj = {
  f1() {
    const fn = () => {
      console.log('this1', this)
    }
    fn()
    fn.call(window)
  },
  f2: () => {
    function fn() {
      console.log('this2', this)
    }
    fn()
    fn.call(this)
  },
}
1. obj.f1() 
obj.f2() 
  • 普通函数的 this
    由调用方式决定:
    • 直接调用时,非严格模式下 this 指向 window(严格模式下为 undefined)。
    • 通过 call/apply/bind 可以显式绑定 this

f1的this指向obj,箭头函数自己没有this,会继承外层作用域的this。箭头函数的this无法通过apply、bind、call修改。所以,obj.f1() 打印 this1 obj this1 obj

f2是箭头函数,没有自己的this,继承外层window的this,所以f2打印了两个window

  • 对象 {} 不构成作用域
  • 箭头函数的外层作用域是函数/全局作用域,而不是对象字面量。
const obj = {
  f2: function() {    // 普通函数 f2
    function fn() {
      console.log(this);
    }
    fn();
  }
};
obj.f2(); // 输出: 

此时 f2 是普通函数,调用时 this 指向 obj,但内部的 fn 仍是普通函数直接调用,this 还是 window对象的大括号 {} 不构成作用域,只有函数、模块、块作用域会影响箭头函数的 this 继承。只有函数、模块、块(let/const)会形成作用域。

箭头函数 + 对象方法 + 定时器

const obj = {
  name: 'Alice',
  sayName() {
    setTimeout(() => {
      console.log(this.name);
    }, 100);
  },
  sayName2: function() {
    setTimeout(function() {
      console.log(this.name);
    }, 100);
  }
};
obj.sayName();    // 输出?
obj.sayName2();   // 输出?
  • obj.sayName()'Alice'(箭头函数继承 sayNamethis,即 obj)。
  • obj.sayName2()undefined(普通函数的 this 默认指向 window,浏览器中 window.name 为空)。

箭头函数 + 构造函数

function Person(name) {
  this.name = name;
  this.sayName = () => {
    console.log(this.name);
  };
}
const person = new Person('Bob');
person.sayName();          // 输出?
const fn = person.sayName;
fn();                      // 输出?xxx
  • person.sayName()'Bob'(箭头函数继承构造函数调用时的 this,即新创建的实例)。
  • fn()'Bob'(箭头函数的 this 绑定后无法修改,仍指向实例)。

若将箭头函数改为普通函数,fn() 的输出是什么?

(答案:undefined,普通函数的 this 默认指向全局。)

箭头函数 + 原型链

function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = () => {
  console.log(this.name);
};
const cat = new Animal('Tom');
cat.speak();  // 输出? xxx
  • cat.speak()undefined(箭头函数定义在全局,this 继承全局作用域的 window)。
  • 关键:箭头函数不适合用作原型方法,因为它无法动态绑定实例的 this

箭头函数 + 事件监听

const button = document.getElementById('myButton');
button.addEventListener('click', (event) => {
  console.log(this);  // 输出?   
});
button.addEventListener('click', function() {
  console.log(this);  // 输出? 
});
  • 箭头函数 → window(继承全局作用域的 this)。 用 event.currentTarget 替代 this
  • 普通函数 → button(事件回调的 this 默认指向触发事件的元素)。

箭头函数 + 类(Class)

class Counter {
  count = 0;
  increment = () => {
    this.count++;
    console.log(this.count);
  };
  decrement() {
    this.count--;
    console.log(this.count);
  }
}
const counter = new Counter();
const inc = counter.increment;
const dec = counter.decrement;
inc();    // 输出?
dec();    // 输出?
  • inc()1(箭头函数绑定实例的 this)。
  • dec() → 报错(普通方法的 this 默认指向全局,this.countundefined)。

如何修正 decrement 方法使其不报错?

(答案:用 bind 绑定实例:this.decrement = this.decrement.bind(this);

箭头函数 + 嵌套对象

const game = {
  level: 1,
  start: {
    tutorial: () => {
      console.log(this.level);
    },
    mission: function() {
      console.log(this.level);
    }
  }
};
game.start.tutorial();  // 输出?xxxx
game.start.mission();   // 输出? 
  • tutorial()undefined(箭头函数的外层是全局作用域,this 指向 window)。
  • mission()undefined(普通函数的 this 指向调用者 start 对象,start 没有 level 属性)。

变体

如何让 tutorial 输出 game.level

(答案:改用普通函数 + 显式绑定:tutorial: function() { console.log(game.level); }

const obj = {
  foo: () => console.log(this === window),
  bar: function() {
    (() => console.log(this === obj))();
  }
};
obj.foo(); // 输出?
obj.bar(); // 输出?
  • obj.foo()true(箭头函数继承全局 this)。
  • obj.bar()true(箭头函数继承 barthis,即 obj