阅读 983

【前端体系】从地基开始打造一座万丈高楼

前言

从上次发文章到现在都一个多月了, 之前还说要写一篇vue-cli相关文章到现在也没写(打脸了🙊), 其实也不是我不想写,那篇文章我其实在发完vue-router最佳实践就开始着手写了,不过因为cli相关文章实在写不出太多的东西, 官方文档已经很详细了,又不想敷衍(其实就是我菜😂),所以就一直没写完结的状态,也因为公司对于实习生马上要考核定级了所以就将重心花在了职能要求上面, 为了证明我不是在找借口下面给出截图,顺便附上公司的梯级评定要求(小公司,技术要求肯定没有大公司难), 各位小伙伴可以对照看看自己能力在哪里

其实除了上面这些技术问题, 对于其他方面也有考察,这个就体现在你述职ppt上了, 好了, 废话不多说,下面就是我这一个月干的事情

转变

其实上面是我最开始设定的目标, 只不过写到后来发现要写太多了, 同样也发现自己要学的也太多了。算了下自己从踏入前端到现在将近一年半的时间了,这一路上可以说是勤勤恳恳。还记得当时没出来工作的时候我在前端群里问了句,毕业生刚出来能拿到10k的薪资嘛(那时候刚入门前端受各大机构招生老师朋友圈轰炸所产生的想法),群里一老哥跟我说了句,你有每天学习,代码不断, 定时总结, 不断提升吗?

这句话记得非常深,也时刻提醒着自学路上每当想放弃的我。现在同样将这句话送给你们

当时思考了很长一段时间发现自己虽然看似每天都在学习, 好像很认真很努力的样子, 可是总会觉得学到后面忘了前面,也问过很多人这个问题, 前端群里的老哥们给我的回答是: 很正常,每个人都是这样过来的。我甚至问过那些机构的培训讲师(听那些公开课的时候问的),给到我的回答是学习太过零散,没有系统性的学习。

直到最近看到文章的回答

为什么会忘记?人们对于一些过于片面,理解过浅且没有亲自动手实践的知识只会在脑中有个短暂的停留,随着时间推移忘的一干二净,但是你真的能将学过的东西记一辈子吗?显然是不可能的,那怎样形成长期记忆,最好的办法就是不停的复习、实践。保证你脑子快忘掉的时候又能快速记起。那对于你学过的那么多知识点不可能做到每块面面俱到,那么你学习的方式就很重要了,在你学习这块知识点的时候一定要深刻理解,达到脑中对于这块知识点有个很深的场景印象,我不知道你们会不会有这种感觉,就是对于某块点自己曾深入实践过就算很久没碰但是只要遇到脑中就会立马出现当时理解的场景印象

这种感觉我只有对于学的特别好的某块才会有,也正是这样的契机我开始思考自己学了这么多,花了那么多时间,真的都会了吗?这也是这篇文章诞生的理由。

其实文章标题最开始是【建议收藏】面试必会清单|万字长文 , 在开始着手写的时候想了下,好像自己学了这么久还没有真正的系统性复习。对于最开始学习方式学的知识点当时学了就认为过了,是真的会了吗?所以我也就打算将自己一年半学的东西汇总整理写一个【前端体系】系列文章。

下面给出我个人的一个前端体系导图,当然这肯定是不全的,里面的内容我会随着不断的学习,不停的修改,完善。

其实我在写这篇文章的时候心理状态变化是这样的

脑子中有万种想法 —> 行动中遇到重重问题 —> 间歇性丧失斗志 —> 又开始推倒重来

因为写文章和做笔记是很不一样的, 虽然我做了不少笔记但想写出好的文章还是很难的,上面的体系我肯定是没有全部复习完的,前端最离不开的就是js了, 随着现在js不断发展,可以说是对于js理解深度很大程度确定了你能不能从初级前端开发上升到中高级前端开发。所以我重点复盘的就是JS的内容, 这篇文章先将JS基础部分发出来后续的整理后再发出来,文章旨在总结、提炼。对于一些太过基础的就不多说了,整体还是代码较多且会在讲完每块知识点后给出一些相关的面试常问题。

数据类型的检测及转换

数据值是一门编程语言进行生产的材料, js中包含的值有以下这些类型

  • 基本数据类型(值类型)
    • Number
    • String
    • Boolean
    • Null
    • Undefined(js中独有的空)
    • Symbol (ES6新增)
    • BigInt (ES6新增)
  • 引用数据类型(引用类型)
    • Object
      • 普通对象
      • 函数对象
      • 数组对象
      • 正则对象
      • 日期对象
      • ....

简单的就不多讲了,直接进入正题。

数据类型检测

  • typeof
  • instanceof
  • constructor
  • Object.prototype.toString.call() 【最佳的方式】

上面的方法各自都弊端,Object.prototype.toString.call()这种方式算是最佳一种方式。

typeof

let a = '北歌',
    b = 18,
    c = [12, 23],
    d = {a: 1},
    e = new Map(),
	f = new Set(),
    g = new RegExp('A-Z'),
    h = new Function(),
    i = null,
    j = undefined,
    k = Symbol(),
    l = false; 

console.log(typeof a); // String
console.log(typeof b); // Number
console.log(typeof c); // object
console.log(typeof d); // object
console.log(typeof e); // object
console.log(typeof f); // object
console.log(typeof g); // object
console.log(typeof h); // function
console.log(typeof i); // object
console.log(typeof j); // undefined
console.log(typeof k); // symbol
console.log(typeof l); // boolean
// => 问题: 不能很好的区分数组和对象
复制代码

typeof 可以检测的数据类型有几种??究竟哪几种自己数。

问题一

console.log(typeof []) ?
console.log(typeof typeof []) ?
复制代码

答案

这个其实就是考察你的细心度, typeof检测的数据类型结果是一个字符串, 所以任何数据类型只要你typeof两次及以上都是string类型

问题二

在以下代码中,typeof atypeof b的值分别是什么:

function foo() {
  let a = b = 0;
  a++;
  return a;
}

foo();
typeof a; // => ??? 
typeof b; // => ???
复制代码

答案

让我们仔细看看第2行:let a = b = 0。这个语句确实声明了一个局部变量a。但是,它确实声明了一个全局变量b

foo()作用域或全局作用域中都没有声明变量 b。因此JavaScript将表达式 b = 0 解释为 window.b = 0

在浏览器中,上述代码片段相当于:

function foo() {
  let a;  window.b = 0;  a = window.b;  a++;
  return a;
}

foo();
typeof a;        // => 'undefined'
typeof window.b; // => 'number'

