js相关知识点

81 阅读14分钟

js

数据类型

基本数据类型
Number,String,Boolean,null,undefined,symbol,bigint(后两个为ES6新增)

  1. Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。比如说,如果代码太多,不知道是不是声明过一个变量的话,那么可以使用symbol来命名
  2. BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

引用数据类型
object,Array,Function

两种数据类型存储方式的区别:

  • 基本数据类型是直接存储在栈中,占据空间小、大小固定。
  • 引用数据类型是存储在堆内存中,占据空间大、大小不固定。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址,当解释器寻找引用值时,会检索其在栈中的地址,取得地址后从堆中获得实体。 image.png
判断数据类型的三种方式
  1. typeof用于判断是基本数据类型还是引用数据类型

  2. instanceof用于判断对象的类型
    [] instanceof Array

  3. constructor()判断数据的类型,但是如果对象的原型被改变了,constructor就不能用来判断数据类型了

console.log((2).constructor === Number); // true
//实际上是用(2).__proto__.constructor === Number判断的(访问原型上的constructor属性)
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

typeof num为object,但是null instanceof Object为false,因为null虽然为空对象,但是不具有对象的属性和方法,比如不能执行null.toString()

NAN

表示不是一个数字,以下几种结果为NAN
1、无法解析的数字类型

Number('abc') // NaN
Number(undefined) // NaN

2、Math计算失败的结果

Math.sqrt(-2) // 负数的平方根
Math.log(-1) // 负数的自然对数(底数为e)
Math.acos(2) // 超过1的反余弦值

3、参与运算的任意一个成员为NaN

NaN + 1 // NaN
10 / NaN // NaN
null和undefined

相似性:
1、undefined和null都表示“无”,都是js中的基本数据类型
2、undefined和null如果用在if语句中,都会转成false
区别:
1、null 指空对象,用于赋值给一些可能会返回对象的变量,作为初始化
2、undefined 指声明未赋值的变量或者是不存在的对象属性值

强制转换和隐式转换
强制转换:
  • 转换成字符串 toString() 、String()
  • 转换成数字 Number()、 parseInt()、 parseFloat()
  • 转换成布尔类型 Boolean()
隐式转换:

1、ToString()
null:转为"null"
undefined:转为"undefined"
布尔类型:true和false分别被转为"true"和"false"
数字类型:转为数字的字符串形式,如10转为"10", 1e21转为"1e+21"
数组:转为字符串是将所有元素按照","连接起来,相当于调用数组的Array.prototype.join()方法,如[1, 2, 3]转为"1,2,3",空数组[]转为空字符串,数组中的null或undefined,会被当做空字符串处理
普通对象:转为字符串相当于直接使用Object.prototype.toString(),返回"[object Object]"
2、ToNumber()
null: 转为0
undefined:转为NaN
字符串:如果是纯数字形式,则转为对应的数字,空字符转为0, 否则一律按转换失败处理,转为NaN
布尔型:true和false被转为1和0
数组:调用valueOf()方法,并按照上述规则转换返回的值。如果转换结果是NAN,则调用toString()方法,再按照转换字符串的规则转换
Number([]) // 0
对象:同数组的处理
Number({}) // NaN
3、ToBoolean()
转换为false的值为null , undefined , "" , 0 , NAN,其他全部为true(包括[] , {} , function() {})

判断为true或者为false

1、if([]==false) 返回true 
[]转为数字为0,false转为数字为0
2、if({} == false) 返回false
{}转为数字为NAN,false转为数字为0,而NAN和谁都不相等,包括它自己
3、if([] == [])  为false  、if({} == {}) 也为false
在双等号左右两边的类型相等时,采用三等号进行判定
每次使用 [] 都是新建一个数组对象。当数组比较的时候其实比较的是他们的引用。[] == []的时候,从值上尽管两边都是[]但是从引用上两边是不相等的。
4、if(null == undefined )为true
实际上undefined值是派生自null值的,因此ECMA-262规定对他们的相等性测试要返回true

