Published on

JavaScript基础

Authors
  • avatar
    Name
    游戏人生
    Twitter

数据类型

基本数据类型

  • string 字符串类型
  • number 数字类型,包括整数和浮点数
  • boolean 布尔值,true 或 flase
  • symbol 表示独一无二的值,防止变量名覆盖(es6新增)
  • undefined 表示未定义的数据类型
  • null 表示空对象指针的数据类型

引用数据类型

  • Object 对象类型
  • Array 数组类型
  • Function 函数

Function、Array、Date 等其实都是 Object 类型

基本类型与引用类型的区别

基本类型与引用类型是根据存储方式的不同而区分的。基本数据类型直接存放在中,引用数据类型是存放于内存中,并将其地址信息存储在栈中。

示例

var a = 100;
var b = a;
a = 101;
console.log(b)  // 输出 100,a 与 b 互不影响

var c = { x: 100 };
var d = c;
c.x = 120;
console.log(d.x); // 输出 120,d变量中实际是存储的c的地址信息,c 与 d同时指向一个对象

判断类型的方法

  • typeof 判断变量类型,返回值为字符串形式number,boolean,string,function,object,undefined
  • instanceof 用于判断一个实例是否是某个对象的实例

用法

typeof A        // 判断数据类型
A instanceof B    // 判断 A 的原型链上是否有 B 的原型

instanceof 只能判断引用类型数据的具体类型可以用来精确判断数据的类型

示例

console.log(typeof 123);  // 'number'

const a = {x: 1, y: 2};
console.log(a instanceof Object);   // true

const c = new String('hello');
console.log(typeof c);  // 'object'
console.log(c instanceof String);   // true  精确判断String

隐式类型转换

在js中,当运算符在运算时,如果两边数据类型不统一,CPU 就无法计算,这时编译器会自动将运算符两边的数据做一个数据类型转换,转成一样的数据类型再进行计算。

js 隐式类型转换有数值类型转换、字符串类型转换、布尔类型转换、对象类型转换等。 隐式转换规则

  • 转成 string 类型 +(字符串连接符)
  • 转成 number 类型 ++ –(自增自减运算符)+ – * / % **(算术运算符)> < >= <= == !=(关系运算符)
  • 转成 boolean 类型 ! !!(逻辑非运算符)

注意 + ,它既是连接符,也是运算符

  • 当 + 两边都有值,且至少一个值是字符串类型,就会出现字符串拼接
  • 当只有 + 后面有值,例如: +"123"等同于Number("123"),会将字符串转换为数字123

示例

console.log("" + null);          // "null"
console.log("" + undefined);     // "undefined"
console.log("" + 123);           // "123"
console.log("1" + "23");         // "123"
console.log("" + [1]);           // "1"
console.log("" + {});            // "[object Object]"

转换的过程

  • 先调用对象的 Symbol.toPrimitive 这个方法,如果不存在这个方法(结果是undefined),目前已知的只有new Date()有这个方法,
  • 再调用对象的 valueOf 获取原始值,如果获取的不是原始值,
  • 再调用对象的 toString 把其变成字符串
  • 最后再把字符串基于 Number 转换为数字

示例

console.log([] + 1);      
// `[].valueOf()`没有原始值,再调用`[].toString()`得到`""`,字符串遇到`+`运算符可以进行拼接,就不需要转成数字 => "1"

console.log([2] - true);  
// `[].valueOf()`没有原始值,再调用`[].toString()`得到`"2"`,再调用`Number("2")`得到数字2。true直接调用`Number(true)`得到1。最后2 - 1 => 1

console.log({} + 1);      
// `{}.valueOf()`没有原始值,再调用`{}.toString()`得到"[object Object]",遇到`+` 号进行字符串拼接 => "[object Object]1"

console.log({} - 1);      
// `{}.valueOf()`没有原始值,再调用`{}.toString()`得到"[object Object]",再调用`Number("[object Object]")`得到NaN,最后NaN - 1 => NaN

结论

  • 对象 == 字符串 | 数字,是把对象转换为字符串或数字,先后进行比较
  • null == undefined,两个等号的情况下是成立的,除此之外,null 和 undefined 和除本身以外的任何值都不相等
  • 对象 == 对象,比较的是堆内存地址,只有地址一样,结果才为 true
  • NaN !== NaN,NaN和任何值都不相等,包括和他自己
  • 除此之外,如果两边的数据类型不一样,全部统一转换为数字类型,然后进行比较

