数据类型
基础类型: String、Number、Boolean、null、undefined、Symbol 存储在栈中,赋值变量和比较均为数据本身。
引用类型:Object、Array、Map、Function 存储在堆中,使用new创建,赋值变量和比较均是内存地址。
检测数据类型
1.typeof 基本数据类型
不能区分null,数组,对象,正则,因为返回的都是”object”- typeof 2 输出 'number'
- typeof NaN 输出 'number'
- typeof null 输出 'object'
- typeof {} 输出 'object'
- typeof [] 输出 'object.
- typeof (function(){}) 输出 'function'
- typeof undefined 输出 'undefined'
- typeof '222' 输出 'string'
- typeof true 输出 'boolean'
复制代码 2.instanceof 引用数据类型;检测一个实例是否属于某个类
- var c= [1,2,3];
- var d = new Date();
- var e = function(){alert(111);};
- var f = function(){this.name="22";};
- console.log(c instanceof Array) //true
- console.log(d instanceof Date) //true
- console.log(e instanceof Function) //true
- // console.log(f instanceof function ) //false
- // 注意左侧必须是对象(object),如果不是,直接返回false,具体见基础类型。
- let num = 1
- num instanceof Number // false
- num = new Number(1)
- num instanceof Number // true
复制代码 3.constructor
根据对象的constructor判断,返回对创建此对象的数组函数的引用。
几乎可以判断基本数据类型和引用数据类型- var c= [1,2,3];
- var d = new Date();
- var e = function(){alert(111);};
- alert(c.constructor === Array) ----------> true
- alert(d.constructor === Date) -----------> true
- alert(e.constructor === Function) -------> true
- //注意: constructor 在类继承时会出错
复制代码 4.Object.prototype.toString.call() 最准确的判断方式
所有数据类型均可判断:Object.prototype.toString.call()
这是对象的一个原生原型扩展函数,用来更精确的区分数据类型(类)。
原理- var gettype = Object.prototype.toString
- gettype.call('aaaa') 输出 [object String]
- gettype.call(2222) 输出 [object Number]
- gettype.call(true) 输出 [object Boolean]
- gettype.call(undefined) 输出 [object Undefined]
- gettype.call(null) 输出 [object Null]
- gettype.call({}) 输出 [object Object]
- gettype.call([]) 输出 [object Array]
- gettype.call(function(){}) 输出 [object Function]
复制代码 let const var
区别
- var声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined
let和const不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报错
- var不存在暂时性死区
let和const存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量
- // var
- console.log(a) // undefined
- var a = 10
- // let
- console.log(b) // Cannot access ‘b’ before initialization
- let b = 10
- // const
- console.log© // Cannot access ‘c’ before initialization
- const c = 10
复制代码
- var不存在块级作用域
let和const存在块级作用域
- // var
- {
- var a = 20
- }
- console.log(a) // 20
- // let
- {
- let b = 20
- }
- console.log(b) // Uncaught ReferenceError: b is not defined
- // const
- {
- const c = 20
- }
- console.log© // Uncaught ReferenceError: c is not defined
复制代码
- var允许重复声明变量
let和const在同一作用域不允许重复声明变量
- // var
- var a = 10
- var a = 20 // 20
- // let
- let b = 10
- let b = 20 // Identifier ‘b’ has already been declared
- // const
- const c = 10
- const c = 20 // Identifier ‘c’ has already been declared
复制代码 例子
- for (var i = 0; i < 5; i++) {
- setTimeout(() => console.log('i======', i))
- }//输出5 5 5 5 5
- for (let j = 0; j < 5; j++) {
- setTimeout(() => console.log('j======', j))
- }//输出0 1 2 3 4
复制代码 setTimeout 的执行机制
- 即使延迟时间为 0 毫秒,setTimeout 的回调函数也会被放入任务队列(Task Queue),而非立即执行。
JavaScript 会先执行完主线程中的所有同步代码(即整个 for 循环),再从任务队列中取出回调函数执行。
- var 的函数作用域
var 声明的变量属于函数作用域(或全局作用域),而非块级作用域。因此,整个循环中只有一个 i 变量。
当循环结束时,i 的值已经变为 5(因为 i 从 0 递增到 4 后,再次执行 i++ 变为 5,此时循环条件 i < 5 不成立,循环终止)。
- 闭包捕获变量引用
每个 setTimeout 的回调函数形成闭包,捕获的是 i 的引用,而非值。
当主线程执行完循环后,任务队列中的回调函数开始执行,但此时 i 的值已固定为 5,因此所有回调都打印 5。
let 的作用域特性:
- let 声明的变量属于块级作用域(即 {} 内的区域)。在 for 循环中,每次迭代都会创建一个新的变量副本,这些副本相互独立。
- 循环中的闭包捕获
每个 setTimeout 的回调函数捕获的是当前迭代的 j 变量副本,而非共享同一个变量。当回调函数在延迟后执行时,它们访问的是捕获时的变量值(即 0 到 4)。
1.var
- 在ES5中,顶层对象的属性和全局变量是等价的,用var声明的变量既是全局变量,也是顶层变量
注意:顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象
- var a = 10;
- console.log(window.a) // 10
复制代码- console.log(a) // undefined
- var a = 20
- 在编译阶段,编译器会将其变成以下执行
- var a
- console.log(a)
- a = 20
复制代码
- 使用var,我们能够对一个变量进行多次声明,后面声明的变量会覆盖前面的变量声明
- 在函数中使用使用var声明变量时候,该变量是局部的
- var a = 20
- function change(){
- var a = 30
- }
- change()
- console.log(a) // 20
复制代码 而如果在函数内不使用var,该变量是全局的- var a = 20
- function change(){
- a = 30
- }
- change()
- console.log(a) // 30
复制代码 2.let
let是ES6新增的命令,用来声明变量
- 用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效
- {
- let a = 20
- }
- console.log(a) // ReferenceError: a is not defined.
复制代码- console.log(a) // 报错ReferenceError
- let a = 2
复制代码 这表示在声明它之前,变量a是不存在的,这时如果用到它,就会抛出一个错误
- 只要块级作用域内存在let命令,这个区域就不再受外部影响
- var a = 123
- if (true) {
- a = ‘abc’ // ReferenceError
- let a;
- }
复制代码 使用let声明变量前,该变量都不可用,也就是大家常说的“暂时性死区”
- let a = 20
- let a = 30
- // Uncaught SyntaxError: Identifier ‘a’ has already been declared
- 注意的是相同作用域,下面这种情况是不会报错的
- let a = 20
- {
- let a = 30
- }
复制代码 3.const
const声明一个只读的常量,一旦声明,常量的值就不能改变- const a = 1
- a = 3
- // TypeError: Assignment to constant variable.
复制代码 这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值- const a;
- // SyntaxError: Missing initializer in const declaration
复制代码 如果之前用var或let声明过变量,再用const声明同样会报错- var a = 20
- let b = 20
- const a = 30
- const b = 30
- // 都会报错
复制代码 const实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动
对于简单类型的数据,值就保存在变量指向的那个内存地址,因此等同于常量
对于复杂类型的数据,变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的,并不能确保改变量的结构不变
- const foo = {};
- // 为 foo 添加一个属性,可以成功
- foo.prop = 123;
- foo.prop // 123
- // 将 foo 指向另一个对象,就会报错
- foo = {}; // TypeError: “foo” is read-only
复制代码 其它情况,const与let一致
事件循环机制
事件循环练习
由于js是单线程,为防止代码阻塞,将代码分为同步和异步,同步代码会直接放入执行栈中执行,异步代码(如setTimeout)放入宿主环境(浏览器,Node)中,时机到了(点击事件即点击后,setTimeout即时间结束后)以后将回调函数放入任务队列中,执行栈中的代码执行完后就会去任务队列中查看有无异步代码要执行。反复循环查看执行,这个过程就是事件循环。
js又把异步任务分为宏任务(由宿主环境发起,如script,事件,网络请求Ajax/Fetch,setTimeout()/setInterval())和微任务(由JS引擎发起,如Promise,Promise本身同步,then/catch回调函数异步)
注意⚠️
- 排前面的 script 先执行,执行其内部的【同】,再执行其【微】,接着就轮到下一个大的宏,也就是执行下一个 script,【同】、【微】......顺序执行完后,再从头开始,看第一个 script 是否有需要执行的【宏】,再去下一个 script 中找 【宏】,等大家宏结束后,进入下一轮循环。
- async函数里面的属于同步代码,await后的代码属于异步微任务
Practice
1️⃣
- console.log('A');·
- setTimeout(() => console.log('B'), 0);
- Promise.resolve().then(() => console.log('C'));
- Promise.resolve().then(() => setTimeout(() => console.log('D'), 0));
- console.log('E');
复制代码 点击查看答案2️⃣
- const promise = new Promise((resolve, reject) => {
- console.log(1);
- console.log(2);
- });
- promise.then(() => {
- console.log(3);
- });
- console.log(4);
复制代码 点击查看答案3️⃣
- async function async1() {
- console.log('1');
- await async2();
- console.log('2');
- }
- async function async2() { console.log('3'); }
- setTimeout(() => console.log('4'), 0);
- async1();
- new Promise(resolve => {
- console.log('5');
- resolve();
- }).then(() => console.log('6'));
- console.log('7');
复制代码 点击查看答案- 1 → 3 → 5 → 7 → 2 → 6 → 4
复制代码 4️⃣
- new Promise((resolve) => {
- console.log(1);
- resolve(3);
- Promise.resolve().then(() => {
- console.log(4);
- });
- }).then((num) => {
- console.log(num);
- });
- setTimeout(() => {
- console.log(6);
- });
- Promise.resolve().then(() => {
- console.log(5);
- });
- console.log(2);
复制代码 点击查看答案5️⃣
- console.log('start');
- setTimeout(() => {
- console.log('Timeout1');
- }, 1000);
- Promise.resolve().then(() => {
- console.log('Promise1');
- });
- Promise.resolve().then(() => {
- console.log('Promise2');
- setTimeout(() => {
- Promise.resolve().then(() => {
- console.log('Promise3');
- })
- console.log('Timeout2');
- }, 0);
- });
- console.log('end');
复制代码 点击查看答案- start → end → Promise1 → Promise2 → Timeout2 → Promise3 → Timeout1
复制代码- function app() {
- setTimeout(() => {
- console.log("1-1");3
- Promise.resolve().then(() => {
- console.log("2-1");5
- });
- });
- console.log("1-2"); 1
- Promise.resolve().then(() => {
- console.log("1-3"); 2
- setTimeout(() => {
- console.log("3-1"); 4
- });
- });
- }
复制代码 点击查看答案``` 1-2 → 1-3 → 1-1 → 3-1 → 2-1```内存管理
JS有如下数据类型:
原始数据类型:String, Number, Boolean, Null, Undefined, Symbol
引用数据类型:Object
而存放这些数据的内存又可以分为两部分:栈内存(Stack)和堆内存(Heap)。原始数据类型存在栈中,引用类型存在堆中。
栈内存
栈是一种只能一端进出的数据结构,先进后出,后进先出。
堆内存
JS中原始数据类型的内存大小是固定的,由系统自动分配内存。但是引用数据类型,比如Object, Array,他们的大小不是固定的,所以是存在堆内存的。JS不允许直接操作堆内存,我们在操作对象时,操作的实际是对象的引用,而不是实际的对象。可以理解为对象在栈里面存了一个内存地址,这个地址指向了堆里面实际的对象。所以引用类型的值是一个指向堆内存的引用地址。
函数也是引用类型,当我们定义一个函数时,会在堆内存中开辟一块内存空间,将函数体代码以字符串的形式存进去。然后将这块内存的地址赋值给函数名,函数名和引用地址会存在栈上。
垃圾回收
垃圾回收就是找出那些不再继续使用的变量,然后释放其占用的内存,垃圾回收器会按照固定的时间间隔周期性执行这一操作。JS使用垃圾回收机制来自动管理内存,但是他是一把双刃剑:
优势: 可以大幅简化程序的内存管理代码,降低程序员负担,减少因为长时间运行而带来的内存泄漏问题。
劣势:程序员无法掌控内存,JS没有暴露任何关于内存的API,我们无法进行强制垃圾回收,更无法干预内存管理。
引用计数
引用计数是一种回收策略,它跟踪记录每个值被引用的次数,每次引用的时候加一,被释放时减一,如果一个值的引用次数变成0了,就可以将其内存空间回收。
使用引用计数会有一个很严重的问题:循环引用。循环引用指的是对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用。- function problem(){
- var objectA = {};
- var objectB = {};
- objectA.a = objectB;
- objectB.b = objectA;
- }
复制代码 在这个例子中,objectA 和 objectB 通过各自的属性相互引用;也就是说,这两个对象的引用次数都是 2。当函数执行完毕后,objectA 和 objectB 还将继续存在,因为它们的引用次数永远不会是 0。
标记—清除算法
算法分为 “标记”和“清除” 两个阶段,最终目的是识别并释放 “不再需要” 的内存。
- 标记阶段:区分 “有用” 和 “无用” 的变量
标记规则:
垃圾回收器会从根对象(Roots) 开始遍历所有变量(根对象通常是全局对象,如浏览器中的window、Node.js 中的global)。
- 所有能被根对象直接或间接访问到的变量,标记为 “有用”(处于 “进入环境” 状态,即仍在执行环境中被使用)。
- 无法被根对象访问到的变量,标记为 “无用”(处于 “离开环境” 状态,即已脱离执行环境,不再被使用)。
- 清除逻辑:
垃圾回收器会遍历内存中所有变量,将标记为 “无用” 的变量占用的内存释放,并将这些内存空间归还给操作系统,供后续使用。
闭包
当一个内部函数引用了外部函数的变量时,就形成了闭包。
闭包的优点/特点
- 通过闭包可以让外部环境访问到函数内部的局部变量
- 通过闭包可以让全局变量持续保存下来,不随着它的上下文一起销毁
通过此特性,我们可以解决一个全局变量污染的问题, 早期在 JavaScript 还无法进行模块化的时候,在多人协作时,如果定义过多的全局变量有可能造成全局变量命名冲突,使用闭包来解决功能对变量的调用将变量写到一个独立的空间里面,从而能够一定程度上解决全局变量污染的问题。
闭包经典面试题一
- for (var i = 1; i <= 3; i++) {
- setTimeout(function () {
- console.log(i);
- }, 1000);
- }
复制代码 (2).Promise.reject
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。- for (var i = 1; i <= 3; i++) {
- (function (index) {
- setTimeout(function () {
- console.log(index);
- }, 1000);
- })(i)
- }
复制代码 (3).Promise.all
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。- function Test(name, age){
- this.name = name
- this.age = age
- }
-
- Test.prototype.say = function(){
- console.log('我能说话')
- }
- var obj3 = new Test('Jack', 26)
- var obj4 = new Test('Rose', 25)
-
- obj3.say() // 我能说话
- obj4.say() // 我能说话
- console.log(obj3.say === obj4.say) // true
-
复制代码 p的状态由p1,p2,p3 决定,分成两种情况。
1️⃣只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
2️⃣只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
(4).Promise.race
Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。- console.log(obj3.__proto__ === Test.prototype)
- // true
复制代码 上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
(5).Promise.allSettled
Promise.allSettled()方法,用来确定一组异步操作是否都结束了(不管成功或失败)。所以,它的名字叫做”Settled“,包含了”fulfilled“和”rejected“两种情况。- function Test(name, age){
- this.name = name
- this.age = age
- }
-
- Test.prototype.say = function(){
- console.log('我能说话')
- }
- var obj3 = new Test('Jack', 26)
-
-
- 1, 构造函数是? 实例是?
- 2, obj3.constructor === Test true or false?
- 3, obj3.__proto__ === Test ?
- 4, Test.prototype === obj3.__proto__ ?
- 5, obj3.__proto__.constructor === Test ?
-
- // 1, Test obj3 2,true 3,false 4,true 5,true
复制代码 (6).Promise.any
只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。
Promise.any()跟Promise.race()方法很像,只有一点不同,就是Promise.any()不会因为某个 Promise 变成rejected状态而结束,必须等到所有参数 Promise 变成rejected状态才会结束。
5.手写Promise
- Test.prototype.__proto__ === Object.prototype
- // true
复制代码 6.Async与Await
(1).Async
async 函数,使得异步操作变得更加方便。
- function Test(name, age){
- this.name = name
- this.age = age
- }
- Test.prototype.say = function(){
- console.log('我能说话')
- }
- var obj3 = new Test('Jack', 26)
- var obj4 = new Test('Rose', 24)
-
- 1, Test.prototype === ( ) ?
- 2, obj3.__proto__.__proto__ === ( ) ?
- 3, obj3.__proto__ === obj4.__proto__ ?
- 4, Test.prototype.__proto__ === ( ) ?
- 5, obj4.__proto__.constructor === ( ) ?
- 6, Object.prototype.__proto__ === ( ) ?
- 7, obj3.say === obj4.say ?
-
-
- // 1, obj3.__proto__ 或 obj4.__proto 2,Object.prototype 3, true (二者都由Test new出来,在原型链上都指向 Test.prototype)
- // 4, Object.prototype 5, Test 6, null (终点) 7,true (同问题3)
复制代码 (2).Await
await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。- ajax({
- url: '我是第一个请求',
- success (res) {
- // 现在发送第二个请求
- ajax({
- url: '我是第二个请求',
- data: { a: res.a, b: res.b },
- success (res2) {
- // 进行第三个请求
- ajax({
- url: '我是第三个请求',
- data: { a: res2.a, b: res2.b },
- success (res3) {
- console.log(res3)
- }
- })
- }
- })
- }
- })
复制代码 (3)错误处理
- new Promise(function (resolve, reject) {
- // resolve 表示成功的回调
- // reject 表示失败的回调
- }).then(function (res) {
- // 成功的函数
- }).catch(function (err) {
- // 失败的函数
- })
复制代码 箭头函数和普通函数
1.语法上
- 普通函数,使用 function 关键字定义,语法相对冗长。
- 箭头函数 使用 => 语法定义,语法简洁,适合单行函数
2.this 的绑定
- 普通函数的 this 是动态绑定的,取决于函数的调用方式。
在全局作用域中,this 指向 window(浏览器)或 global(Node.js)。
在对象方法中,this 指向调用该方法的对象。
- 箭头函数的 this 是词法绑定的,继承自外层作用域的 this。
箭头函数没有自己的 this,因此不能通过 call、apply 或 bind 改变 this。
普通函数中
- 在 setTimeout 的回调函数中,使用了普通函数 function() {... }。普通函数的 this 指向是动态的,它的值取决于函数的调用方式。
- 在 setTimeout 中,回调函数是在全局作用域下被调用的(严格模式下为 undefined),而不是在 normal 对象的上下文中被调用的。因此,在这个回调函数中,this 并不指向 normal 对象,而是指向全局对象(在浏览器环境中是 window 对象)。
当你调用 setTimeout 并传入一个回调函数时,JavaScript 引擎会将这个回调函数添加到一个任务队列中。当指定的时间到达时,引擎会从任务队列中取出这个回调函数并执行它。任务队列处于全局环境中。
- 由于全局对象中没有定义 bibi 属性,所以 this.bibi 的值是 undefined,导致最终输出 普通函数: undefined。
箭头函数中
- 在 setTimeout 的回调函数中,使用了箭头函数 () => {... }。箭头函数的 this 指向是静态的,它继承自外层作用域的 this。
箭头函数的 this 绑定是在定义时就确定的,因此在箭头函数被添加到任务队列之前,this 的值已经被捕获并保存。当箭头函数在任务队列中被执行时,它会使用这个已经确定的 this 值。
- 在 arrow 对象的 biubiu 方法中,this 指向 arrow 对象。箭头函数的 this 继承了外层作用域(即 biubiu 方法)的 this,因此在箭头函数中,this 仍然指向 arrow 对象。
3.构造函数
- 普通函数可以作为构造函数使用,通过 new 关键字创建实例。构造函数内部的 this 指向新创建的实例。
- 箭头函数不能作为构造函数使用,使用 new 调用箭头函数会抛出错误。箭头函数没有 prototype 属性。
This指向
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |