纲要

  • 基本类型和引用类型

  • 类型判断

  • 强制类型转换

  • 作用域

  • 执行上下文

  • 理解函数的执行过程

  • this 指向

  • 闭包

  • 原型和原型链

  • js 的继承

  • event loop

基本类型和引用类型

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

  • number

  • string

  • boolean

  • null

  • undefined

  • symbol (es6)

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

  • object

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

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等对象原型上的方法。不信?

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含义为“无”、“空”或“值未知”的特殊值。

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 中类型转换只有三种情况:toNumber 、 toString 、 toBoolean 。正常情况下转换规则如下:

|原始值/类型|目标类型: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'|
|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。)

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

执行上下文:

执行上下文是指 函数调用时 在执行栈中产生的变量对象,这个变量对象我们无法直接访问,但是可以访问其中的变量、this对象等。例如:

let fn, bar; // 1、进入全局上下文环境
bar = function(x) {
  let b = 5;
  fn(x + b); // 3、进入fn函数上下文环境
};
fn = function(y) {
  let c = 5;
  console.log(y + c); //4、fn出栈,bar出栈
};
bar(10); // 2、进入bar函数上下文环境

每次函数调用时,执行栈栈顶都会产生一个新的执行上下文环境,JavaScript引擎会以栈的方式来处理它们,这个栈,我们称其为函数调用栈(call stack)。栈底永远都是全局上下文,而栈顶就是当前处于活动状态的正在执行的上下文,也称为活动对象(running execution context,图中蓝色的块),区别与底下被挂起的变量对象(执行上下文)。

理解函数的执行过程

