# Javascript 基础
# 数据类型
在 javascript
中数据值的类型可以分为两大类,分别是基本类型和引用类型。基本类型都存储在栈内存上,而引用类型通常都存储在堆内存上。
# 基本类型
基本数据类型有七大类:
Number:number 类型包括了整数类型和浮点数类型,所有的数字类型都可以用 number 表示
Boolean:boolean 类型就只有两个值,true 和 false。通常用于条件判断
String:string 类型表示字符串
Null:null 类型一般用于给一个准备设置为引用类型的数据赋初始值,如
var a = null
,表示变量a
在后面可能会被赋予一个引用类型的值Undefined:undefined 类型用于给变量当默认值,如
var a;console.log(a)
,此时会输出undefined
,我们没有在声明变量后给一个初始值时 js 引擎会给它默认添加undefined
作为默认值undefined 在 JavaScript 中不是一个保留字,它作为一个属性挂载在 Windows 对象上,这意味着可以使用 undefined 来作为一个变量名,但是这样的做法是非常危险的,它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。
Symbol:symbol 类型被设计之初是用于解决命名冲突问题,用它作为标识符永远不会与其他变量冲突。如
console.log(Symbol(1)===Symbol(1))
,输出 false。即便传入的变量都为 1,获取的结果也是不同的BigInt:BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
基本数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储
# 引用类型
除了基本类型以外的类型都是引用类型,如 Array、Object、Function。
引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体
# 类型判断
# typeof
typeof
一般用于判断基本数据类型,但是它在判断 null
时会被判断为 object
console.log(typeof 2); // number | |
console.log(typeof true); // boolean | |
console.log(typeof 'str'); // string | |
console.log(typeof []); // object | |
console.log(typeof function(){}); // function | |
console.log(typeof {}); // object | |
console.log(typeof undefined); // undefined | |
console.log(typeof null); // object |
# instanceof
instanceof
一般用于判断引用数据类型,它的运行原理是判断其在原型链中是否能够找到该类型的原型
console.log(2 instanceof Number); // false | |
console.log(true instanceof Boolean); // false | |
console.log('str' instanceof String); // false | |
console.log([] instanceof Array); // true | |
console.log(function(){} instanceof Function); // true | |
console.log({} instanceof Object); // true |
instanceof
简单实现
function myInstanceof(left, right) { | |
if (left === null) return false | |
if (typeof left === "object" || typeof left === "function") { | |
let leftProto = Object.getPrototypeOf(left) | |
const rightProto = right?.prototype | |
while (leftProto) { | |
if (leftProto === rightProto) { | |
return true | |
} | |
leftProto = Object.getPrototypeOf(leftProto) | |
} | |
return false | |
} | |
return false | |
} |
# constructor
constructor
有两个作用,一是判断数据的类型,二是对象实例通过 constructor
对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型, constructor
就不能用来判断数据类型了
console.log((2).constructor === Number); // true | |
console.log((true).constructor === Boolean); // true | |
console.log(('str').constructor === String); // true | |
console.log(([]).constructor === Array); // true | |
console.log((function() {}).constructor === Function); // true | |
console.log(({}).constructor === Object); // true |
function Fn(){}; | |
Fn.prototype = new Array(); | |
var f = new Fn(); | |
console.log(f.constructor===Fn); // false | |
console.log(f.constructor===Array); // true |
# Object.prototype.toString.call()
Object.prototype.toString.call()
使用 Object 对象的原型方法 toString 来判断数据类型
var a = Object.prototype.toString; | |
console.log(a.call(2)); | |
console.log(a.call(true)); | |
console.log(a.call('str')); | |
console.log(a.call([])); | |
console.log(a.call(function(){})); | |
console.log(a.call({})); | |
console.log(a.call(undefined)); | |
console.log(a.call(null)); |
同样是检测对象 obj 调用 toString 方法,obj.toString () 的结果和 Object.prototype.toString.call (obj) 的结果不一样,这是为什么?
这是因为 toString 是 Object 的原型方法,而 Array、function 等类型作为 Object 的实例,都重写了 toString 方法。不同的对象类型调用 toString 方法时,根据原型链的知识,调用的是对应的重写之后的 toString 方法(function 类型返回内容为函数体的字符串,Array 类型返回元素组成的字符串…),而不会去调用 Object 上原型 toString 方法(返回对象的具体类型),所以采用 obj.toString () 不能得到其对象类型,只能将 obj 转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用 Object 原型上的 toString 方法。
# 判断数组
- 通过 Object.prototype.toString.call () 做判断
Object.prototype.toString.call(obj).slice(8,-1) === 'Array'; |
- 通过原型链做判断
obj.__proto__ === Array.prototype; |
- 通过 ES6 的 Array.isArray () 做判断
Array.isArray(obj);
- 通过 instanceof 做判断
obj instanceof Array |
- 通过 Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj) |
# 类型转换
# == 和 ===
对于 ==
来说,如果对比双方的类型不一样,就会进行类型转换。假如对比 x
和 y
是否相同,就会进行如下判断流程:
- 首先会判断两者类型是否 ** 相同,** 相同的话就比较两者的大小;
- 类型不相同的话,就会进行类型转换;
- 会先判断是否在对比
null
和undefined
,是的话就会返回true
- 判断两者类型是否为
string
和number
,是的话就会将字符串转换为number
1 == '1' | |
↓ | |
1 == 1 |
- 判断其中一方是否为
boolean
,是的话就会把boolean
转为number
再进行判断
'1' == true
↓
'1' == 1
↓
1 == 1
- 判断其中一方是否为
object
且另一方为string
、number
或者symbol
,是的话就会把object
转为原始类型再进行判断
'1' == { name: 'js' }
↓
'1' == '[object Object]'
其流程图如下:
# 其他类型到数值类型的转换规则
- Undefined 类型的值转换为 NaN。
- Null 类型的值转换为 0。
- Boolean 类型的值,true 转换为 1,false 转换为 0。
- String 类型的值转换如同使用 Number () 函数进行转换,如果包含非数字值则转换为 NaN,空字符串为 0。
- Symbol 类型的值不能转换为数字,会报错。
- 对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。
为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先(通过内部操作 DefaultValue)检查该值是否有 valueOf () 方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString () 的返回值(如果存在)来进行强制类型转换。
如果 valueOf () 和 toString () 均不返回基本类型值,会产生 TypeError 错误。
# 其他类型到布尔类型的转换规则
以下这些是假值:
- undefined
- null
- false
- +0、-0 和 NaN
- ""
假值的布尔强制类型转换结果为 false。从逻辑上说,假值列表以外的都应该是真值。
# 变量类型
# var
在全局作用域下声明的 var
变量会有变量提升,同时它会被挂载到 window
对象上作为一个属性
console.log(asuhe) | |
var asuhe = 10 | |
console.log(window) |

