夯实JS主要知识点

从事前端行业到如今,感受本身进步最大的时候就是去年打算换工做开始学习的那段时间,特别是看 yck 大佬的掘金小册《前端面试之道》的那段时间。正是那段时间的学习,慢慢对前端知识体系有了个模糊的轮廓,并且也开始接触掘金这个有意思的技术平台。现在工做尘埃落定,倒开始懒散了,通勤路上又开始玩游戏了,晚上回家又开始玩游戏不看书了,闲的时候开始在微信群QQ群注水了。但是距离30岁愈来愈近,眼前的路却愈来愈模糊。我知道留给我补课的时间很少了。工做的前三年已经被我挥霍掉,若是这两年不把失去的时间补回来,我可能永远都只能停留在初中级程序员的水平。谨记我仍是一个半路出家的非科班出身的大龄初级前端开发工程师,自勉!javascript

小刚老师

基本类型和引用类型

js中数据类型分为基本类型和引用类型,基本类型有六种:html

  • number
  • string
  • boolean
  • null
  • undefined
  • symbol (es6)

引用类型包括对象object、数组array、函数function等,统称对象类型:前端

  • object

string类型即字符串,除了单引号双引号,es6 中引入了新的反引号 ` ` 来包含字符串。反引号的扩展功能是能够用${…}将变量和表达式嵌入到字符串中。使用以下:java

let n = 3
let m = () => 4
let str = `m + n = ${m() + n}` // "m + n = 7"
复制代码

number类型值包括整数、浮点数、NaNInfinity等。其中NaN类型是js中惟一不等于自身的类型,当发生未定义的数学操做的时候,就会返回NaN,如:1 * 'asdf'Number('asdf')。浮点数的运算可能会出现如0.1 + 0.2 !== 0.3的问题,这是因为浮点运算的精度的问题,通常采用toFixed(10)即可以解决此类问题。程序员

booleanstringnumber类型做为基本类型,按理说应该是没有函数能够调用的,由于基本类型没有原型链能够提供方法。可是,这三种类型却能调用toString等对象原型上的方法。不信?es6

true.toString() // 'true'
`asdf`.toString() // 'asdf'
NaN.toString() // 'NaN'
复制代码

你可能会说,那为何数字1不能调用toString方法呢?其实,不是不能调用:面试

1 .toString()
1..toString()
(1).toString()
复制代码

以上三种调用都是能够的,数字后面的第一个点会被解释为小数点,而不是点调用。只不过不推荐这种使用方法,并且这样作也没什么意义。数组

为何基本类型却能够直接调用引用类型的方法呢?实际上是js引擎在解析上面的语句的时候,会把这三种基本类型解析为包装对象(就是下面的new String()),而包装对象是引用类型能够调用Object.prototype上的方法。大概过程以下:浏览器

'asdf'.toString()  ->  new String('asdf').toString()  -> 'asdf'
复制代码

null含义为“无”、“空”或“值未知”的特殊值。bash

undefined的含义是“未被赋值”。除了变量已声明未赋值的状况下是undefined,若对象的属性不存在也是undefined。因此应该尽可能避免使用var a = undefined; var o = {b: undefined}这样的写法,取而代之用var a = null; var o = {b: null},以与“未被赋值”默认undefined的状况相区分。

Symbol值表示惟一的标识符。能够用Symbol()函数建立:

var a = Symbol('asdf')
var b = Symbol('asdf')
a === b // false
复制代码

还能够建立全局标识符,这样能够在访问相同的名称的时候都获得同一个标识符。以下:

var a = Symbol.for('asdf')
var b = Symbol.for('asdf')
a === b // true
复制代码

还能够用作对象的属性,但此时是不能被for...in遍历的:

let id = Symbol('id')
let obj = {
  [id]: 'ksadf2sdf3lsdflsdjf090sld',
  a: 'a',
  b: 'b'
}
for(let key in obj){ console.log(key) } // a b
obj[id] // "ksadf2sdf3lsdflsdjf090sld"
复制代码