变量提升和函数提升

简单说就是在 JavaScript 代码执行前引擎会先进行预编译,预编译期间会将变量声明与函 数声明提 升至其对应作用域的最顶端,函数内声明的变量只会提升至该函数作用域最顶层
需要注意的问题:

在变量声明提升时,使用var关键字定义的变量才存在变量提升
在函数声明提升时,

  • 函数声明提升的特点是,在函数声明的前面,可以调用这个函数
  • 函数提升只会提升函数声明式写法,函数表达式的写法不存在函数提升
function A() {}
var A = function(){}
  • 函数提升的优先级大于变量提升的优先级,即函数提升在变量提升之上 变量提升的优点和问题:
    优点:容错性更好
    a = 1;var a;console.log(a);
    这行代码不会报错
var tmp = new Date();

function fn() {
    console.log(tmp);
    if (false) {
        var tmp = 'hello world'; //会进行变量提升
    }
}

fn();  // undefined

在这个函数中,原本是要打印出外层的tmp变量,但是因为变量提升的问题,内层定义的tmp被提到函数内部的最顶部,相当于覆盖了外层的tmp,所以打印结果为undefined。

var定义变量的问题:

var tmp = 'hello world';

for (var i = 0; i < tmp.length; i++) {
    console.log(tmp[i]);
}

console.log(i); // 11

由于遍历时定义的i会变量提升成为一个全局变量,在函数结束之后不会被销毁,所以打印出来11

this指向
一、全局模式下
function func() {
  console.log(this);
}
function func() {
  // 如果是严格模式,this的值为undefined
  'use strict'
  console.log(this)
}
// 普通函数调用:this指向window
func()

二、上下文对象下
// 2、对象方法的this指向:方法的调用者
var stu = {
  sing: function () {
    // this为stu对象
    console.log(this);
  }
}
// 对象形式调用:this指向对象
stu.sing()

// 3、构造函数的this指向:实例对象
function Star(uname) {
  this.uname = uname
  console.log("第三个函数");
}

四、绑定事件调用
// 绑定事件的this指向:函数的调用者,也就是btn按钮
this.btn.onclick = function () {
  console.log(this);
}

// 5、定时器的this指向:window
setInterval(function () { }, 1000);

// 6、立即执行函数的this指向:window
(function () {
  console.log("第六个函数");
})()
7、回调函数中的this指向:函数的调用者
所以回调函数一般采用匿名函数,用于继承上一层的this

面试题1

var o1 = {
  text: 'o1',
  fn() {
    return this.text
  }
}
var o2 = {
  text: 'o2',
  fn() {
    return o1.text
  }
}
var o3 = {
  text: 'o3',
  fn() {
    var fn2 = o1.fn;
    return fn2()
  }
}
console.log(o1.fn()) // o1
console.log(o2.fn()) // o1
console.log(o3.fn()) // undefined 因为fn2是被重新赋值的,所以fn2指向window

面试题2

<div id='div1'>我是一个div</div>
window.id = 'window'
document.getElementById('div1').onclick = function() {
  console.log(this.id)  // div1,此时的this指向div1元素
  const callback = function() {
    console.log(this.id) // window,因为callback()是普通函数调用,所以this指向window
  }
  callback()
}

解决:使用临时变量保存this
window.id = 'window'
document.getElementById('div1').onclick = function() {
  console.log(this.id)  // div1
  const that = this
  const callback = function() {
    console.log(that.id) // window
  }
  callback()
}

面试题3:

var name = 'lisi'
var obj = {
  name: 'zhangsan',
  arr: [1, 2, 3],
  print() {
    this.arr.forEach(function(n) {
      console.log(this.name) // 'lisi', 因为在forEach()中的回调函数是普通函数调用方式,所以其中的this指向window
    })
  }
}
改变this指向