# let
let
声明的变量其作用域会绑定在最近的 {}
花括号里面,而且不存在变量提升。
// 作用域绑定 | |
{ | |
let a = 10 | |
console.log("inner",a) // inner 10 | |
} | |
console.log("outer",a) // ReferenceError |
同时对于函数作用域内的同名变量会有 暂时性死区
的效果, 暂时性死区
和没有变量提升配合起来会屏蔽变量作用域的查找
var a = 10 | |
function foo(){ | |
console.log(a) | |
let a = 20 | |
} | |
foo() // ReferenceError |
# const
const
变量基本特点和 let
一致,但是 const
所指向的那个值不允许被修改,因此 const
在声明之初一定要赋予一个初始值
const a // SyntaxError | |
const b = 10 | |
b = 20 // TypeError | |
const c = { n:30 } | |
c.n = 40 | |
console.log(c.n) // 40 |
# 函数
# 函数声明
函数声明有函数提升的效果,而函数声明表达式则没有
// 因为函数提升,所以函数可以在声明前被调用 | |
foo(1,2) // 3 | |
function foo(a,b){ | |
console.log(a + b) | |
} |
# 函数声明表达式
函数声明表达式没有函数提升效果,所以必须在赋值完成后才能调用
// 无函数提升 | |
foo(1,2) // TypeError 此时 foo 为 undefined | |
var foo = function(a,b){ | |
console.log(a*b) | |
} | |
foo(1,2) // 2 |
foo(1, 2) // 3 | |
function foo(a, b) { | |
console.log(a + b) | |
} | |
foo(1, 2) // 3 | |
var foo = function foo(a,b){ | |
console.log(a * b) | |
} | |
foo(1, 2) // 2 |
# this 指向
在使用 function
关键字声明的函数中, this
指向的是其调用者
function foo(){ | |
console.log(this.a) | |
} | |
var a = 10 | |
let obj = { | |
f:foo, | |
a:20 | |
} | |
obj.f() // 20 | |
foo() // 10 |
匿名函数没有自己的 this
,匿名函数中的 this
是匿名函数定义时的上层作用域的 this
let obj = { | |
f:()=> console.log(this.a), | |
a:10 | |
} | |
var a = 20 | |
obj.f() // 20 |
# 匿名函数
匿名函数只有一个形参时,可以省略(),且在函数体只有一句时可以省略 {} 并将该语句结果返回
let f = n => n+1
console.log(f(2)) // 3
匿名函数没有自己的
this
,因此也不能使用call
、apply
、bind
等方法来改变this
指向由于箭头函数时没有自己的 this 的,且 this 指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用
let Student = (a, b) => a + b
let s1 = new Student() // TypeError
箭头函数没有自己的 arguments 对象。在箭头函数中访问 arguments 实际上获得的是它外层函数的 arguments 值
箭头函数没有 prototype
箭头函数不能用作 Generator 函数,不能使用 yeild 关键字
# 作用域 && 作用域链
在 ES6 以前 javascript 并没有像其他语言一样拥有 块级作用域
的概念,仅有函数作用域和全局作用域这两种作用域。例如:
{ | |
var a = 20 | |
} | |
console.log(a) // 20 |
而在 ES6 新增了 let
、 const
关键字后,由这两种关键字声明的变量其作用域会绑定在其最近的 {}
内。例如:
{ | |
let a = 20 | |
} | |
console.log(a) // ReferenceError |
在 JavaScript 里面,函数、块、模块都可以形成作用域(一个存放变量的独立空间),他们之间可以相互嵌套,作用域之间会形成引用关系,这条链叫做作用域链。
当我们去查找一个变量 a 时,若这个变量 a 不在本级作用域中那么它会继续往它的上级作用域中去寻找该变量的声明。若最后找不到就报错。
# 闭包
闭包就是在一个变量对象里持有另一个变量对象里内容的引用,这时就会产生闭包。常见的表现形式就是,内部函数持有外部函数的变量,我们可以通过返回内部函数去让更外层的作用域能够访问到内部函数的父函数里的变量。
function fun(n, o) { | |
console.log(o) | |
return { | |
fun: function (m) { | |
return fun(m, n) | |
} | |
} | |
} | |
var a = fun(0) | |
a.fun(1) | |
a.fun(2) | |
a.fun(3) | |
var b = fun(0).fun(1).fun(2).fun(3) |
# 常用对象 && 对象方法
# Array 对象
# 静态方法
# Array.from
Array.from()
从一个可迭代对象或类数组对象中,创建一个新的数组实例。它实现的是浅拷贝。
参数:
arrayLike:一个可迭代对象或类数组对象
mapFn(可选):遍历函数
thisArg(可选):执行遍历函数时的 this 指针
console.log(Array.from("asuhe")) // ["a", "s", "u", "h", "e"] | |
console.log(Array.from([2,4,5,6])) // [2,4,5,6] | |
// 传入遍历函数 | |
function map(x){ | |
return x + 1 | |
} | |
console.log(Array.from([2,4,5,6],map)) // [3,5,6,7] | |
// 传入 this 参数 | |
function Map(x){ | |
console.log(this.a) | |
return x + 2 | |
} | |
const obj = { a:666 } | |
console.log(Array.from([1,2,3],Map,obj)) // 666 666 666 [3,4,5] |
# Array.isArray
判断一个变量是否为数组
参数:
value:需要判断的变量
console.log(Array.isArray([1,2,3])) // true | |
console.log(Array.isArray(1)) // false | |
console.log(Array.isArray({a:666})) // false | |
console.log(Array.isArray("asuhe")) // false |
# Array.of
根据传入的参数创建一个数组,浅拷贝
参数:
elementN:传入的参数可以为可变数量,传多少个就创建多长的数组
const obj = {a:666} | |
const test = Array.of(1,2,'asuhe',obj) | |
console.log(test) // [1, 2, "asuhe", Object { a: 666 }] | |
obj.a = 777 | |
console.log(test) // [1, 2, "asuhe", Object { a: 777 }] |
# 实例方法
# concat
合并多个参数,返回一个新数组。浅拷贝
参数:
valueN:参数可以为数组,也可以为普通参数。
const array1 = ['a', 'b', 'c'] | |
const array2 = ['d', 'e', 'f'] | |
const array3 = array1.concat(array2,666) | |
console.log(array3) // ["a", "b", "c", "d", "e", "f",666] |
# filter
过滤出回调函数返回值为 true 的元素,返回一个符合条件的新数组。
参数:
callbackFn:遍历的回调
thisArg(可选):回调执行时的 this
const arr = [1,2,3,4,5,6,7] | |
const res = arr.filter((element,index,arr)=>{ | |
return element%2 === 1 | |
}) | |
console.log(res) // [1, 3, 5, 7] |
# every
当所有的元素都通过了回调函数的条件时,返回一个 true 的布尔值,否则返回 false。
参数:
callbackFn:遍历的回调
thisArg(可选):回调执行时的 this
function isOdd(e){ | |
return e%2===1 | |
} | |
const arr = [1,3,5,7] | |
console.log(arr.every(isOdd)) // true |
# some
当数组中有一个元素通过了回调函数的条件时,返回一个 true 的布尔值,否则返回 false。
参数:
callbackFn:遍历的回调
thisArg(可选):回调执行时的 this
function isOdd(e){ | |
return e%2===1 | |
} | |
const arr = [2,3,4,6,8] | |
console.log(arr.some(isOdd)) // true |
# reduce
根据遍历函数的返回值,一直传递给下次遍历函数。一般用于计算总和
参数:
callbackFn:遍历的回调
initialValue(可选):回调执行时的 this
const initialValue = 0 | |
const arr = [1,2,3,4,5] | |
const res = arr.reduce((preValue,e) => preValue+e,initialValue ) | |
console.log(res) // 15 |
# Object 对象
# 静态方法
# Object.keys
返回一个数组,数组内容为对象中所有的可枚举属性的 key 值
const flag = Symbol() | |
const obj = { | |
a:1, | |
b:"asuhe", | |
hhh:666, | |
[flag]:"sphinx" | |
} | |
console.log(Object.keys(obj)) // ["a", "b", "hhh"] |
# Object.getPrototypeOf
获取一个对象的隐式原型
const obj = { | |
name:"asuhe", | |
age:16 | |
} | |
const objProto = Object.getPrototypeOf(obj) | |
console.log(objProto === Object.prototype) // true |
# Object.create
使用传入的对象作为 prototype
,创建一个新的对象
const obj = { | |
name:"asuhe", | |
age:16 | |
} | |
const res = Object.create(obj) | |
console.log(Object.getPrototypeOf(res) === obj) // true |
# Object.assign
从源对象中,合并源对象的可枚举属性进目标对象,并返回修改后的对象
参数:
target:需要修改的目标对象
source:提供可枚举属性来源的源对象
const target = { | |
name:"asuhe", | |
age:16 | |
} | |
const source = { | |
gender:"male" | |
} | |
const res = Object.assign(target,source) | |
console.log(res) // { name: "asuhe", age: 16, gender: "male" } | |
console.log(res === target) // true |
# 逻辑中断
# &&
&&
逻辑与,当前一个条件判断为真时,才会继续执行后一个条件,并返回表达式的值
console.log(1+2 && 3+4) // 7 | |
console.log(0 && 2 + 4) // 0 |
# ||
||
逻辑或,当前一个条件判断为假时,才会继续执行后一个条件,并返回表达式的值
console.log( 0 || 3 + 3) // 6 | |
console.log( 1 + 4 || 4 + 5) // 5 |
# ? :
? :
三目运算符,当条件值为 true 时,返回第一个表达式的值,否则返回第二个表达式的值
console.log( 1 ? 1+2 : 3+4 ) // 3 | |
console.log( 0 ? 1+2 : 3+4) // 7 |
# 原型链
- 所有对象都有一个隐式原型
__proto__
- 所有函数对象都有一个原型
prototype
- 以某函数为构造函数,
new
出实例的隐式原型__proto__
都会指向该函数的原型prototype
- 所有函数对象都是
Function
的实例,包括Function
自身 - 所有函数对象的
prototype
都是Object
的实例
function Foo(a,b){ | |
return a+b | |
} | |
const obj = new Foo() | |
console.log(obj instanceof Object) // true | |
console.log(obj instanceof Foo) // true | |
console.log(Foo instanceof Object) // true | |
console.log(Foo instanceof Function) // true |
# 异步编程
# event-loop
事件循环模型示意图
# Promise
# Task && Microtask
Microtask
MDN 中 Task(宏任务)的描述:
A task is any JavaScript code which is scheduled to be run by the standard mechanisms such as initially starting to run a program, an event callback being run, or an interval or timeout being fired. These all get scheduled on the task queue.
MDN 中 Microtask(微任务)的描述:
At first the difference between microtasks and tasks seems minor. And they are similar; both are made up of JavaScript code which gets placed on a queue and run at an appropriate time. However, whereas the event loop runs only the tasks present on the queue when the iteration began, one after another, it handles the microtask queue very differently.
两者不同之处的 MDN 描述:
There are two key differences.
First, each time a task exits, the event loop checks to see if the task is returning control to other JavaScript code. If not, it runs all of the microtasks in the microtask queue. The microtask queue is, then, processed multiple times per iteration of the event loop, including after handling events and other callbacks.
Second, if a microtask adds more microtasks to the queue by calling
queueMicrotask()
, those newly-added microtasks execute before the next task is run. That's because the event loop will keep calling microtasks until there are none left in the queue, even if more keep getting added.