# 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)

# 类型转换

# == 和 ===

对于 == 来说,如果对比双方的类型不一样,就会进行类型转换。假如对比 xy 是否相同,就会进行如下判断流程:

  1. 首先会判断两者类型是否 ** 相同,** 相同的话就比较两者的大小;
  2. 类型不相同的话,就会进行类型转换;
  3. 会先判断是否在对比 nullundefined ,是的话就会返回 true
  4. 判断两者类型是否为 stringnumber ,是的话就会将字符串转换为 number
1 == '1'
1 ==  1
  1. 判断其中一方是否为 boolean ,是的话就会把 boolean 转为 number 再进行判断
'1' == true
        ↓
'1' ==  1
        ↓
 1  ==  1
  1. 判断其中一方是否为 object 且另一方为 stringnumber 或者 symbol ,是的话就会把 object 转为原始类型再进行判断
'1' == { name: 'js' }
        ↓
'1' == '[object Object]'

其流程图如下:

image

# 其他类型到数值类型的转换规则

  • 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)

![image-20220708145618892](/Users/admin/Library/Application Support/typora-user-images/image-20220708145618892.png)

# 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

# 匿名函数

  1. 匿名函数只有一个形参时,可以省略(),且在函数体只有一句时可以省略 {} 并将该语句结果返回

    let f = n => n+1
    console.log(f(2)) // 3
  2. 匿名函数没有自己的 this ,因此也不能使用 callapplybind 等方法来改变 this 指向

  3. 由于箭头函数时没有自己的 this 的,且 this 指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用

    let Student = (a, b) => a + b
    let s1 = new Student() // TypeError
  4. 箭头函数没有自己的 arguments 对象。在箭头函数中访问 arguments 实际上获得的是它外层函数的 arguments 值

  5. 箭头函数没有 prototype

  6. 箭头函数不能用作 Generator 函数,不能使用 yeild 关键字

# 作用域 && 作用域链

在 ES6 以前 javascript 并没有像其他语言一样拥有 块级作用域 的概念,仅有函数作用域和全局作用域这两种作用域。例如:

{
  var a = 20
}
console.log(a) // 20

而在 ES6 新增了 letconst 关键字后,由这两种关键字声明的变量其作用域会绑定在其最近的 {} 内。例如:

{
  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.

更新于 阅读次数