函数的执行过程分成两部分,一部分用来生成执行上下文环境,确定this的指向、声明变量以及生成作用域链;另一部分则是按顺序逐行执行代码。

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

  1. 生成变量对象,顺序:创建 arguments 对象 --> 创建function函数声明 --> 创建var变量声明

  2. 生成作用域链

  3. 确定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已经递增到了5,所以定时器打印了五次5 。(定时器中并没有找到当前作用域的变量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
Array.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() // 'gim'
SubType.sayName() // '17'

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

kyle大佬倡导的行为委托

event loop

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

  1. 所有同步任务都是在主线程执行,形成一个“执行栈”(execution context stack);

  2. 所有的异步任务都会暂时挂起,等待运行有了结果之后,其回调函数就会进入“任务队列”(task queue)排队等待;

  3. 当执行栈中的所有同步任务都执行完成之后,就会读取任务队列中的第一个的回调函数,并将该回调函数推入执行栈开始执行;

  4. 主线程不断循环重复第三步,这就是“event loop”的运行机制。

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

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

JS - JavaScript 主要知识点(基础夯实)的更多相关文章

  1. 前端基础 & 初识JS(JavaScript)

    JavaScript概述 JavaScript的历史 1992年Nombas开发出C-minus-minus(C--)的嵌入式脚本语言(最初绑定在CEnvi软件中),后将其改名ScriptEase(客 ...

  2. JS获取url中query_str JavaScript RegExp 正则表达式基础详谈

    面我们举例一个URL,然后获得它的各个组成部分:http://i.cnblogs.com/EditPosts.aspx?opt=1 1.window.location.href(设置或获取整个 URL ...

  3. JavaScript基本知识点——带你逐步解开JS的神秘面纱

    JavaScript基本知识点--带你逐步解开JS的神秘面纱 在我们前面的文章中已经深入学了HTML和CSS,在网页设计中我们已经有能力完成一个美观的网页框架 但仅仅是网页框架不足以展现出网页的魅力, ...

  4. 前端开发 JavaScript 干货知识点汇总

    很多初学的朋友经常问我,前端JavaScript都需要学习哪些东西呀?哪些是JavaScript的重点知识啊? 其实做前端开发工程师,所有的知识点都是我们学习必备的东西,只有扎实的技术基础才是高薪的关 ...

  5. Javascript重要知识点梳理

    Javascript重要知识点梳理 一.Javascript流程控制 js中常用的数据类型 var关键字的使用 if – else if – else switch while for 二.Javas ...

  6. JavaScript学习02 基础语法

    JavaScript学习02 基础语法 JavaScript中很多基础内容和Java中大体上基本一样,所以不需要再单独重复讲了,包括: 各种算术运算符.比较运算符.逻辑运算符: if else语句.s ...

  7. 从头开始学JavaScript 笔记(一)——基础中的基础

    原文:从头开始学JavaScript 笔记(一)--基础中的基础 概要:javascript的组成. 各个组成部分的作用 . 一.javascript的组成   javascript   ECMASc ...

  8. 关于JS脚本语言的基础语法

    JS脚本语言的基础语法:输出语法  alert("警告!");  confirm("确定吗?");   prompt("请输入密码");为弱 ...

  9. Android基础夯实--重温动画(四)之属性动画 ValueAnimator详解

    宝剑锋从磨砺出,梅花香自苦寒来:千淘万漉虽辛苦,吹尽狂沙始到金: 长风破浪会有时,直挂云帆济沧海 一.摘要 Animator类作为属性动画的基类,它是一个抽象类,它提供了实现动画的基本架构,但是我们不 ...

  10. Android基础夯实--重温动画(三)之初识Property Animation

    每个人都有一定的理想,这种理想决定着他的努力和判断的方向.就在这个意义上,我从来不把安逸和快乐看作生活目的的本身--这种伦理基础,我叫它猪栏的理想.--爱因斯坦 一.摘要 Property Anima ...

随机推荐

  1. Spring事务(三)-事务传播行为

    在Spring里,一个事务方法被另外一个事务方法调用时,两个方法的事务应该如何进行,说白话一点,就是说当出现异常需要回滚时,各个方法的数据操作是否要全部回滚,事务传播行为就是决定了这样的一个处理结果. ...

  2. 本地锁 & 分布式锁

    引子: 解决缓存击穿问题 synchronized (this){代码块} public synchronized Map<String,List<Catelog2Vo>> g ...

  3. C#实现一个简单的日志类

    目录 自定义日志类 NLog版本的日志类 Serilog版本的日志类 上个月换工作,新项目又要重新搭建基础框架,把日志实现部分单独记录下来方便以后参考. 自定义日志类 代码大部分使用ChatGPT生成 ...

  4. C++数值类型与string、CString之间的转换

    目录 数值范围 数值类型与string互相转换 数值类型转换为string 使用函数模板+ostringstream 使用标准库函数std::to_string() string转换为数值类型 使用函 ...

  5. day10-Spring Cloud Alibaba Nacos-服务注册与配置中心

    Spring Cloud Alibaba Nacos-服务注册与配置中心 官网:https://github.com/alibaba/nacos,Nacos官方中文手册 Nacos:Spring Cl ...

  6. day06-Java流程控制

    Java流程控制 1.用户交互Scanner java.util.Scanner是Java5的新特征,我们可以通过Scannner类来获取用户的输入. 基本语法: Scanner s = new Sc ...

  7. dotNet8 全局异常处理

    前言 异常的处理在我们应用程序中是至关重要的,在 dotNet 中有很多异常处理的机制,比如MVC的异常筛选器, 管道中间件定义try catch捕获异常处理亦或者第三方的解决方案Hellang.Mi ...

  8. Miracast技术详解(二):RTSP协议

    目录 RTSP概述 抓包准备 WFD能力协商(Capability Negotiation) RTSP M1 Messages RTSP M2 Messages RTSP M3 Messages RT ...

  9. Python配置文件使用教程

    在 Python 应用程序开发过程中,配置文件扮演着重要的角色.配置文件可以用来存储应用程序的各种设置.选项和参数,使得程序更加灵活和可配置.本文将介绍 Python 中如何使用配置文件,并提供一些常 ...

  10. 记录--@click和@click.native有什么区别,如何阻止第三方组件内部的冒泡

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 一.@click和@click.native的区别 vue @click.native 原生点击事件: 1,给vue组件绑定事件时候,必须 ...