还存在不少系统内置的Symbol,如Symbol.toPrimitive Symbol.iterator 等。当发生引用类型强制转基本类型的操做时,就会触发内置的Symbol.toPrimitive函数,固然也能够给对象手动添加Symbol.toPrimitive函数来覆盖默认的强制类型转换行为。

object是引用类型,引用类型和基本类型不一样的是,原始类型存储的是值,引用类型存储的是一个指向对象真实内存地址的指针。在 js 中,对象包括Array Object Function RegExp Math等。

js 全部的函数语句都是在执行栈中执行的,全部的变量也在执行栈中保存着值或引用。基本类型就存储在栈内存中,保存的是实际值;引用类型存储在堆内存中,在栈中只保存着变量指向内存地址的指针。

var o = {
  a: 'a',
  b: 'b'
}
var o2 = o // 变量o2复制了变量o的指针,如今他们都指向同一个内存地址,如今开始他们的增删改实际上是在同一个内存地址上的操做
o2.c = 'c' // (增)如今o.c也是'c'
delete o2.b // (删)如今o.b也不存在了
o2.a = 'a2' // (改)如今o.a也是'a2'
o2 = 'o2' // 如今变量o2被赋值'o2',已经和原来的内存地址断绝了关系,但变量 o 仍然指向老地址
复制代码

类型判断

判断引用类型和基本类型的类型是不一样的,判断基本类型能够用typeof

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
复制代码

能够看到除了null其余基本类型的判断都是正常的,typeof(null) === 'object'是一个历史悠久的 bug,就是在 JS 的最第一版本中null的内存存储信息是000开头的,而000开头的会被判断为object类型。虽然如今内部类型判断代码已经改变了,可是这个 bug 却不得不随着版本保留了下来,由于修改这个 bug 会致使巨多的网站出现 bug 。

typeof对引用类型,除了函数返回function,其余都返回object。但咱们开发中数组确定是要返回array类型的,因此typeof对引用类型来讲并非很适用。判断引用类型通常用instanceof

var obj = {}
var arr = []
var fun = () => {}
typeof obj // 'object'
typeof arr // 'object'
typeof fun // 'function'
obj instanceof Object // true
arr instanceof Array // true
fun instanceof Function // true
复制代码

能够看到instanceof操做符能够正确判断出引用类型的类型。instanceof本质上是判断右边的构造函数的prototype对象是否存在于左边的原型链上,是的话返回true。因此不论数组、对象仍是函数,... instanceof Object都返回true

最后来一种全能型判断类型方法:Object.prototype.toString.call(...),能够自行尝试。

强制类型转换

JS 是弱类型语言,不一样类型之间在必定状况下会发生强制类型转换,好比在相等性比较的时候。

基本类型的相等性比较的是值是否同样,对象相等性比较的是内存地址是否相同。下面来看一个有意思的比较把:

[] == [] // ?
[] == ![] // ?
复制代码

对于[] {} function (){}这样的没有被赋值给变量的引用类型来讲,他们只在当前语句中有效,并且不相等于其余任何对象。由于根本没法找到他们的内存地址的指针。因此[] == []false

对于[] == ![],由于涉及到强制类型转换,因此复杂的多了。想要更加详细了解强制类型转换能够看我这篇文章

在 JS 中类型转换只有三种状况:toNumbertoStringtoBoolean 。正常状况下转换规则以下:

原始值/类型 目标类型:number 结果
null number 0
symbol number 抛错
string number '1'=>1 '1a'=>NaN ,含非数字则为NaN
数组 number []=>0 ['1']=>1 ['1', '2']=>NaN
object/function/undefined number NaN
原始值/类型 目标类型:string 结果
number string 1=>'1'
array string [1, 2]=>'1,2'
布尔值/函数/symbol string 原始值加上引号,如:'true' 'Sumbol()'
object string {}=>'[object Object]'
原始值/类型 目标类型:boolean 结果
number boolean 除了0NaNfalse,其余都是true
string boolean 除了空字符串为false,其余都为true
null/undefined boolean false
引用类型 boolean true