判断数组的方法

  • Array.isArray()
  • arr instanseOf Array
  • arr.constructor === Array
  • Array.prototype.isPrototypeOf(arr) B.isPrototypeOf(b) 用于判断B是不是在b对象的原型链上
  • Object.prototype.toString.call(arr) === “[object Array]”
  • Object.getPrototypeOf(arr) === Array.prototype getPrototypeOf 获取当前对象的__proto__

变量作用域、闭包

JS是静态作用域,即函数中变量作用域在程序运行前便已经确定。

作用域分类

  • 全局作用域 作用于整个JavaScript代码块
  • 局部作用域(函数作用域) 作用于函数内的代码环境,该函数外部不能被访问
  • 块作用域 (es6新增) 块作用域由 包括

注意

  • 如果在函数内定义了和全局变量相同名称的局部变量,那么在函数内部使用就近原则:即在函数内部局部变量起作用
  • 应尽量避免使用全局变量,以免团队开发变量发生冲突

块级作用域(es6新增)

  • 块级作用域内声明的变量只能在块内生效,即 let 和 const 的声明
  • 块级作用域内不允许重复声明变量,但也会存在声明提前,形成暂时性死区

全局声明的 var 会影响全局变量,但是let 和 const不会

作用域链 一个块内调用的变量如果不存在,便会往上一个块中去找,从而形成作用域链

变量提升

变量提升是 JavaScript 引擎在代码执行前将变量的声明部分提升到作用域的顶部的行为。这意味着可以在变量声明之前使用变量,尽管它们尚未被赋值。

变量提升的原理

JavaScript 引擎在代码执行前的编译阶段会对变量和函数进行解析,将它们的声明提升到作用域的顶部。这意味着变量的声明会被提升,但初始化(赋值)不会提升。因此,在使用变量之前,它的声明必须存在于当前作用域中。

提升范围当前作用域下

提升优先级

  • 变量优先(函数需要变量,所以变量优先)
  • 变量、函数同时提升时,变量的优先级更高

变量提升的影响

  • 变量声明提升 可以在变量声明之前使用变量,避免了在使用变量前必须先声明的限制
  • 值的初始化 尽管变量声明被提升,但变量的赋值操作仍然保留在原来的位置。如果在初始化之前使用变量,其值将为 undefined

注意事项

  • 始终在作用域的顶部声明变量,以避免混淆和意外的行为
  • 尽量避免依赖变量提升,提倡在使用变量之前先进行声明
  • 使用严格模式("use strict")可以禁止变量提升,强制遵循更严格的变量声明和使用规范

this上下文

this 是在执行时动态读取上下文所决定的

this 指向总结

调用方式示例函数中 this 指向
new 调用new method()新对象
直接调用method()全局对象 window
通过对象调用obj.method()前面的对象obj
callmethod.call(ctx)call 的第一个参数
applymethod.apply(ctx)apply 的第一个参数

call apply bind 区别

  • call | apply 传参不同, 参数依次传入 | 作为数组传入
  • bind 返回值不同

手写apply

Function.prototype.myApply = function(context){
  context = context || window; // 参数兜底
  if(typeof context !== 'object'){
    if(typeof context == 'number') context = new Number(context);
    if(typeof context == 'boolean') context = new Boolean(context);
    if(typeof context == 'string') context = new String(context);
  }

  let fn = Symbol();
  context.fn = this;
  // arguments 就是传进去的参数集合,通过 arguments[1] 得到传进来的数组参数
  let result;
  result = arguments[1] ? context.fn(...args) : context.fn();

  delete context.fn;
  return result;
}

手写 bind

Function.prototype.myBind = function() {
  // 1.1 bind 原理
  const _this = this;
  const args = Array.prototype.slice.call(arguments); // 类数组
  const newThis = args.shift();

  // 1.2 返回值不执行 => 返回函数
  return function() {
    // 执行核心
    return _this.myApply(newThis, args);
  }
}

闭包

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量。