typeof a是 'undefined'。变量a仅在 foo()范围内声明,在外部范围内不可用。
typeof b等于'number'。b是一个值为 0的全局变量
复制代码

instanceof

instanceof运算符用来检测一个实例对象在其原型上是否存在一个构造函数的prototype属性

a instaceof B
// => 用来检测 a 是否是 B 的实例, 是为true, 否则反之


[] instanceof Array; // true
{} instanceof Object; // true
new Date() instanceof Date; // true
new RegExp() instanceof RegExp // true
复制代码

问题一: 对于基本类型字面量方式创建和实例方式创建有区别

console.log(1 instanceof Number) // false
console.log(new Number(1) instanceof Number)// true
复制代码

问题二: 只要在当前实例的原型链上,我们用其检测出来的结果都是true

let arr = [1, 2, 3];
console.log(arr instanceof Array) // true
console.log(arr instanceof Object);  // true

function fn() {}
console.log(fn instanceof Function) // true
console.log(fn instanceof Object) // true

// => 原理就是判断右边的prototype是否在左边的原型链上, 这就导致了在类的继承中检测的结果不正确
let arr = new Array('1'),
    fun = new Function();

最开始arr数组实例对象最顶端也就是Object.prototype不可能和Function.prototype扯上关系
console.log(arr instanceof Function); // false 

将数组的原型链上的原型链, 也就是Array的prototype的原型链, 也就是顶端Object.prototype指向改为了Function.prototype
arr.__proto__.__proto__ = Function.prototype;
console.log(arr instanceof Function); // true

复制代码

下面简单的画了个图(有点丑小伙伴们能看懂就行)

instaceof原理

function myInstanceof(leftVal, rightVal) {
	let proto = leftVal.__proto__,
        rightPrototype = rightVal.prototype;
    while(true) {
        if (proto === null) return false;
        if (proto === rightPrototype) return true
        proto = proto.__proto__;
    }
}
复制代码

问题三: 不能检测null 和 undefined

对于特殊的数据类型null和undefined,他们的所属类是Null和Undefined,但是浏览器把这两个类保护起来了,不允许我们在外面访问使用。

下面开始做几道题

function Foo(){} 
function BFoo(){} 
Foo.prototype = new BFoo();

let foo = new Foo();
console.log(foo instanceof Foo); ?
console.log(foo instanceof BFoo); ?
复制代码

上面这题在讲过instanceof原理后相信应该是不会难道大家的, 下面再看几个复杂点的

console.log(String instanceof String); 
console.log(Object instanceof Object); 
console.log(Function instanceof Function); 
console.log(Function instanceof Object);

function Foo(){} 
function BFoo(){} 
Foo.prototype = new BFoo();
console.log(Foo instanceof Function);
console.log(Foo instanceof Foo);
复制代码

这里涉及到了原型链的知识, 关于原型后面也会详细说, 这里就先简单说下

  • 所有实例对象都会有一个__proto__(我们叫它原型链属性)指向所属构造函数的prototype (原型属性)
  • 浏览器会给每个构造函数开辟一个prototype(原型对象), 该对象提供了供实例对象调用的成员属性和方法
  • 每个prototype都会自带一个constructor指回了该原型对象所属的构造函数(重写了原型对象会造成constructor丢失)
  • 内置类中最大的Boss不是Object基类,Object也是通过内置Function构造出来的,所以Object.__proto __=== Function.prototype
  • 内置Function也是通过自身构造出来,所以Function.__proto__ === Fcuntion.prototype

总结一句话:普通对象中最大的Boss是Object,函数对象中最大的Boss是Function

/**
 * 内置类:
 *  - Function
 *  - Object
 *  - Number
 *  - Array
 *  - String
 *  - Boolean下面简单的画了个图(有点丑小伙伴们能看懂就行)
 *  - RegExp
 *  - Date
 *  - Map
 *  - Set
 *  .......
 */

console.log(String.__proto__ === Function.prototype);
console.log(Number.__proto__ === Function.prototype);
console.log(Boolean.__proto__ === Function.prototype);
console.log(Date.__proto__ === Function.prototype);
console.log(RegExp.__proto__ === Function.prototype);
console.log(Object.__proto__ === Function.prototype);
console.log(Array.__proto__ === Function.prototype);
console.log(Map.__proto__ === Function.prototype);
console.log(Set.__proto__ === Function.prototype);
复制代码

好,下面终于可以回到正题了!

拿这两个题举个栗子🌰

console.log(Object instanceof Object); 
console.log(Foo instanceof Foo);
复制代码

根据上面实现的原理分析:

第一题

第一轮赋值:

L = Object.__proto__ = Function.prototype

R = Object.prototype

第一轮判断 L !== R 判断不为true,继续寻找L的原型链的准备一下轮赋值

第二轮赋值: L = Object.__proto__.__proto__ = Function.prototype.__proto__ = Object.prototype

R = Object.prototype

第二轮判断: L === R 为true

第二题

第一轮赋值:

L = Foo.__proto__ = Funtion.protoype

R = Foo.prototype (重写为了BFoo的实例对象)

第一轮判断 L !== R 判断不为true,继续寻找L的原型链的准备一下轮赋值

第二轮赋值: L = Foo.__proto__.__proto__ = Function.prototype.__proto__ = Object.prototype

R = Foo.prototype (重写为了BFoo的实例对象)

第二轮判断: L === R 为false, 其实后面也不需要去判断了,一直都是Object.prototype,也不可能等于 BFoo的实例对象

方法已经教给你们了其他的就自行去判断吧。

constructor

简单理解就是指向该对象的构造函数

function Foo(){};
var foo = new Foo();
alert(foo.constructor); // Foo
alert(Foo.constructor); // Function
alert(Object.constructor); // Function
alert(Function.constructor); // Function
// 其原理就是找该实例对象的的原型链对象中的constructor
复制代码

问题一

对于null和undeinfed这两个无效的值是不存在constructor, 需要用其他方式判断

问题二

通过构造函数的constructor是不稳定的, 如果原型重置或者constructor丢失会出现不必要的麻烦

function Fn(){}
Fn.prototype = new Array()
var f = new Fn
console.log(f.constructor) // Array

Fn.prototype = {}
console.log(f.constructor) // Object
复制代码

Object.prototype.toString.call()

  • 返回某个数据的内部属性[[class]],能够帮助我们准确的判断出某个数据类型

这个方法算是用的最多的一种检测类型的方式了

let a = '北歌',
    b = 18,
    c = [12, 23],
    d = {a: 1},
    e = new Map(),
	f = new Set(),
    g = new RegExp('A-Z'),
    h = new Function(),
    i = null,
    j = undefined,
    k = Symbol(),
    l = false; 

console.log(Object.prototype.toString.call(a)); // [object String]
console.log(Object.prototype.toString.call(b)); // [object Number]
console.log(Object.prototype.toString.call(c)); // [object Array] 
console.log(Object.prototype.toString.call(d)); // [object Object] 
console.log(Object.prototype.toString.call(e)); // [object Map] 
console.log(Object.prototype.toString.call(f)); // [object Set]
console.log(Object.prototype.toString.call(g)); // [object RegExp]
console.log(Object.prototype.toString.call(h)); // [object Function]
console.log(Object.prototype.toString.call(i)); // [object Null]
console.log(Object.prototype.toString.call(j)); // [object Undefined]
console.log(Object.prototype.toString.call(k)); // [object Symbol]
console.log(Object.prototype.toString.call(l)); // [object Boolean]
复制代码

好用的方法当然要封装一波

let isType = (type) => (o) => Object.prototype.toString.call(o) === `[object ${type}]`;
console.log(isType('Array')([]));
复制代码

这么好用的方法,大家有没有想过里面是怎么实现的呢?

在讲这个之前我们需要讲下Symbol.toStringTag, 查了下开发手册是这样描述它的:

Symbol.toStringTag公知的符号是在创建对象的默认字符串描述中使用的字符串值属性。它由该Object.prototype.toString()方法在内部访问。大体意思就是说这个方法决定了刚刚我们提到所有数据类型中[[class]]这个内部属性是什么。

let map = new Map(),
    set = new Set();
console.dir(map);
console.dir(set);
复制代码

接着我们调用一下它们的toString方法看看

console.log(map.toString()); // [object Map]
console.log(set.toString()); // [object Set]
console.log(arr.toString()); // '12, 23' 
复制代码

为啥和map和set调用toString()结果和Object.prototype.toString.call()调用结果一样呢??那为什么arr不是呢??

我将arr打印一下看看

发现它并没有Symbol.toStringTag这个方法,回过头来看上面map和set发现它们并没有toString()这个方法

我们是不是可以这样理解:

  • 没有Symbol.toStringTag内置属性的类型在调用toString()的时候相当于是String(obj)这样调用转换为相应的字符串
  • Symbol.toStringTag内置属性的类型在调用toString()的时候会返回相应的标签(也就是"[object Map]"这样的字符串)

回到正题,它和Object.prototype.toSting()又有什么基情呢🤔️?? 还是通过代码理解

class Super {}
console.log(Object.prototype.toString.call(new Super()))
复制代码

这应该和好理解,定义了一个类,打印出来肯定是"[object Function]", 那如果我们将这个对象原型上添加一个Symbol.toStringTag内置属性呢??

class Super {
  get [Symbol.toStringTag]() {
    return 'Test'; // => Symbol.toStringTag允许我们自定义返回的类标签
  }
}
console.log(Object.prototype.toString.call(new Super())); // [object Test]
// 注意: Symbol.toStringTag重写的是Super这个类的实例对象的标签, 而不是重写Super这个类的标签
console.log(Object.prototype.toString.call(Super)) // "[object Function]"
复制代码

通过上面代码我们返现在调用toString方法的时候如果该对象原型上有Symbol.toStringTag会在内部访问这个方法,如果没有内部会帮你实现一个,这也就是为什么调用Object.prototype.toString会得到具体的对象标签

对于前三种方式的弊端小结一波:

  • typeof
    • 不能细分对象和数组
    • 对于null这种特殊对象也不能区分
  • instanceof
    • 对于字面量创建的基本类型不能很好的判断
    • 对于实例的判断只要左侧在其原型链上的对象和右侧原型对象一致就行,在类继承上会出现问题
    • 对于null和undefined这类型不能检测
  • constructor
    • 对于null和undefined无效
    • 对于constructor这种方式, 如果原型对象重写或继承也会出现问题

所以最后一种方式就成了目前最完美的一种方式了

数据类型转换

都知道js是一门弱类型语言,除了这个标签它还有一个——js也是一门动态语言,所谓动态语言可以理解为所有的数据类型都是不确定的,在运算的过程中可能会发生类型转换,比如定义的时候是个字符串,通过运算符转换后可能就是一个数值类型了。

强制转换

其他的数据类型转换为String

  • toString()方法
  • String()函数

注意null和undefined不能调用toString()方法, 且每个内置类都对toString方法进行了重写,大体规则如下

let a = 123,
 	b = null,
 	c = undefined;
a.toString() // "123"
b.toString() // "报错"
c.toString() // "报错"

// => toString()
String({a: 1}) // "[object Object]" => 不管啥对象都是转换成这个
String([1, 2, 3]) // "1,2,3"
String([1]) // "1" 
String(null) // 'null'
String(undefined) // 'undefined'
String(new Map()) // "[object Map]"
String(new Set()) // "[object Set]"

// => 特殊情况
console.log(class A {}.toString()) // 'class A {}'
console.log(function () {}.toString()) // 'function () {}'
console.log(/(\[|\])/g.toString()) // '/(\[|\])/g'
console.log(new Date().toString()) // 'Fri Mar 27 2020 12:33:16 GMT+0800 (中国标准时间)'

复制代码

其他的数据类型转换为Number

  • Number()函数
  • parseInt()&parseFloat()
  • +, - 操作

Number()函数对于基本类型转换,null false 转换为0, true转换为1。

parsetInt()取整数, parseFloat()取第一浮点数转换

// 基本类型
Number(true) // 1
Number(false) // 0
Number(null) // 0
Number(3.15) // 3.15
Numer(0x12) // 18 => 可以识别 hex dec oct bin 进制
复制代码

其他的数据类型转换为Boolean

  • Boolean
  • ! (转换为布尔类型再取反)
  • !! (转换为布尔类型取反再取反,相当于没变只是转换为布尔类型了)

null, undefined, 0, NaN, ''(空字符串)这五个转换布尔为false其余都会true, 记住这个规则就行

引用类型转换为其他类型

因为在js中引用类型都是对象,所以下面我就简称'对象'

  • 对象转换为字符串
  • 对象转换为数值

对象转换为字符串

  • 调用对象的toString()方法, 如果返回的是原始类型, 将值转换为字符串并返回
  • 如果没有toString()这个方法或返回的不是原始类型, 调用valueOf()方法获取它的原始类型值, 将值转换为字符串返回
  • 如果对象两个方法都没有或都没有转换成功直接抛错

对象转换为数值

  • 调用对象的valueOf()方法获取它的原始类型, 将值转换为数值返回
  • 如果没有valueOf()这个方法或返回的不是原始类型, 调用toStrig()方法获取它的原始类型值, 将值转换为数值返回
  • 如果对象两个方法都没有或都没有转换成功直接抛错

上面废话一大堆无非是谁先谁后, 且后面是否调用要看基本方法调用之后是否是原始类型才会往下面走。

流程图如下:

常见的对象转换字符串

[] => ''
[1] => 1
[12, 23] => '12, 23'
[' '] => ' '
{a: 1} => '[object Object]'
复制代码

常见的对象转换数值

其实对象转换为数值都要先经历一下对象转换为字符串,因为即使你通过valueOf获取对象原始值,这里需要注意的是获取原始值是获取引用地址的值的意思,说到底也还是对象类型。

下面我们来通过一些奇淫技巧来将上面的流程走一篇。

let obj = {}, // 用于转换数值
    arr = [], // 用于转换为字符串
    testError = {};

// ===================================> 转换字符串
obj.toString = function() { // 先走它
  return [12] // 返回的不是原始类型, 意味着要继续向后走了
}
obj.valueOf = function() {
  return 12 // 返回了原始值12, 注意是Number类型, 
}

console.log(String(obj), typeof String(obj)); // 12 string => 最后将结果转换为字符串返回了


// ===================================> 转换数值
arr.toString = function() {
  return '12' // 返回了原始值12, 注意是String类型, 
} 
arr.valueOf = function() { // 先走它
  return {}  // 返回的不是原始类型, 意味着要继续向后走了
}
console.log(Number(obj), typeof Number(obj)); // 12 number => 最后将结果转换为数值返回了


// ===================================> 最后再来一个走不通的
testError.toString = function() {
  return {} 
} 
testError.valueOf = function() { 
  return {} 
}
console.log(Number(testError)); // 抛错

// 其他流程大家可以自己测试一下
复制代码

来一道面试题练练手

let a = ?
if (a == 1 && a == 2 & a == 3) {
    console.log(1)
}
// 怎么样可以让结果输入1???
复制代码

上面这道题其实有很多解法,我讲的是通过对象转换规则上达到这个目的, 上面说过不同类型比较会进行自动转换且两端类型不同时都转换为数值比较。

好了,知道转换方式之后就可以来解这道题了。

let a = {value: 0}
a.valueOf = function() { // 我们通过重写valueOf在每次比较的时候调用valueOf让a的自增
    return ++this.value;
}
if (a == 1 && a == 2 & a == 3) { // 在比较的时候调用valueOf获取对象的原始值
	console.log(1)
}

// 除了重写valueOf方法也可以通过重写toString方法达到上面一样的效果
let a = {
    value: 0,
    toString: function() {
        return ++this.value;
    }
}
复制代码

除了这种方式还有很多种,上面可以算是ES6之前的代码,下面再写一个ES6后的实现思路

// => Object.definedProperty()创建对象某个属性时,通过get再获取属性时做操作, 代码如下:
let value = 0;
Object.defineProperty(window, 'a', {
	get() {
      	console.log('调用了'); 
		return ++value;
	}
})

if (a == 1 && a == 2 & a == 3) {
	console.log(1)
}
复制代码

上面代码会调用3次,有人可能就会问道了,并没有访问a对象的属性啊,怎么会触发get呢?

问这问题的说明上面没有好好看,说了隐式转换对象的时候会调用valueOf | toString,只不过它是隐式调用的。definedProperty虽的getter方法和ES6新增对象的getter方法并不一样的,不能拿到调用的目标对象和属性,所以上面get方法中也接不到参数。不过明白这一点就行——在对象转换之前会调用一下valueOf()|toString()方法

隐式转换

在前面讲到js动态特性提到在运算的时候会出现隐式转换,这也是js一直以来被苟的一点。下面通过学习彻底搞明白隐式转换

隐式转换的条件

  1. 当使用 ==&&|| 等逻辑操作符进行判断时
  2. 当使用 + - * / 四则运算符进行操作时

自动转换为布尔值

if (12 + 'px') {
	// 12 + 'px => Boolean('12px') 为true
}
复制代码

自动转换为数值

// 记住一点除了 + 运算符如果有一端出现字符串就是拼接,其他都是跟数学一样的运算
true + 1 // 2
'true' + 1 // 'true1'
2 + null // 2
undefined + 1 // NaN
2 + NaN // NaN 任何值和NaN做运算都得NaN
'5' - '2' // 3
'5' * '2' // 10
true - 1  // 0
'1' - 1   // 0
'5' * []    // 0
false / '5' // 0
'abc' - 1   // NaN
复制代码

自动转换为字符串

'6' + true // "6true"
'6' + false // "6alse"
'6' + {} // "6object Object]"
'6' + [] // "6 => '6' + String([]) => '6' + ''
'6' + function (){} // "6function (){}"
'6' + undefined // "6undefined"
'6' + null // "6null"
复制代码
  • "=="比较中的隐式转换
{name: '北歌'} == {name: '北歌'} // => 对于引用类型比较的是内存地址
[] == [] => false

let obj1 = {},
    obj2 = obj1;
obj1 == obj2 // true
复制代码

转换规则

  • 两端都是不同类型比较,都是转换为数值类型再比较,唯一的区别是(对象 == 字符串 )将对象转换为字符串
  • null == undefined 为true 全等为false,需要特殊记忆
  • NaN和谁都不相等,对于NaN的比较可以用ES6中的Object.is()方法

思考题

12+true+false+null+undefined+[]+'北歌'+null+undefined+[]+true
!!" " + !!"" - !!false ||document.write("能打印嘛") 
复制代码

上面的题我就不讲了,通过上面的讲解这题目是难不到大家的

下面再来几道题,全对说明你就通关了

console.log([] == 0); 
console.log(![] == 0);
console.log([] == ![])
console.log([] == []);
console.log({} == !{});
console.log({} == {});
复制代码

就选这道变态题吧😣

console.log(![] == 0);
复制代码
  • 首先规则大家应该知道,不同类型比较都转换数字
  • ![] 使用"!"强制转换为布尔类型再取反 false
  • false == 0 false转数字为0
  • 0 == 0

好了!就是这么神奇(坑爹),剩下的看你们了🙃。

图解作用域链

引言

在讲作用域链(scopeChain)之前我们先来了解下什么叫作用域,(scope) 《你不知道的javaScript(上)》 书中是这么解释的: 作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。

作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行

我们看个例子,用泡泡来比喻作用域可能好理解一点:

最后输出的结果为 2, 4, 12

  • 泡泡1是全局作用域,有标识符foo;
  • 泡泡2是作用域foo,有标识符a, b, bar;foo函数中的形参a也相当于函数中的私有变量(标识符)
  • 泡泡3是作用域bar,仅有标识符c。