如今来揭开 [] == ![] 返回true的真相把:

[] == ![] // true
/* * 首先,布尔操做符!优先级更高,因此被转变为:[] == false * 其次,操做数存在布尔值false,将布尔值转为数字:[] == 0 * 再次,操做数[]是对象,转为原始类型(先调用valueOf(),获得的仍是[],再调用toString(),获得空字符串''):'' == 0 * 最后,字符串和数字比较,转为数字:0 == 0 */
NaN == NaN // false NaN不等于任何值
null == undefined // true
null == 0 // false
undefined == 0 // false
复制代码

做用域

js 中的做用域是词法做用域,是由 函数声明时 所在的位置决定的。词法做用域是指在编译阶段就产生的,一整套函数标识符的访问规则。 说到底js的做用域只是一个“空地盘”,其中并无真实的变量,可是却定义了变量如何访问的规则。(词法做用域是在编译阶段就确认的,区别于词法做用域,动态做用域是在函数执行的时候确认的,js的没有动态做用域,但js的this很像动态做用域,后面会提到。语言也分为静态语言和动态语言,静态语言是指数据类型在编译阶段就肯定的语言如 java,动态语言是指在运行阶段才肯定数据类型的语言如 javascript。)

做用域链本质上是一个指向变量对象的指针列表,它只引用不包含实际变量对象,是做用域概念的延申。做用域链定义了在当前上下文访问不到变量的时候如何沿做用域链继续查询变量的一套规则。

event loop

js 是单线程的,全部任务须要排队,前一个任务结束,才会执行后一个任务。若是前一个任务耗时很长,后一个任务就不得不一直等着。可是IO设备(输入输出设备)很慢(好比Ajax操做从网络读取数据),js 不可能等待IO设备执行完成才继续执行下一个的任务,这样就失去了这门语言的意义。因此 js 的任务分为同步任务和异步任务。

  1. 全部同步任务都是在主线程执行,造成一个“执行栈”(就是下图中的stack);
  2. 全部的异步任务都会暂时挂起,等待运行有告终果以后,其回调函数就会进入“任务队列”(task queue)排队等待;
  3. 当执行栈中的全部同步任务都执行完成以后,就会读取任务队列中的第一个的回调函数,并将该回调函数推入执行栈开始执行;
  4. 主线程不断循环重复第三步,这就是“event loop”的运行机制。

上图中,主线程运行的时候,产生堆(heap)和栈(stack),堆用来存放数组对象等引用类型,栈中的代码调用各类外部API,它们在"任务队列"中加入各类事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。

任务队列中有两种异步任务,一种是宏任务,包括script setTimeout setInterval等,另外一种是微任务,包括Promise process.nextTick MutationObserver等。每当一个 js 脚本运行的时候,都会先执行script中的总体代码;当执行栈中的同步任务执行完毕,就会执行微任务中的第一个任务并推入执行栈执行,当执行栈为空,则再次读取执行微任务,循环重复直到微任务列表为空。等到微任务列表为空,才会读取宏任务中的第一个任务并推入执行栈执行,当执行栈为空则再读取执行微任务,微任务为空才再读取执行宏任务,如此循环。

执行上下文:

执行上下文是指 函数调用时 在执行栈中产生的当前函数(或全局对象window)的执行环境,这个环境就像一个隔绝外面世界的容器结界,里面存放着能够访问的变量、this对象等。例如:

let fn, bar; // 一、进入全局上下文环境
bar = function(x) {
  let b = 5;
  fn(x + b); // 三、进入fn函数上下文环境
};
fn = function(y) {
  let c = 5;
  console.log(y + c); //四、fn出栈,bar出栈
};
bar(10); // 二、进入bar函数上下文环境
复制代码

每次函数调用时,执行栈栈顶都会产生一个新的执行上下文环境。栈底永远都是全局上下文,而栈顶就是当前处于活动状态的正在执行代码的上下文。

理解函数的执行过程

本文重点,让你对函数执行过程的理解更上一层楼!