特性

  • 函数嵌套函数
  • 函数内部可以引用函数外部的参数和变量
  • 参数和变量不会被垃圾回收机制回收

闭包的主要形式

  • 函数作为返回值

示例

function fn() {
  var a = 100;
  return function() {
    var b = 0;
    console.log(++a);
    console.log(++b);
  }
}

var fn1 = fn();
fn1(); // 101  1
fn1(); // 102  1
  • 闭包作为参数传递

示例

var a = 1;
var fn = function (x) {
  if(x > a) {
    console.log(x);
  }
};

(function (fn1) {
  var a = 100;
  fn1(10);
})(fn);// 将 fn 当做参数传入

闭包优缺点

优点

  • 保护函数内的变量安全 ,实现封装,防止变量流入其他环境发生命名冲突
  • 在内存中维持一个变量,可以做缓存(但使用多了同时也是一项缺点,消耗内存)
  • 匿名自执行函数可以减少内存消耗

缺点

  • 被引用的私有变量不能被销毁,增大了内存消耗,造成内存泄漏,解决方法是可以在使用完变量后手动为它赋值为null;
  • 由于闭包涉及跨域访问,所以会导致性能损失,可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响

原型、原型链

原型

每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。

使用原型对象的好处是,在它上面定义的属性和方法都可以被对象实例共享。原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型。

原型是一个对象,为其他对象提供共享属性的对象,又称原型对象

不同对象原型存放方式也不同

  • 函数(function)

    函数是一种特殊的对象,函数的原型存放在prototype属性上

  • 对象(Object)

    普通对象的原型是存放到内置属性**[[Prototype]]上,可以通过对象的proto**来访问对象的原型

  • 数组(Array)

    数组也是一种特殊的对象,但与函数不同的是它的原型和普通对象一样,也是存放到内置属性[[Prototype]]上,可以通过数组的__proto__来访问数组的原型

构造函数,原型对象和函数实例三者间的关系

示例

// 创建一个构造函数
const Person = function(){};
// 创建一个函数实例
let user = new Person();

console.log(Person.prototype === user.__proto__);  // true
console.log(Person.prototype.constructor === Person);  // true

总结

  • 函数对象的 prototype 指向的对象便是原型对象,该原型对象的 constructor 属性又指向该函数
  • 只有函数对象和Object才有原型,实例化后的对象都是没有原型对象的
  • 所有的对象实例都有一个私有属性 proto
  • 由函数对象生成的实例的 proto 属性指向函数对象的原型对象
  • 由Object对象生成的实例的 proto 属性指向父对象
  • 最终所有的对象 proto 属性都指向 Object.prototype
  • Object.prototype.proto = null

constructor作用

  • 判断类型
// 简写方式,最初写法 let arr = new Array(10,20,30);
let arr = [10, 20, 30];
console.log(arr.constructor === Array); // true

原型链

原型链是通过对象特有的原型构成的一种链式结构,主要用来继承多个引用类型的属性和方法。默认情况下,所有引用类型都继承自Object。

完整的原型链

new 关键字

new 关键字做了什么

  • 首先创建了一个空对象
  • 这个对象的原型 proto,指向了这个 函数对象 的prototype
  • 该对象实现了 函数对象 的方法(执行了函数对象的构造函数);
  • 根据一些特定情况返回对象
    • 如果这个构造函数没有返回值,或者返回一个非对象类型,则new 最后返回创建的这个对象(this 指向这个新的对象);
    • 如果这个构造函数明确返回了一个对象,则返回这个对象(this指向第一步创建的空对象);

new 的实现

function newFunc(Fun, ...rest) {
  if(typeof Fun !== "function") {
    throw new Error('new operator function the frist param must be a function');
  }

  var obj = Object.create(Fun.prototype);
  var result = Fun.apply(obj, rest);
  return result && typeof result === 'object' ? result : obj;
}

面相对象

创建对象的方法

  • let obj = 直接使用
  • let p = new Person() 使用构造函数
  • let obj = new Object() 使用new Object
  • let obj = Object.create() 使用Object.create

与 Object.create() 对比

实现 Object.create

function inherit(p) {
  if(p === null) throw TypeError();
  if(Object.create) {
    return Object.create(p)
  };

  if( typeof p !== "object" && typeof p !== "function") throw TypeError();

  function f() {};
  f.prototype = p;
  return new f();
}