image.png

  1. call()
    应用:调用原生的方法
  • 接收一个参数:传递的参数即为this的指向
var n = 123
var obj = { n: 456 }
function a() {
  console.log(this.n)
}

a.call() // 123
a.call(undefined) // 123
a.call(null) // 123
a.call(window) // 123
a.call(obj) // 456
  • 接收多个参数:第一个参数是this的指向,之后的是函数回调所需的参数
function add(a,b) {
  return a + b
}
console.log(add.call(null, 1, 2))

2. apply()
和call()基本一模一样,区别在于apply接收的参数是一个数组

应用1:调用原生的方法

var arr = [1, 2, 3, 4, 5]

// Math.max()使用
Math.max(3, 5, 7, 5)

// 使用apply()方法
// apply()方法的第二个参数就是接收一个数组,把它变为调用者的所有参数
console.log(Math.max(null, arr))

应用2: 配合数组对象的slice方法,可以将一个类似数组的对象(比如arguments对象)转为真正的数组

Array.prototype.slice.apply({0:1, 1:2, 2:3, length: 3}) // 结果:[1, 2, 3]
// 给apply传入一个对象后,this指向这个对象,至于传入length:3为什么可以转为数组,需要看slice()方法怎么实现的

3. bind()用于将函数体内的this绑定到某个对象,然后返回一个新函数
举例:

var d = new Date()

var fn = d.getTime
fn() // 报错,因为这样调用,使得this指向了全局对象

var fn = d.getTime.bind(d)
fn() // 将getTime()的this指向了d

针对面试题3优化

var name = 'lisi'
var obj = {
  name: 'zhangsan',
  arr: [1, 2, 3],
  print() {
    this.arr.forEach(function(n) {
      console.log(this.name)
    }.bind(this))
  }
}
垃圾回收机制
什么是内存泄漏

不再用到的内存,如果没有及时回收,就会造成内存泄漏

JS中的垃圾回收

浏览器的JS具有自动垃圾回收机制,原理是垃圾收集器会定期找出那些不再继续使用的变量,然后释放其内存,但是这个过程不是实时的,因为其开销比较大并且垃圾回收时会停止响应其它操作,所以垃圾回收器会按照固定的时间间隔周期性的进行
1)标记清除
标记清除是浏览器常见的垃圾回收方式。
工作原理:当变量进入执行环境时,就标记这个变量“进入环境”,被标记为“进入环境”的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为“离开环境”。

function test() {
    var a = 10 // 被标记,进入环境
    var b = 20 // 被标记,进入环境
}
test(); // 执行完毕,之后a, b又被标记为“离开环境”,被回收

工作流程:
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。
然后,它会去掉环境中的变量以及被环境中的变量引用的标记。
而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。
最后,垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
2)引用计数
另外一种垃圾回收机制就是引用计数,这个用的相对较少。
工作原理:跟踪记录每个值被引用的次数
工作流程:
当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。
如果同一个值有被赋给另一个变量,则该值的引用次数加1;相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。
当这个引用次数变为0时,说明这个变量已经没有价值。
因此,在在机回收期下次再运行时,这个变量所占有的内存空间就会被释放出来

