js
数据类型
基本数据类型
Number,String,Boolean,null,undefined,symbol,bigint(后两个为ES6新增)
- Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。比如说,如果代码太多,不知道是不是声明过一个变量的话,那么可以使用symbol来命名
- BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
引用数据类型
object,Array,Function
两种数据类型存储方式的区别:
- 基本数据类型是直接存储在栈中,占据空间小、大小固定。
- 引用数据类型是存储在堆内存中,占据空间大、大小不固定。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址,当解释器寻找引用值时,会检索其在栈中的地址,取得地址后从堆中获得实体。
判断数据类型的三种方式
-
typeof用于判断是基本数据类型还是引用数据类型
-
instanceof用于判断对象的类型
[] instanceof Array -
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指向
- 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
这种方法会引起循环引用的问题:例如: obj1和obj2通过属性进行相互引用,两个对象的引用次数都是2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,obj1和obj2还将会继续存在,因此它们的引用次数永远不会是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
}
注意点:
- 模块代码放在_run()中解释了为什么变量等不会被污染,因为放在了函数中
- 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
面试题:
- 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.json的scripts,可以省略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-包名