枚举对象属性的方式

  • for…in

    用来遍历对象及其原型链上的所有可枚举属性

  • Object.keys

    用来遍历对象上所有可枚举的属性,但不包含原型链上的属性

  • Object.getOwnProperityNames

    返回对象自身包含的所有属性(包含不可枚举的)名称数组,但不包含原型链上的

继承

继承,是描述类和类之间的关系

继承的分类

  • 原型继承

重写子对象的原型对象,将其指向父对象实例,缺点是子对象实例修改原型链方法时会影响所有子对象

function Parent(name) {
  this.name = name;
  this.c = "parent"
}

Parent.prototype.getName = function() {
  console.log(this.name);
};

function Child() {};

Child.prototype = new Parent();
Child.prototype.constructor = Child;
  • 构造函数继承

在子对象的构造函数中调用父对象的构造函数,并将this传入父对象,缺点是无法继承父对象原型上的属性和方法

function Parent(name) {
  this.name = name;
}

Parent.prototype.getName = function() {
  console.log(this.name);
};

function Child(name) {
  Parent.call(this, name);
}
  • 组合继承

使用原型链去继承父对象原型上的属性方法,构造函数继承父对象自己的属性和方法,本质上是在原型链继承的基础上将父对象属性方法复制了一份到子对象实例中

function Parent(name) {
  this.name = name;
}

Parent.prototype.getName = function() {
  console.log(this.name);
};

function Child(name) {
  Parent.call(this, name);
}

// 问题:我只想构建一个 原型链的关系。
Child.prototype = new Parent();

Child.prototype.constructor = Child;
  • 组合寄生式继承
function Parent(name) {
  this.name = name;
}

Parent.prototype.getName = function() {
  console.log(this.name);
};

function Child(name) {
  Parent.call(this, name);
}

// 问题:我只想构建一个 原型链的关系。
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
  • extends 继承

es6 新增的 Class 继承方式

组合寄生和 class 的区别

Loose 模式应该差不多,主要是这两个区别 (loose 模式,babel 进行降级编译的时候,考虑到体积等,只实现核心功能)

  • Class 继承,会继承静态属性
  • 子类中,必须在 constructor中调用 super, 因为子类的 this 对象,必须先通过父类的构造函数完成

事件循环

浏览器 JS 异步执行的原理

JS 是单线程的,浏览器是多线程的。当 JS 需要执行异步任务时,浏览器会另外启动一个线程去执行该任务。也就是说,“JS 是单线程的”指的是执行 JS 代码的线程只有一个,是浏览器提供的 JS 引擎线程主线程)。浏览器中还有定时器线程和 HTTP 请求线程等,这些线程主要不是来跑 JS 代码的。

浏览器不仅有多个线程,还有多个进程,如渲染进程、GPU 进程和插件进程等。

渲染进程

渲染进程下包含了 JS 引擎线程HTTP 请求线程定时器线程事件触发线程GUI线程等,这些线程为 JS 在浏览器中完成异步任务提供了基础。

事件驱动

浏览器异步任务的执行原理背后其实是一套事件驱动的机制。事件触发、任务选择和任务执行都是由事件驱动机制来完成的。事件循环其实就是在事件驱动模式中来管理和执行事件的一套流程

浏览器中的事件循环

执行栈与任务队列

JS 解析代码时,会将同步代码按顺序排在某个地方,即执行栈,然后依次执行里面的函数。当遇到异步任务时交给其他线程处理,待当前执行栈所有同步代码执行完成后,会从任务队列中取出已完成异步任务的回调加入执行栈继续执行,遇到异步任务时又交给其他线程,…..,如此循环往复。而其他异步任务完成后,将回调放入任务队列中待执行栈来取出执行

在事件驱动的模式下,至少包含了一个执行循环来检测任务队列是否有新的任务。通过不断循环去取出异步回调来执行,这个过程就是事件循环而每一次循环就是一个事件周期或称为一次 tick

宏任务和微任务

任务队列不只一个,根据任务的种类不同,可以分为微任务队列和宏任务队列。