function test() {
    var a = {} // a 指向对象的引用次数为1
    var b = a // a 指向对象的引用次数加1, 为2
    var c = a // a 指向对象的引用次数加1, 为3
    var b = {} // a 指向对象的引用次数减1, 为2

这种方法会引起循环引用的问题:例如: obj1obj2通过属性进行相互引用,两个对象的引用次数都是2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,obj1obj2还将会继续存在,因此它们的引用次数永远不会是0,就会引起循环引用。

function fn() {
    var a = {}
    var b = {}
    a.pro = b
    b.pro = a
}
fn()

工程化

CommonJS

关键词

  • 社区标准
  • 使用函数实现(require)
  • 仅node环境支持
  • 动态依赖(需要代码运行后才能确定依赖)
  • 动态依赖是同步执行的

require函数的伪代码

// require函数的伪代码

function require(path) {
  if (该模块有缓存) {
    return 缓存结果
  }
  function _run(exports, require, module, __filename, __dirname) {
    // 模块代码会放到这里
  }

  var module = {
    exports: {}
  }

  _run.call(
    module.exports, // this
    module.exports, // exports
    require,
    module,
    模块路径,
    模块所在目录
  );

  // 把module.exports加入到缓存
  return module.exports
}

注意点:

  1. 模块代码放在_run()中解释了为什么变量等不会被污染,因为放在了函数中
  2. this、exports和module.exports指向同一个对象

面试题:

问题:下面的模块导出了什么结果
exports.a = 'a' 
module.exports.b = 'b'
this.c = 'c' // 此时this,exports和module.exports的值都为{a:'a', b:'b', c:'c'}
// 但因为这里重新赋值了,所以模块导出结果为{d:'d'}
module.exports = {
  d: 'd'
}
ES Module

关键词:

  • 官方标准
  • 使用新语法实现(import和export)
  • 所有环境均支持(cmj是node环境,esm是node和浏览器环境)
  • 同时支持静态依赖和动态依赖 静态依赖:在代码运行前就要确定依赖关系
  • 动态依赖是异步的
  • 符号绑定

符号绑定:

// module a.js
export var a = 1;
export function changeA() {
    a = 2;
}

// index.js
// 导入位置的符号和导出的符号并非赋值,也就是说a.js中的变量a和index.js中的变量a共享一块内存地址
import {a, changeA} from './a.js'
console.log(a); // 1
changeA();
console.log(a); // 2

面试题:

  1. commonjs和es6模块的区别是什么

CMJ是社区标准,ESM是官方标准

CMJ是使用API实现的模块化,ESM是使用新语法实现的模块化

CMJ仅在node环境中支持,ESM各种环境均支持

CMJ是动态的依赖,同步执行,ESM支持动态和静态,动态依赖是异步执行的

ESM导入时有符号绑定,CMJ只是普通函数调用和赋值

2、export和export default的区别是什么

export为普通导出,导出的数据必须带有命名,一个模块中可以有多个具名导出

export default 为默认导出,无需命名,一个模块中只能有一个默认导出

3、符号绑定,输出结果

// counter.js
var count = 1
export { count }
export function increase() {
    count++
}

//main.js
import {  count, increase } from './counter'
import * as counter from './counter'
const { count: c } = counter  // 解构相当于 为变量c重新分配内存空间
increase()
console.log(count)  // 2
console.log(counter.count)  // 2
console.log(c)  // 1
npx

目的:如果不想全局安装一个包,则可以将包安装在项目中,使用npx命令局部运行包

运行本地命令

目的:使用项目中的局部webpack打包代码
那么使用npx命令时,它会首先从本地工程的node_modules/.bin目录中寻找是否有对应的命令
例如: npx webpack
上面这条命令寻找本地工程的node_modules/.bin/webpack
如果将命令配置到package.jsonscripts,可以省略npx

临时下载执行

当执行某个命令时,如果无法从本地工程中找到对应命令,则会把命令对应的包下载到一个临时目录,下载完成后执行,临时目录中的命令会在适当的时候删除
例如,npx prettyjson 1.json
npx会下载prettyjson包到临时目录,然后运行该命令
如果命令名称和需要下载的包名不一致时,可以手动指定包名
例如:@vue/cli是包名,vue是命令名,两者不一致,可以使用下面的命令
npx -p @vue/cli vue create vue-app

npm init

npm init通常用于初始化工程的package.json文件
除此之外,有时也可以充当npx的作用

npm init 包名  // 等效于npx create-包名
npm init @命名空间  // 等效于npx @命名空间/create
npm init @命名空间/包名  // 等效于npx @命名空间/create-包名