函数的执行过程分红两阶段,第一阶段是建立执行上下文环境阶段,第二阶段是代码执行阶段:

  • 建立执行上下文阶段(发生在 函数被调用时 && 函数体内的代码执行前 )。

  1. 建立变量对象,这个过程会:建立 arguments 对象,初始化函数参数变量 ---> 检查建立当前上下文环境中的function函数声明(即所谓的函数声明提高) ---> 检查建立当前上下文环境中的var变量声明(即所谓变量提高)、let const声明;

  1. 创建做用域链,肯定在当前上下文环境寻找变量的规则;
  2. 肯定this对象的指向;
  • 代码执行阶段
  1. 执行函数体内的代码,这个阶段会完成变量赋值,函数引用,以及执行其余代码。

在未进入执行阶段以前,变量对象中的属性还在建立都不能访问。可是进入执行阶段以后,变量对象建立完成转变为了活动对象,里面的属性都能被访问了,而后才开始进行执行阶段的操做。也就是说,变量对象和活动对象的惟一区别就是处于执行上下文的不一样生命周期。

变量对象更详细介绍参考此文

this 指向

let fn = function(){
  alert(this.name)
}
let obj = {
  name: '',
  fn
}
fn() // 方法1
obj.fn() // 方法2
fn.call(obj) // 方法3
let instance = new fn() // 方法4
复制代码
  1. 方法1中直接调用函数fn(),这种看着像光杆司令的调用方式,this指向window(严格模式下是undefined)。
  2. 方法2中是点调用obj.fn(),此时this指向obj对象。点调用中this指的是点前面的对象。
  3. 方法3中利用call函数把fn中的this指向了第一个参数,这里是obj。即利用callapplybind函数能够把函数的this变量指向第一个参数。
  4. 方法4中用new实例化了一个对象instance,这时fn中的this就指向了实例instance

若是同时发生了多个规则怎么办?其实上面四条规则的优先级是递增的:

fn() < obj.fn() < fn.call(obj) < new fn()

首先,new调用的优先级最高,只要有new关键字,this就指向实例自己;接下来若是没有new关键字,有call、apply、bind函数,那么this就指向第一个参数;而后若是没有new、call、apply、bind,只有obj.foo()这种点调用方式,this指向点前面的对象;最后是光杆司令foo() 这种调用方式,this指向window(严格模式下是undefined)。

es6中新增了箭头函数,而箭头函数最大的特点就是没有本身的this、arguments、super、new.target,而且箭头函数没有原型对象prototype不能用做构造函数(new一个箭头函数会报错)。由于没有本身的this,因此箭头函数中的this其实指的是包含函数中的this。不管是点调用,仍是call调用,都没法改变箭头函数中的this

闭包

很长时间以来我对闭包都停留在“定义在一个函数内部的函数”这样肤浅的理解上。事实上这只是闭包造成的必要条件之一。直到后来看了kyle大佬的《你不知道的javascript》上册关于闭包的定义,我才豁然开朗:

当函数可以记住并访问所在的词法做用域时,就产生了闭包。

let single = (function(){
  let count = 0
  return {
    plus(){
      count++
      return count
    },
    minus(){
      count--
      return count
    }
  }
})()
single.plus() // 1
single.minus() // 0
复制代码

这是个单例模式,这个模式返回了一个对象并赋值给变量single,变量single中包含两个函数plusminus,而这两个函数都用到了所在词法做用域中的变量count。正常状况下count和所在的执行上下文会在函数执行结束时被销毁,可是因为count还在被外部环境使用,因此在函数执行结束时count和所在的执行上下文不会被销毁,这就产生了闭包。每次调用single.plus()或者single.minus(),就会对闭包中的count变量进行修改,这两个函数就保持住了对所在的词法做用域的引用。

闭包实际上是一种特殊的函数,它能够访问函数内部的变量,还可让这些变量的值始终保持在内存中,不会在函数调用后被垃圾回收机制清除。

看个经典安利:

// 方法1
for (var i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i)
  }, 1000)
}
// 方法2
for (let i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i)
  }, 1000)
}
复制代码

方法1中,循环设置了五个定时器,一秒后定时器中回调函数将执行,打印变量i的值。毋庸置疑,一秒以后i已经递增到了6,因此定时器打印了五次6 。(定时器中并无找到当前做用域的变量i,因此沿做用域链找到了全局做用域中的i

方法2中,因为es6的let会建立局部做用域,因此循环设置了五个做用域,而五个做用域中的变量i分布是1-5,每一个做用域中又设置了一个定时器,打印一秒后变量i的值。一秒后,定时器从各自父做用域中分别找到的变量i是1-5 。这是个利用闭包解决循环中变量发生异常的新方法。

原型和原型链

js 中的几乎全部对象都有一个特殊的[[Prototype]]内置属性,用来指定对象的原型对象,这个属性实质上是对其余对象的引用。在浏览器中通常都会暴露一个私有属性 __proto__,其实就是[[Prototype]]的浏览器实现。假若有一个对象var obj = {},那么能够经过obj.__proto__ 访问到其原型对象Object.prototype,即obj.__proto__ === Object.prototype。对象有[[Prototype]]指向一个原型对象,原型对象自己也是对象也有本身的[[Prototype]]指向别的原型对象,这样串接起来,就组成了原型链。

var obj = [1, 2, 3]
obj.__proto__ === Array.prototype // true
Number.prototype.__proto__ === Object.prototype // true
Object.prototype.__proto__ === null // true
obj.toString()
复制代码

能够看出,上例中存在一个从objnull的原型链,以下:

obj----__proto__---->Array.prototype----__proto__---->Object.prototype----__proto__---->null
复制代码

上例中最后一行调用obj.toString()方法的时候,js 引擎就是沿着这条原型链查找toString方法的。js 首先在obj对象自身上查找toString方法;未找到,继续沿着原型链查找Array.prototype上有没有toString;未找到,继续沿着原型链在Object.prototype上查找。最终在Object.prototype上找到了toString方法,因而泪流满面的调用该方法。这就是原型链最基本的做用。原型链仍是 js 实现继承的本质所在,下一小节再讲。

上面我说“js 中的几乎全部对象都有一个特殊的[[Prototype]]内置属性”,为何不是所有呢?由于 js 能够建立没有内置属性[[Prototype]]的对象:

var o = Object.create(null)
o.__proto__ // undefined
复制代码

Object.create是 es5 的方法,全部浏览器都已支持。该方法建立并返回一个新对象,并将新对象的原型对象赋值为第一个参数。在上例中,Object.create(null)建立了一个新对象并将对象的原型对象赋值为null。此时对象 o 是没有内置属性[[Prototype]]的(不知道为何o.__proto__不是null,但愿知道的大佬评论解释下,万分感激)。

js 的继承

js 的继承是经过原型链实现的,具体能够参考个人这篇文章,这里我只讲一讲你们可能比较陌生的“行为委托”。行为委托是《你不知道的JavaScript》系列做者 kyle 大佬推荐的一种代替继承的方式,该模式主要利用setPrototypeOf方法把一个对象的内置原型[[Protytype]]关联到另外一个对象上,从而达到继承的目的。

let SuperType = {
  initSuper(name) {
    this.name = name
    this.color = [1,2,3]
  },
  sayName() {
    alert(this.name)
  }
}
let SubType = {
  initSub(age) {
    this.age = age
  },
  sayAge() {
    alert(this.age)
  }
}
Object.setPrototypeOf(SubType,SuperType)
SubType.initSub('17')
SubType.initSuper('gim')
SubType.sayAge() // '17'
SubType.sayName() // 'gim'
复制代码

上例就是把父对象SuperType关联到子对象SubType的内置原型上,这样就能够在子对象上直接调用父对象上的方法。行为委托生成的原型链比class继承生成的原型链的关系简单清晰,一目了然。

kyle大佬倡导的行为委托
相关文章
相关标签/搜索