作用域 (scope)

在 JavaScript 中有两种作用域

  • 全局作用域:拥有全局作用域的对象可以在代码的任何地方访问到。
  • 局部作用域:和全局作用于相反,局部作用域一般只能在固定代码片段内可以访问到。最常见的就是函数作用域
全局作用域

在js中一般有以下几种情形拥有全局作用域:

  1. 最外层的函数以及最外层变量:
var globleVariable= 'global';  // 最外层变量
function globalFunc(){         // 最外层函数
    var childVariable = 'global_child';  //函数内变量
    function childFunc(){        // 内层函数
        console.log(childVariable);
    }
    console.log(globleVariable)
}
console.log(globleVariable);  // global
globalFunc();                 // global
console.log(childVariable)   // childVariable is not defined
console.log(childFunc)       // childFunc is not defined
复制代码

从上面代码中可以看到globleVariableglobalFunc在任何地方都可以访问到, 反之不具有全局作用域特性的变量只能在其作用域内使用。

  1. 未定义直接赋值的变量(由于变量提升使之成为全局变量)
function func1(){
    special = 'special_variable'; // 没有用var声明自动提升全局变量
    var normal = 'normal_variable';
}
func1();
console.log(special);    //special_variable
console.log(normal)     // normal is not defined

// 有var和不带var有什么区别呢??
// => 带var不能被delete删除

var a = 10;
	b = 20;
delete a; // false 删除不了这个变量存储的值
delete b; // true 可以删除
复制代码

虽然我们可以在全局作用域中声明函数以及变量, 使之成为全局变量, 但是不建议这么做,因为这可能会和其他的变量名冲突,一方面如果我们再使用const或者let声明变量, 当命名发生冲突时会报错。

// 变量冲突
var globleVariable = "person";
let globleVariable = "animal"; // Error, thing has already been declared
复制代码

另一方面如果你使用var申明变量,第二个申明的同样的变量将覆盖前面的,这样会使你的代码很难调试。

// 张三写的代码
var name = 'beige'

// 李四写的代码
var name = 'yizhan'
console.log(name);  // yizhan
复制代码
局部作用域

和全局作用于相反,局部作用域一般只能在固定代码片段内可以访问到。最常见的就是函数作用域

1、函数作用域

定义在函数中的变量就在函数作用域中, 形参变量也相当于在函数内声明的,并且每个函数拥有自己独立的作用域,意味着同名变量可以用在不同的函数中,彼此之间不能访问。

function test1() {
    var a = 10;
    console.log(a);
}

function test2() {
    var a = 20;
    console.log(a);
}

test1(); // 10
test2(); // 20例子,用泡泡来比喻作用域可能好理解一点:

// => 两个函数内的同名变量a相互独立,互不影响。
复制代码

2、块级作用域

ES6 引入了块级作用域,让变量的生命周期更加可控,块级作用域可通过新增命令let和const声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:

  1. 在一个函数内部
  2. 在一个代码块(由一对花括号包裹)内部

let声明和var声明的区别:

  1. 不存在变量提升
  2. 不允许重复声明
  3. 会形成暂时性死区(temporal dead zone)简称TDZ
  4. 不存在和全局window之间的相互映射

代码演示

// 变量提升
console.log(str); // undefined;
var str = '北歌'; 

// 不存在变量提升
console.log(str); // str is not defined;
let str = '北歌'; 

// 允许重复声明  => 后面覆盖前面
var a = 10;
var a = 20;

// 不允许重复声明 => Identifier 'b' has already been declared
let a = 10;
let a = 20;

// TDZ
function foo1() {
    console.log(a); // a is not defined
    var a = 10;
}

function foo2() {
    console.log(a); // Cannot access 'a' before initialization
    let a = 10;
}

foo1()
foo2()

// 存在映射
var a = 10;
console.log(window.a); // 10;
window.a = 20;
console.log(a); // 20

// 不存在映射
var a = 10;
console.log(window.a); // undefined => window对象没有这个属性
window.a = 20;
console.log(a); // 10
复制代码

循环中的绑定块作用域的妙用

for (let i = 0; i < 10; i++) {
  // ...
}
console.log(i);
// ReferenceError: i is not defined
复制代码

上面代码中,计数器i只在for循环体内有效,在循环体外引用就会报错。

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10
复制代码

上面代码中,变量i是var命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。

如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6
复制代码

上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc
复制代码

上面代码正确运行,输出了 3 次abc。这表明内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。

内部实现相当于这样

{
    // 父作用域
    let i; 
    for (i = 0; i < 3; i++) {
        // 子作用域
        let i = 'abc'; 
        console.log(i); 
    }
}
复制代码

作用域链

在讲解作用域链之前先说一下,先了解一下 JavaScript是如何执行的?

JavaScript是如何执行的?

为了能够完全理解 JavaScript 的工作原理,你需要开始像引擎(和它的朋友们)一样思考, 从它们的角度提出问题,并从它们的角度回答这些问题。

  • 引擎 •从头到尾负责整个 JavaScript 程序的编译及执行过程。
  • 编译器 •引擎的好朋友之一,负责语法分析及代码生成等脏活累活
  • 作用域 • 引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查 询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

JavaScript代码执行分为两个阶段:

分析阶段

javascript编译器编译完成,生成代码后进行分析

  • 分析函数参数
  • 分析变量声明
  • 分析函数声明

分析阶段的核心,在分析完成后(也就是接下来函数执行阶段的瞬间)会创建一个AO(Active Object 活动对象)

执行阶段

分析阶段分析成功后,会把给AO(Active Object 活动对象)给执行阶段

  • 引擎询问作用域,作用域中是否有这个叫X的标识(变量)
  • 如果作用域有标识(变量),引擎会使用这个标识(变量)
  • 如果作用域中没有,引擎会继续寻找(向上层作用域),如果到了最后都没有找到这个标识(变量),引擎会抛出错误。

执行阶段的核心就是,具体怎么,后面会讲解LHS查询RHS查询

JavaScript执行举例说明

看一段代码:

function a(age) {
    console.log(age);
    var age = 20
    console.log(age);
    function age() {
    }
    console.log(age);
}
a(18);
复制代码

首先进入分析阶段

前面已经提到了,函数运行的瞬间,创建一个AO (Active Object 活动对象)

AO = {}
复制代码

第一步:分析函数参数:

形参:AO.age = undefined
实参:AO.age = 18
复制代码

第二步,分析变量声明:

// 第3行代码有var age
// 但此前第一步中已有AO.age = 18, 有同名属性,不做任何事
即AO.age = 18
复制代码

第三步,分析函数声明:

// 第5行代码有函数age
// 则将function age(){}付给AO.age
AO.age = function age() {}

复制代码

进入执行阶段

分析阶段分析成功后,会把给AO(Active Object 活动对象)给执行阶段,引擎会询问作用域,的过程。所以上面那段代码AO链中最终应该是JavaScript是如何执行的?

AO.age = function age() {}
//之后
AO.age=20
//之后
AO.age=20
复制代码

所以最后的输出结果是:

function age(){
    
}
20
20
复制代码

过程中LHS和RHS查询特殊说明

LHS,RHS 这两个术语就是出现在引擎对标识(变量)进行查询的时候。在《你不知道的javaScript(上)》也有很清楚的描述。freecodecamp上面的回答形容的很好:

LHS = 变量赋值或写入内存。想象为将文本文件保存到硬盘中。 RHS = 变量查找或从内存中读取。想象为从硬盘打开文本文件。

LHS和RHS特性

  • 都会在所有作用域中查询
  • 严格模式下,找不到所需的变量时,引擎都会抛出ReferenceError异常。
  • 非严格模式下,LHR稍微比较特殊: 会自动创建一个全局变量
  • 查询成功时,如果对变量的值进行不合理的操作,比如:对一个非函数类型的值进行函数调用,引擎会抛出TypeError异常

LHS和RHS举例说明,例子来自于《你不知道的Javascript(上)》

function foo(a) {
    var b = a;
    return a + b;
}
var c = foo( 2 );
复制代码
引擎:我说作用域,我需要为 c 进行 LHS引用, 你见过吗?
作用域:别说,我还真见过,编译器那小子刚刚声明了它,给你。
引擎:哥们太够意思了!
引擎:作用域,还有个事儿。我需要为 c 进行赋值,foo RHS引用这个你见过吗?
作用域:这个也见过,编译器最近把它声名为一个函数
引擎: 好现在我来执行一下foo, 它最好是一个函数类型
引擎  作用域,还有个事儿。我需要为 a 进行LHS引用,这个你见过吗?
作用域:这个也见过,编译器最近把它声名为 foo 的一个形式参数了,拿去吧。
引擎:大恩不言谢,你总是这么棒。现在我要把 2 赋值给 a 。
引擎:哥们,不好意思又来打扰你。我要给b进行LHS引用, 你见过这个人嘛?
作用域:咱俩谁跟谁啊,再说我就是干这个。编译器那小子刚声明了它, 我给你
引擎:么么哒。能帮我再找一下对 a 的RHS引用吗?虽然我记得它,但想再确认一次。
作用域:放心吧,这个变量没有变动过,拿走,不谢。
引擎:能帮我再找一下对 a 和 b 的RHS引用吗?虽然我记得它,但想再确认一次
作用域:放心吧,这个变量没有变动过,拿走,不谢。
引擎:好, 现在我要返回 2 + 2 的值
复制代码

现在来看引擎在作用域这个过程: LSH(写入内存):

c=, a=2(隐式变量分配), b=
复制代码

RHS(读取内存)

读foo(2), = a, a ,b
(return a + b 时需要查找a和b)
复制代码

最后对作用域链做一个总结,引用《你不知道的Javascript(上)》中的一张图解释!

好, 你现在应该要在脑子里把作用域链想象成一栋楼,当前执行的作用域所处的位置就在一层,楼顶就是全局作用域。作用域内收集并维护由所有声明的标识符(变量),当调用函数时如果自己没有这个标识就向上一层查找(上一个作用域),直到顶楼(window)还没有话就停止。

最后来看看代码

let str = 'global' // 全局作用域
function outer() { // 第二层作用域
    let str = 'outer';
    return function inner() { // 第一层作用域 
        console.log(str); 
    }
}
let inner = outer();
inner(); // outer
复制代码

作用域还是概念较多,其实就是一个向上查找的规则,这篇文章可能写的有点啰嗦了,其实主要是从作用域话题引出js运行机制😊, 题目也没啥可做的。

原型详解

本来是不想写原型、闭包、this这些的,因为这类文章真的太多了且都讲的非常详细了, 但这也算是学习以来第一次做总结就把这些也写上了,但对于这三篇文章我打算换一种方式来写, 以代码为主,从一些相关面试题来讲解里面一些难以理解的知识点。考虑到看我文章的应该都和我一样是踏入前端不久的小伙伴们。所以也贴出个人认为挺不错的文章,对于这块不太了解的可以看看再来刷题

看完上面三篇文章基本上就能对原型链有很好的认识, 但是因上面文章是16年写的里面讲的虽详细但是有些内容和慨念在ES6之后可能就稍有不同。且对于文章里面所说的函数对象的__proto__所指向的Function.prototype是一个空函数抱质疑态度,下面我会概括自己对于原型链的理解。

还是先把我上面对于原型自己总结的几句话搬下来

  • 所有实例对象都会有一个__proto__(我们叫它原型链属性)指向所属构造函数的prototype (原型属性)
  • 浏览器会给每个构造函数开辟一个prototype(原型对象), 该对象提供了供实例对象调用的成员属性和方法
  • 每个prototype都会自带一个constructor(构造器属性)指回了该原型对象所属的构造函数(重写了原型对象会造成constructor丢失)
  • 内置类中最大的Boss不是Object基类,Object也是通过内置Function构造出来的,所以Object.__proto __=== Function.prototype
  • 内置Function也是通过自身构造出来,所以Function.__proto__ === Fcuntion.prototype

ES5和ES6之后的不同

叫法上的不同