事件循环的过程中,执行栈执行完成后,优先检查微任务队列是否有任务需要执行,如果没有,再去宏任务队列检查是否有任务执行。微任务一般在当前循环就会优先执行,而宏任务会等到下一次循环,因此,微任务一般比宏任务先执行,并且微任务队列只有一个,宏任务队列可能有多个

常见宏任务

  • setTimeout()
  • setInterval()
  • setImmediate()

常见微任务

  • promise.then()、promise.catch()
  • new MutaionObserver()
  • process.nextTick()

在浏览器中 setTimeout 的延时设置为 0 的话,会默认为 4ms,NodeJS 为 1ms。具体值可能不固定,但不是为 0

浏览器的渲染视图是在微任务执行完成之后,宏任务执行之前

代码执行动画过程

  • 宏任务特征:有明确的异步任务需要执行和回调;需要其他异步线程支持
  • 微任务特征:没有明确的异步任务需要执行,只有回调;不需要其他异步线程支持

NodeJS 中的事件循环

异步方法

  • 文件 I/O 异步加载本地文件
  • setImmediate() 与 setTimeout 设置 0ms 类似,在某些同步任务完成后立马执行
  • process.nextTick() 在某些同步任务完成后立马执行
  • server.close、socket.on(‘close’,…)等 关闭回调

事件循环模型

NodeJS 的事件循环主要是在 Libuv 中完成的

事件循环各阶段

  • timers 阶段

    执行所有 setTimeout() 和 setInterval() 的回调

  • pending callbacks 阶段

    某些系统操作的回调,如 TCP 链接错误。除了 timers、close、setImmediate 的其他大部分回调在此阶段执行

  • poll 阶段

    轮询等待新的链接和请求等事件,执行 I/O 回调等。V8 引擎将 JS 代码解析并传入 Libuv 引擎后首先进入此阶段。如果此阶段任务队列已经执行完了,则进入 check 阶段执行 setImmediate 回调(如果有 setImmediate),或等待新的任务进来(如果没有 setImmediate)。在等待新的任务时,如果有 timers 计时到期,则会直接进入 timers 阶段。此阶段可能会阻塞等待

  • check 阶段

    setImmediate 回调函数执行

  • close callbacks 阶段

    关闭回调执行,如 socket.on(‘close’, …)

上面每个阶段都会去执行完当前阶段的任务队列,然后继续执行当前阶段的微任务队列,只有当前阶段所有微任务都执行完了,才会进入下个阶段。

垃圾回收GC

堆和栈

数据的存储方式

  • 栈内存 线性有序存储,容量小,系统分配效率高。(存放原始类型)
  • 堆内存 首先要在堆内存新分配存储区域,之后又要把指针存储到栈内存中,效率相对低一些(存放引用类型的值)

垃圾回收分类

因为数据是存储在栈和堆两种内存空间中的,所以浏览器的垃圾回收机制根据数据的存储方式分为 “栈垃圾回收” 和 “堆垃圾回收”。

栈垃圾回收

当一个函数执行结束之后,JS引擎通过向下移动ESP指针(记录调用栈当前执行状态的指针),来销毁该函数保存在栈中的执行上下文(变量环境、词法环境、this、outer),遵循先进后出的原则。

堆垃圾回收

当函数执行结束,栈空间处理完成了,但是堆空间的数据虽然没有被引用,但还是存储在堆空间中,需要垃圾回收器将堆空间中的垃圾数据回收。

垃圾回收的方法

  • 标记清除法
  • 引用计数法

标记清除法

  • 垃圾收集器在运行时会给内存中的所有变量都加上一个标记
  • 然后从各个根对象开始遍历,把还在被上下文变量引用的变量标记去掉标记
  • 清理所有带有标记的变量,销毁并回收它们所占用的内存空间
  • 最后垃圾回收程序做一次内存清理

标记整理

标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存。

引用计数法

跟踪记录每个值被引用的次数,每次引用加一,被释放时减一,当这个值的引用次数变成0时,就可以将其内存空间回收。但是当出现循环引用的时候,会导致内存泄露

注意

使用 let 和 const 可以将变量限制在较小的块级作用域中,有利于垃圾回收。

检查内存泄露的方式

  • 查看任务管理器
  • 使用开发者工具中的 Performance 模块