在ES6之前没有类的概念所以用构造函数模拟类,在ES6之后有了类的概念,通过class来定义类其原理也是通过(类本身指向构造函数(Class = Class.prototype.constructor),所以也是构造函数的另一种语法糖。

对于__proto__(原型链属性),这个属是实例对象隐式找构造函数的prototype(原型对象)的属性。在ES6之前并没有被标准化之前是打算废除这个属性,因大部分现代浏览器都有这个属性所以在ES5之后也就被标准化了,但还是不建议使用这种方式来找实例对象的原型,在ES6之后我们可以通过Object.getPrototypeOf来获取

写法上的不同

// ===========================> ES5的写法
// 将父类原型指向子类
function inheritPrototype(subType, SuperType) {
  let prototype = Object.create(SuperType.prototype); // 获取父类的原型副本
  prototype.constructor = subType; // 解决子类重写原型对象constructor丢失问题
  subType.prototype = prototype
}

// 父类
function Es5Super(colors) {
  this.colors = ['red', 'blue', 'green'];
}
Es5Super.prototype.getColor = function (index) {
  console.log(this.colors[index]);
}

// 子类
function Es5(name) {
  this.name = name;
}

inheritPrototype(Es5, Es5Super); 

// 在原型上写方法
Es5.prototype.getName = function() {
  console.log(this.name + '公有成员');
}
// 在函数对象上添加成员
Es5.getName = function() {
  console.log(this.name + '私有成员');
}

// ===========================> ES6的写法
class ECMAScript{
  constructor(version) {
    this.version = version;
  }
}

class Es6 extends ECMAScript {
  constructor(name, version) {
    super(version)
    this.name = name;
    this.version = this.version;
}

  getName() { // 在原型上写方法
    console.log(this.name + '公有成员');
  }

  getVersion() {
    console.log(this.version);
  }
  
  static getName() { // 在函数对象上添加成员
    console.log(this.name + '私有成员');
  }

  
}

let es5 = new Es5('es5')
let es6 = new Es6('es6', 'ES2015')

console.log(es5);
console.log(es6);
// => 在ES6出现类继承之后基本就大一统了继承。
复制代码

从原型神图中说说自己的理解

下面祭出原型神图👇

画图分析一波

从上图我们能得出几结论:

  • 实例对象为什么没有prototype: 只有构造函数才会有prototype属性指向原型对象
  • Object.prototype.__proto__为什么是null: 原型对象(也就是浏览器为构造函数开辟的对象)都是Object基类的实例对象,所以原型对象的__proto__指向Object.prototype, 同样Object.prototype.__proto__也如此, 自己指向自己没有任何意义所以置为null
  • Function.__proto__为什么指向自己: 上面说过内置Function也是通过Function构造出来的所以Function.__proto__也就指向了自己。

原型相关面试题

题一

class Re {
  constructor () {
    this.num = 3;
  }
  rand () {
    return this.num;
  }
}

var c1 = new Re();
console.log(c1.num, c1.rand()); 
Re.prototype.num = 4;
Re.prototype.rand = function () {
  return this.num;
}
var c2 = new Re();
console.log(c1.num, c1.rand()); 
console.log(c2.num, c2.rand()); 
复制代码

题二

let name = 'oop'
let Person = function (options){
  this.name = options.name 
}

Person.prototype.name = 'Person'
Person.prototype.getName = function(){
  return this.name 
}
let p = new Person({name: 'Beige'})


Object.getPrototypeOf(p) === p.__proto__;
let proto = Object.getPrototypeOf(p);
let targetObj = Object.assign(proto, {public: '前端自学驿站'})

console.log(targetObj === p.__proto__);
console.log(p.__proto__.constructor === Person)
console.log(p instanceof Person) 
console.log(p.__proto__ === Person.prototype) 
console.log(p.__proto__.public);
console.log(p.hasOwnProperty('name')) 
console.log(p.hasOwnProperty('getName')) 


let getName = p.getName 
console.log(getName === Person.prototype.getName) 
console.log(getName()) 
console.log(Person.prototype.getName()) 
console. log(p.getName()) 
复制代码

题三

function Foo() {
  getName = function () {console.log(1);};
  return this;
}

Foo.getName = function() {console.log(2);};
Foo.prototype.getName = function() {console.log(3);};
var getName = function() {console.log(4);}; 
function getName() {console.log(5)}
Foo.getName(); 
getName(); 
Foo.getName();
getName(); 


var a = new Foo.getName(); 
var b = new Foo().getName();
var c = new new Foo().getName(); 
console.log(a, b, c);
复制代码

老规矩, 我选最后一题。

这道题相对来将并不是很复杂,但难点在于最后三个输出。知道答案的原型基本就通过了。

function Foo() {
  getName = function () {console.log(1);};
  return this;
}
// 向Foo函数对象上添加私有成员
Foo.getName = function() {console.log(2);};
// 向函数原型上添加公有成员
Foo.prototype.getName = function() {console.log(3);};
// 表达式声明一个全局变量值为一个函数
var getName = function() {console.log(4);}; // 执行到这一步重新赋值了之前发声明式函数
// 声明一个函数: 优先级高于上面的方式
function getName() {console.log(5)}
Foo.getName(); // 2
getName(); // 4
Foo.getName(); // 2
getName(); // 4

// 难点
var a = new Foo.getName(); 
var b = new Foo().getName();
var c = new new Foo().getName(); 
console.log(a, b, c);
复制代码

对于最后三输出需要先讲下运算符优先级问题了,成员访问是要大于new 不带括号的,先给出优先级列表(值越大优先级越高,值相同遵循从左到右规则)

先分析第一个

var a = new Foo.getName(); 
// => 首先 new Foo.getName,按照优先级成员访问大于new不带括号,所以应该是这样
new (Foo.getName)() // => 将Foo.getName当做整体来new, 并不是有些人理解的 new (Foo.getName())
所以这题的返回的是 Foo.getName构造出来的实例对象
复制代码

第二个

var b = new Foo().getName();
// 这个和上面就不一样了,new Foo()带了小括号和成员访问优先级一致,应该遵循从左到右
(new Foo()).getName()
// 返回的是new Foo()构造函数返回实例调用getName()方法的返回值
复制代码

第三个

这个就更变态了,咳咳

var c = new new Foo().getName(); 
// => new (new Foo().getName())
// => new (返回的实例.getName)()
// => 返回 new 实例.getName()构造出来的实例对象
复制代码

逼逼了这么多,也不知道对了没对,下面我们验证一下

// 我们稍微改下代码,验证上面是否正确
function Foo() {
  getName = function () {console.log(1);};
  return this;
}

Foo.getName = function(name) {
    console.log(2);
    this.name = name;
};
Foo.prototype.getName = function() {
    console.log(3);
    retrun '前端自学驿站'
    // return {info: '自己返回的对象'} => 如果返回引用类型,第二三都会得到这个对象
};
var getName = function() {console.log(4);}; 
function getName() {console.log(5)}
Foo.getName(); 
getName(); 
Foo.getName();
getName(); 


var a = new Foo.getName(name); 
// new (Foo.getName)() => 相当于将Foo.getName当做构造函数来new, 那我是不是可以传递参数
console.log(a) // {name: '北歌'}
console.log(a.__proto__.constructor === Foo.getName) // true


var b = new Foo().getName();
// (new Foo()).getName() => 最后返回的是实例调用.getName()方法返回的结果 
// Foo并没有给实例添加私有的getName成员方法,所以调用的是原型上的方法,为了不影响下面的我返回非引用类型
console.log(b) // '前端自学驿站'


var c = new new Foo().getName(); 
// => new ( (new Foo())getName() ) // => 和上面的区别就是最后还new了下 实例.getName
console.log(c.__proto__.constructor === Foo.prototype.getName)
console.log(c) // {}
复制代码

闭包

对于闭包相关文章很多我就不过多赘述了, 这里先不要脸的推荐一篇我写的闭包相关文章, 彻底理解js闭包

再推荐一篇个人认为不错的文章: 我从来不理解JavaScript闭包,直到有人这样向我解释它...

全面解析this

this:当前方法执行的主体(谁最后执行的这个方法,那么this就是谁,所以this和当前方法在哪创建的或者在哪执行的都没有必然的关系

this的绑定规则及优先级

在 JavaScript 中,this 指向的绑定规则有以下四种:

  • 1.默认绑定(非严格模式情况下,this 指向 window, 严格模式下,this指向 undefined。)
  • 2.隐式绑定(如果函数调用时,前面存在调用它的对象,那么this就会隐式绑定到这个对象上)
  • 3.显式绑定(函数通过 call()、apply()、bind()调用,this 指向被绑定的对象。)
  • 4.new 绑定(函数被 new 调用,this 指向由 new 新构造出来的这个对象。)

绑定规则的优先级: new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定

function test() {
  this.a = 'Ge';
}

let obj = {
  a: 'Bei',
  fn() {
      console.log(this);
  }
};

function scope() {   // 放在函数里面防止影响结果
    test() // 默认绑定
	obj.fn() // 隐式绑定
	test.call(obj) // 显示绑定
	new test() // new 绑定
}

// 显示绑定
const Bar = test.bind(obj);

// new绑定
const bar = new Bar();
console.log(obj.a, '--', bar.a) // Bei -- Ge

new绑定改变了显示绑定中指定的this(obj)
显示绑定  > new 绑定 
复制代码

this的几种情况

  • 全局作用域下的this指向window (严格模式也是)
  • 如果给元素的事件行为绑定函数,那么函数中的this指向当前被绑定的那个元素
  • 函数中的this,要看函数执行前有没有 (点) 有 (点) 的话,点前面是谁,this就指向谁,如果没有点,指向window 自执行函数中的this永远指向window
  • 定时器中函数的this指向window
  • 构造函数中的this指向当前的实例
  • call、apply、bind可以改变函数的this指向
  • 箭头函数中没有this,如果输出this,就会输出箭头函数定义时所在的作用域中的this
  • 括号表达式也有可能改变this, 括号内只有一个项不会,两个或多个的话this指向window
  1. 全局作用域中的this默认指向widnow,严格模式下没有就是undefined
function windowScope() {
    // => this: window
}
复制代码

2.给元素的某个事件绑定方法,方法中的this就是当前操作的元素本身

document.body.onclick = function () {
    //=>this:body
};
复制代码

3.函数执行,看函数前面是否有点,有的话,点前面是谁this就是谁,没有点,this是window(在JS的严格模式下,没有点this是undefined)

let fn = function () {
    console.log(this.name);
};
let obj = {
    name: '哈哈',
    fn: fn
};
fn();//=>this:window
obj.fn();//=>this:obj
复制代码

4.构造函数执行,方法中的this一般都是当前类的实例

let Fn = function () {
    this.x = 100;//=>this:f
};
let f = new Fn;
复制代码

5.箭头函数中没有自己的this,this是上下文中的this

let obj = {
    fn: function () {
        // this:obj
        setTimeout(() => {
            //this:obj
        }, 1000);
    }
};
obj.fn();
复制代码

6.在小括号表达式中,会影响this的指向

let obj = {
    fn: function () {
        console.log(this);
    }
};
(obj.fn)(); // => this:obj
;(12, obj.fn)();//=>this:window
复制代码

结论: 在括号表达式中如果只有一个参数, 不会改变this指向, 如果有多个参数,this指向window

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

var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4};

o.foo(); // 3
(p.foo = o.foo)(); // 2
// 结论: 特殊情况的是如果括号表达式中有表达式语句执行, this也还是指向window
// 还有一种说法, p.foo = o.foo引用的内存地址, 也就是foo引用的内存地址, 匿名函数将foo引用的函数执行,
// this -> window
复制代码

7.使用call/apply/bind可以改变this指向

fn.call(obj); // => this:obj
fn.call(12); // => this:12
fn.call(); // => this:window 非严格模式下call/apply/bind第一个参数不写或者写null和undefined,this都是window,严格模式下写谁this就是谁,不写是undefined
复制代码
  1. 定时器中的this指定window
setInterval(function() {
  console.log(this);
}, 1000);
复制代码

也可以理解为匿名函数中的执行主休默认指向window

this相关面试题

题一

var num = 10
const obj = {num: 20}
obj.fn = (function (num) {
  this.num = num * 3
  num++
  return function (n) {
    this.num += n
    num++
    console.log(num)
  }
})(obj.num)
var fn = obj.fn
fn(5)
obj.fn(10)
console.log(num, obj.num)
复制代码

题二

var a = {
    name:"zhang",
    sayName:function(){
        console.log("this.name="+this.name);
    }
};
var name = "ling";
function sayName(){
    var sss = a.sayName;
    sss(); //this.name = ?
    a.sayName(); //this.name = ?
    (a.sayName)(); //this.name = ?
    (b = a.sayName)();//this.name = ?
}
sayName();
复制代码

题三

var obj = {
  a: 1,
  foo: function (b) {
    b = b || this.a
    return function (c) {
      console.log(this.a + b + c)
    }
  }
}
var a = 2
var obj2 = { a: 3 }

obj.foo(a).call(obj2, 1)
obj.foo.call(obj2)(1)
复制代码

题四

var name = 'window'
function Person (name) {
  this.name = name
  this.obj = {
    name: 'obj',
    foo1: function () {
      return function () {
        console.log(this.name)
      }
    },
    foo2: function () {
      return () => {
        console.log(this.name)
      }
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.obj.foo1()()
person1.obj.foo1.call(person2)()
person1.obj.foo1().call(person2)

person1.obj.foo2()()
person1.obj.foo2.call(person2)()
person1.obj.foo2().call(person2)
复制代码

后语

文章总体来讲还是以总结性方式呈现出来的, 对于【前端体系】这系列的文章我是抱着很认真,很想写好的心态的,但毕竟我还是前端小白&写作新人,如果文章中有那块写的不太好或有问题欢迎大家指出,我也会在后面的文章不停修改。也希望自己进步的同时能跟你们一起成长。喜欢我文章的朋友们也可以关注一下

我会很感激第一批关注我的人。此时,年轻的我和你,轻装上阵;而后,富裕的你和我,满载而归。

参考文章

CONSTRUCTOR实现原理(转)

JavaScript的数据类型及其检测

JavaScript数据类型转换

你不知道的JavaScript(上卷)