数据类型的分类和判断

基本(值)类型

  • Number ——- 任意数值 ———— typeof
  • String ——- 任意字符串 ——— typeof
  • Boolean —— true/false ——- typeof
  • undefined —- undefined ——- typeof/===
  • null ———— null ————— ===

    对象(引用)类型

  • Object ——- typeof/instanceof

  • Array ——— instanceof (特别的对象,有特定顺序,数值下标)
  • Function —— typeof (特别的对象,可以执行)

    总结:

  1. 只要后面带括号调用的都是函数;比如 console.log()
  2. 确定好得到的是什么类型的数据才能决定下一步怎么操作;比如obj.setName之后得到的是一个函数 那么就要加括号obj.setName()如果函数里返回的还是一个函数 那么需要再次加括号 obj.setName()()
  3. instenceof用来判断对象的具体类型,看所判断的是否是这个对象的实例;(instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。。)
  4. typeof可以判断 数值、字符串、布尔值、undefined、function ;

不能判断 null和object,array和object因为它俩同类型;typeof它返回的是数据类型的字符串,且为小写, 比如:
a = 3;
console.log(typeof a === ‘number’)
由于undefined的类型也是undefined 所以值和类型容易混淆,所以如果需要返回undefined 则返回 void 0就可以代替undefined

  1. ==/===可以判断undefined和null,比如:

    1. var a<br /> console.log(a, typeof a, a===undefined) // undefined 'undefined' true<br /> console.log(a===typeof a)//false<br /> //值为undefined,类型为'undefined'或者 值为null,类型为'object' ;<br /> //===在判断时不会进行类型转换,==会进行类型转换
  2. null是定义并赋值了,只是值为null;而undefined是定义了未赋值

  3. null虽然是基本数据类型,但是其typeof却是object,这是因为:

    1. 初始赋值,表明变量将要赋值为对象,比如:var obj = null<br /> 结束前让对象成为垃圾对象(被垃圾回收器回收)比如:var obj = {name:'qcq'}; obj = null
  4. 严格区分变量类型与数据类型

    1. ![2022-01-24_213833.jpg](https://cdn.nlark.com/yuque/0/2022/jpeg/22947697/1643031793866-54299c1e-0c4d-46da-8878-320260d68a9f.jpeg#clientId=ucaa67b3c-673b-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u52c0b8a8&margin=%5Bobject%20Object%5D&name=2022-01-24_213833.jpg&originHeight=266&originWidth=538&originalType=binary&ratio=1&rotation=0&showTitle=false&size=39346&status=done&style=none&taskId=u8d88db38-362d-4b34-94e6-cd8970fc752&title=)
  5. 形参的本质是 :局部变量,实参的本质是: 数据

  6. constructor有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了:

function Fn(){};
Fn.prototype = new Array();
var f = new Fn();
console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true
11.Object.prototype.toString()

为什么0.1+0.2 ! == 0.3,如何让其相等

在开发过程中遇到类似这样的问题:
let n1 = 0.1, n2 = 0.2 console.log(n1 + n2) // 0.30000000000000004 复制代码
这里得到的不是想要的结果,要想等于0.3,就要把它进行转化:
(n1 + n2).toFixed(2) // 注意,toFixed为四舍五入 复制代码
toFixed(num) 方法可把 Number 四舍五入为指定小数位数的数字。

object.assign和扩展运算法是深拷贝还是浅拷贝,两者区别

扩展运算符:
let outObj = { inObj: {a: 1, b: 2} } let newObj = {…outObj} newObj.inObj.a = 2 console.log(outObj) // {inObj: {a: 2, b: 2}} 复制代码
Object.assign():
let outObj = { inObj: {a: 1, b: 2} } let newObj = Object.assign({}, outObj) newObj.inObj.a = 2 console.log(outObj) // {inObj: {a: 2, b: 2}} 复制代码
可以看到,两者都是浅拷贝。

  • Object.assign()方法接收的第一个参数作为目标对象,后面的所有参数作为源对象。然后把所有的源对象合并到目标对象中。它会修改了一个对象,因此会触发 ES6 setter。6
  • 扩展操作符(…)使用它时,数组或对象中的每一个值都会被拷贝到一个新的数组或对象中。它不复制继承的属性或类的属性,但是它会复制ES6的 symbols 属性。

深拷贝:JSON.parse(JSON.stringiify(obj))一种深拷贝的方法,先将一个对象转化为json对象,然后再解析这个json对象
源对象和拷贝对象相互独立,修改一个对象的属性都不会对另一个对象造成影响
var arr = [[7,8],[9.10]];//二维数组
var a = JSON.parse(JSON.stringify(arr))
a[0][0] = 100
console.log(arr[0][0])
console.log(a[0][0])//此时输出的是7和100
深拷贝过后,修改新数组中的一个子数组的元素,原数组不会改变
例子说明(如果a复制了b,修改b a不发生变化,修改b a不发生变化 那么这个就是深拷贝;好比我直接拿你的电脑硬盘复制了一份,然后插在我的电脑上,虽然我们一开始的内容相同,但实际上我对我电脑的操作对你的电脑不会产生影响,你对我也不会有影响)
总结:只要通过栈内存拷贝的就是浅拷贝,通过堆内存拷贝的就是深拷贝。

call和bind和apply的区别:call和apply和bind都可以改变this的指向,apply和call会让当前函数立即执行,而bind会返回一个函数,后续需要的时候再调用执行,需要从函数调用加(),bind需要将实参封装到对象中传递

块级作用域: 块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:

  • 内层变量可能覆盖外层变量
  • 用来计数的循环变量泄露为全局变量

9. 如何获取安全的 undefined 值?

因为 undefined 是一个标识符,所以可以被当作变量来使用和赋值,但是这样会影响 undefined 的正常判断。表达式 void _ 没有返回值,因此返回结果是 undefined。void 并不改变表达式的结果,只是让表达式不返回值。因此可以用 void 0 来获得 undefined。

10. isNaN 和 Number.isNaN 函数的区别?

  • 函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。
  • 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

    11. == 操作符的强制类型转换规则?

    对于 == 来说,如果对比双方的类型不一样,就会进行类型转换。假如对比 x 和 y 是否相同,就会进行如下判断流程:
  1. 首先会判断两者类型是否相同,相同的话就比较两者的大小;
  2. 类型不相同的话,就会进行类型转换;
  3. 会先判断是否在对比 null 和 undefined,是的话就会返回 true
  4. 判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number

    12. typeof NaN 的结果是什么?

    NaN 指“不是一个数字”(not a number),NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。
    typeof NaN; // “number” 复制代码
    NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN !== NaN 为 true。

实现call、apply 及 bind 函数

(1)call 函数的实现步骤:

  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  • 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  • 处理传入的参数,截取第一个参数后的所有参数。
  • 将函数作为上下文对象的一个属性。
  • 使用上下文对象来调用这个方法,并保存返回结果。
  • 删除刚才新增的属性。
  • 返回结果。

Function.prototype.myCall = function(context) { // 判断调用对象 if (typeof this !== “function”) { console.error(“type error”); } // 获取参数 let args = […arguments].slice(1), result = null; // 判断 context 是否传入,如果未传入则设置为 window context = context || window; // 将调用函数设为对象的方法 context.fn = this; // 调用函数 result = context.fn(…args); // 将属性删除 delete context.fn; return result; }; 复制代码
(2)apply 函数的实现步骤:

  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  • 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  • 将函数作为上下文对象的一个属性。
  • 判断参数值是否传入
  • 使用上下文对象来调用这个方法,并保存返回结果。
  • 删除刚才新增的属性
  • 返回结果

Function.prototype.myApply = function(context) { // 判断调用对象是否为函数
if (typeof this !== “function”) { throw new TypeError(“Error”); }
let result = null; // 判断 context 是否存在,如果未传入则为 window
context = context || window; // 将函数设为对象的方法
context.fn = this; // 调用方法
if (arguments[1]) { result = context.fn(…arguments[1]); }
else { result = context.fn(); }
// 将属性删除
delete context.fn; return result; };
(3)bind 函数的实现步骤:

  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  • 保存当前函数的引用,获取其余传入参数值。
  • 创建一个函数返回
  • 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。

Function.prototype.myBind = function(context) { // 判断调用对象是否为函数
if (typeof this !== “function”) { throw new TypeError(“Error”); } // 获取参数
var args = […arguments].slice(1),
fn = this;
return function Fn() { // 根据调用方式,传入不同绑定值
return fn.apply( this instanceof Fn ? this : context, args.concat(…arguments) );
}; };

数据,变量, 内存的理解

什么是数据?

  • 在内存中可读的, 可传递的保存了特定信息的’东东’
  • 一切皆数据, 函数也是数据
  • 在内存中的所有操作的目标: 数据

    什么是变量?

  • 在程序运行过程中它的值是允许改变的量

  • 一个变量对应一块小内存, 它的值保存在此内存中
  • 变量名用来查找对应的内存,变量值就是内存中保存的数据(变量是内存的标识)

    什么是内存?

  • 内存条通电后产生的存储空间(临时的)

  • 一块内存包含2个方面的数据
    • 内部存储的数据
    • 地址值数据:标识自己位置的数据
  • 内存空间的分类
    • 栈空间: 全局变量和局部变量
    • 堆空间: 对象({ }对象本身在堆空间里,而标识这个对象的变量名在栈空间里,也就是说对象名在栈空间里)
  • 特别分清:

    1. ![2022-01-24_223757.jpg](https://cdn.nlark.com/yuque/0/2022/jpeg/22947697/1643039095045-de3201cc-1356-4ce7-b83e-39ccc8b27919.jpeg#clientId=u4e73bfd1-93bf-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u7fda8656&margin=%5Bobject%20Object%5D&name=2022-01-24_223757.jpg&originHeight=732&originWidth=1135&originalType=binary&ratio=1&rotation=0&showTitle=false&size=144347&status=done&style=none&taskId=u8740031c-2484-4a57-a929-73ea0f9792c&title=)

    内存,数据, 变量三者之间的关系

  • 内存是容器, 用来存储不同数据

  • 变量是内存的标识, 通过变量我们可以操作(读/写)内存中的数据
  • 只有变量在等号的左边就是写,其他都是读的情况;比如 var a = 3;function fun(b){ };fun(a);相当于 var b = a 。这是写的操作

    相关问题

    问题1:var a = xxx, a中到底保存的是什么2022-01-24_231050.jpg

    问题2:关于引用变量赋值问题

    2022-01-24_235414.jpg2022-01-24_235440.jpg2022-01-24_235846.jpg

    问题3:在js调用函数时传递变量参数时,是值传递还是引用传递

    image.png

    问题4:js引擎如何管理内存

    1. ***局部变量在函数执行时产生,在函数执行完毕之后自动释放。全局变量不会释放,对象通过等于null之后变成垃圾对象 后被垃圾回收器回收**<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22947697/1643041482964-adcf252c-473a-4bf3-b471-3f9b5dbc0e3e.png#clientId=u4e73bfd1-93bf-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=489&id=uf65eb96b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=611&originWidth=710&originalType=binary&ratio=1&rotation=0&showTitle=false&size=256332&status=done&style=none&taskId=u11bdb5b0-25e8-4c93-b2be-7e09a3fd95d&title=&width=568)

    垃圾回收的方式

    浏览器通常使用的垃圾回收方法有两种:标记清除,引用计数。 1)标记清除
  • 标记清除是浏览器常见的垃圾回收方式,当变量进入执行环境时,就标记这个变量“进入环境”,被标记为“进入环境”的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为“离开环境”,被标记为“离开环境”的变量会被内存释放。
  • 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。

对象的理解和使用

什么是对象?

  • 多个数据(属性)的封装体
  • 用来保存多个数据(属性)的容器
  • 一个对象代表现实世界中的一个事物

    为什么用对象?

  • 统一管理多个数据

    属性组成:

  • 属性名 : 字符串(标识)

  • 属性值 : 任意类型

    属性的分类:

  • 一般 : 属性值不是function 描述对象的状态

  • 方法 : 属性值为function的属性 描述对象的行为

    特别的对象

  • 数组: 属性名是0,1,2,3之类的索引

  • 函数: 可以执行的

    如何操作内部属性(方法)

  • 对象.属性名:编码简单有时不能用

  • 对象[‘属性名’]:编码麻烦,但是能通用

    问题:什么时候使用obj[‘属性名’]的方式

    属性名不确定时这种中括号操作方式会经常使用,需要用变量来存储,必须用[‘属性名’]的方式
    image.png

函数的理解和使用

什么是函数?

  • 用来实现特定功能的, n条语句的封装体
  • 只有函数类型的数据是可以执行的, 其它的都不可以

    为什么要用函数?

  • 提高复用性

  • 便于阅读交流

image.png

函数也是对象

  • instanceof Object===true
  • 函数有属性: prototype
  • 函数有方法: call()/apply()
  • 可以添加新的属性/方法

    函数的3种不同角色

  • 一般函数 : 直接调用

  • 构造函数 : 通过new调用
  • 对象 : 通过.调用内部的属性/方法

    函数中的this

  • fun() //this指的是window

  • obj.fun() //this指的是obj
  • var obj = new Fun() //this指的是obj
  • fun.call(obj) //this指的是obj

    对this对象的理解

    this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。
  • 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
  • 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。
  • 第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
  • 第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。

这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。

匿名函数自调用

  • 专业术语为: IIFE (Immediately Invoked Function Expression) 立即调用函数表达式
  • 我们又称“匿名函数自调用”
  • (function(w, obj){
    //实现代码
    })(window, obj)
  • 作用:隐藏实现,不会污染外部(全局)命名空间

image.png
2022-01-25_183901.jpg

回调函数的理解

什么函数才是回调函数?

  1. - 你定义的
  2. - 你没有调用
  3. - 但它最终执行了(在一定条件下或某个时刻)

常用的回调函数

  1. - dom事件回调函数
  2. - 定时器回调函数
  3. - ajax请求回调函数(后面讲解)
  4. - 生命周期回调函数(后面讲解)

原型与原型链

  • 所有函数都有一个特别的属性:
    • prototype : 显式原型属性
    • 执行函数定义时产生var fun = function(){},函数体没有执行,只是创建了函数对象 会执行this.prototype = {},this指的是函数对象,就是原型对象(区别:执行函数时,函数加( ),函数体执行)
    • 函数原型上的方法是给实例对象来使用的
  • 所有实例对象都有一个特别的属性:
    • proto : 隐式原型属性
    • 对象的隐式原型的值为其构造函数显式原型的值,都指向空的object
    • 创建实例对象时执行this.proto=Fun.prototype this指的是实例对象
  • 总结:

    • Fun.prototype和fn.proto保存的都是地址值(引用变量),而且地址值是一样的
    • 显式原型属性是在执行函数定义的时候产生的
    • 实例调用方法时找的是隐式原型属性(读取),而添加方法时用的是显式原型属性(添加);互不干扰
    • 函数的prototype属性是在函数定义时自动添加的,默认是一个空的object对象(但object自身不满足)
    • 对象的proto属性是在创建实例对象时自动添加的,默认值为构造函数的prototype属性值
    • 程序员能直接操作的是显式原型,但不能操作隐式原型

      1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/22947697/1643121493857-05902755-5585-4283-86d2-9bdf6ee3ac79.png#clientId=ufd1d60ff-31d9-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=618&id=ubb4971e8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=772&originWidth=1324&originalType=binary&ratio=1&rotation=0&showTitle=false&size=628776&status=done&style=none&taskId=u15f71215-8bcd-4a4c-9a40-f9b74f57aaf&title=&width=1059.2)

image.png

  • 显式原型与隐式原型的关系
    • 函数的prototype: 定义函数时被自动赋值, 值默认为{}, 即用为原型对象(空的object实例对象)
    • 实例对象的proto: 在创建实例对象时被自动添加, 并赋值为构造函数的prototype值
    • 原型对象即为当前实例对象的父对象
  • 原型链
    • 所有的实例对象都有proto属性, 它指向的就是原型对象
    • 这样通过proto属性就形成了一个链的结构——>原型链
    • 当查找对象内部的属性/方法时, js引擎自动沿着这个原型链查找
    • 当给对象属性赋值时不会使用原型链, 而只是在当前对象中进行操作
    • 原型链是用来查找对象的属性的
    • 当设置属性时不看原型链obj.test=xxx,属性一般通过构造函数定义在对象自身上,方法一般在原型上。因为方法大多一样,而属性一般不一样
    • 也就是说原型上的属性和方法就是给实例对象来用的,利用的是原型链
    • 函数的prototype属性是在函数定义时自动添加的,默认是一个空的object对象(但object自身不满足),也就是说除了Object自身之外,剩下的所有函数都是Object的实例
    • console.log(Object instanceof Object) 推导:Object.proto== Function.prototype, Function.prototype.proto == Object.prototype(所有函数的原型对象默认都是Object的实例,但Object除外)。所以:Object instanceof Object
  • 总结:查找对象的属性是按照隐式原型链进行的,与显式原型链没有关系,所以原型链的别名为隐式原型链


    image.png

    • 由于任何函数都是Function的实例,所以任何函数都有隐式原型属性和显式原型属性,其隐式原型属性指向Function的显式原型,因此所有函数的proto都是一样的,都指向Function的prototype; 且Object的proto也指向Function的prototype,因为Object也是Function的实例
    • 由于Function是new自身创建的:var Function= new Function(),所以Function的显式原型属性指向Function的隐式原型属性 Function.prototype === Function.proto
    • Object的原型对象是原型链的尽头 Object.prototype.proto===null

image.png
image.png
image.png

原型修改、重写

function Person(name) { this.name = name } // 修改原型 Person.prototype.getName = function() {} var p = new Person(‘hello’) console.log(p.proto === Person.prototype) // true console.log(p.proto === p.constructor.prototype) // true // 重写原型 Person.prototype = { getName: function() {} } var p = new Person(‘hello’) console.log(p.proto === Person.prototype) // true console.log(p.proto === p.constructor.prototype) // false 复制代码
可以看到修改原型的时候p的构造函数不是指向Person了,因为直接给Person的原型对象直接用对象赋值时,它的构造函数指向的了根构造函数Object,所以这时候p.constructor === Object ,而不是p.constructor === Person。要想成立,就要用constructor指回来:
Person.prototype = { getName: function() {} }
var p = new Person(‘hello’)
p.constructor = Person
console.log(p.proto === Person.prototype) // true
console.log(p.proto === p.constructor.prototype) // true

原型链的指向:

p.proto // Person.prototype
Person.prototype.proto // Object.prototype
p.proto.proto //Object.prototype
p.proto.constructor.prototype.proto // Object.prototype Person.prototype.constructor.prototype.proto // Object.prototype
p1.proto.constructor // Person
Person.prototype.constructor // Person

如何获得对象非原型链上的属性?

使用后hasOwnProperty()方法来判断属性是否属于原型链的属性:
function iterate(obj){
var res=[];
for(var key in obj){
if(obj.hasOwnProperty(key))
res.push(key+’: ‘+obj[key]);
} return res;
}

image.png
b.n=1,b.m=’undefined’,c.n=2,c.m=3
image.png
f.a()=>a(), f.b()=>’undefined’, F.a()=>a(), F.b()=>b()
注意:给谁点就是用谁的隐式原型属性去找(一路隐式原型属性) 比如F.a() F的proto为Function的prototype,而Function的prototype.proto为Object的prototype的值

执行上下文与执行上下文栈

变量提升与函数提升

// var a = 123
// function fun() {
// alert(a)
// a = 456
// }
// fun() //123
// alert(a) //456

  1. // var a = 123<br /> // function fun(a) {<br /> // // var a<br /> // alert(a) // undefined<br /> // a = 456<br /> // }<br /> // fun()<br /> // alert(a) // 123
  2. // var a = 123<br /> // function fun(a) {<br /> // alert(a) // 123<br /> // a = 456<br /> // }<br /> // fun(123)<br /> // alert(a) // 123
  • 变量提升: 通过var声明的变量,在变量定义语句之前, 就可以访问到这个变量(undefined)
  • 函数提升:通过function声明的函数, 在函数定义语句之前, 就执行该函数定义(函数定义/对象)
  • 先有变量提升, 再有函数提升
  • 注意:通过函数表达式声明的函数不能提升 var fun = function(){}
  • 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间
  • 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行
    • 理解
  • 执行上下文: 由js引擎自动创建的对象, 包含对应作用域中的所有变量属性(代码运行前的准备工作,这也就是函数提升和变量提升的原因)
  • image.png

    • 简单来说执行上下文就是指:

    在执行一点JS代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。
    在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。

  • 全局上下文:变量定义,函数声明
  • 函数上下文:变量定义,函数声明,this,arguments

    • 全局执行上下文
    • image.png
    • 函数执行上下文:在调用函数准备执行函数体之前,创建对应的执行上下文对象(虚拟的,存在于栈中)(在调用时产生函数执行上下文对象)
    • image.png
    • 执行上下文时,全局变量和局部变量会存在栈空间中,当局部变量执行完毕之后会自动释放
    • 执行上下文栈: 用来管理产生的多个执行上下文
    • 如果要形成window—-fun1()—-fun2()这样的执行上下文栈的情况,由于fun1和fun2同时存在,所以必须是在fun1里面调用fun2。否则fun2的执行上下文产生时fun1已经释放
    • 2022-01-27_220410.png
      执行上下文栈
  • JavaScript引擎使用执行上下文栈来管理执行上下文

  • 当JavaScript执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。
  • 分类:
    • 全局: window
    • 函数: 对程序员来说是透明的
  • 生命周期
    • 全局 : 准备执行全局代码前产生, 当页面刷新/关闭页面时死亡
    • 函数 : 调用函数时产生, 函数执行完时死亡
  • 包含哪些属性:

    • 全局 :
      • 用var定义的全局变量 ==>undefined
      • 使用function声明的函数 ===>functio
      • n
      • this ===>window
    • 函数

      • 用var定义的局部变量 ==>undefined
      • 使用function声明的函数 ===>function
      • this ===> 调用函数的对象, 如果没有指定就是window
      • 形参变量 ===>对应实参值
      • arguments ===>实参列表的伪数组

        什么是尾调用,使用尾调用有什么好处?

        尾调用指的是函数的最后一步调用另一个函数。代码执行是基于执行栈的,所以当在一个函数里调用另一个函数时,会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中。使用尾调用的话,因为已经是函数的最后一步,所以这时可以不必再保留当前的执行上下文,从而节省了内存,这就是尾调用优化。但是 ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

        执行上下文创建和初始化的过程

    • 全局:

      • 在全局代码执行前最先创建一个全局执行上下文(window)
      • 收集一些全局变量, 并初始化
      • 将这些变量设置为window的属性
    • 函数:
      • 在调用函数时, 在执行函数体之前先创建一个函数执行上下文
      • 收集一些局部变量, 并初始化
      • 将这些变量设置为执行上下文的属性

作用域与作用域链

  • 理解:
    • 作用域: 一块代码区域, 在编码时就确定了, 不会再变化
    • 作用域链: 多个嵌套的作用域形成的由内向外的结构, 用于查找变量
  • 分类:
    • 全局
    • 函数
    • js没有块作用域(在ES6之前)(在大括号里面创建的变量在外面可以访问到)function fun(){var a = 3} console.log(a)
    • 块作用域的作用:1.避免内部变量覆盖外部变量 2.避免在计数过程中循环变量泄露为全局变量
    • n+1原则(n是函数作用域,1是全局作用域)
  • 作用
    • 作用域: 隔离变量, 可以在不同作用域定义同名的变量不冲突
    • 作用域链: 查找变量(找a.b 先找作用域链 找到a变量 再沿着原型链找a.b(原型链找属性))
  • 区别作用域与执行上下文

    • 作用域: 静态的, 函数定义时就确定了(不是在运行时), 一旦确定就不会变化了;不是在函数调用时产生的
    • 执行上下文: 动态的, 执行代码时动态创建, 当执行结束消失,函数执行上下文在函数调用时产生,调用结束后自动释放
    • 联系: 执行上下文环境是在对应的作用域中的
      全局作用域和函数作用域
      (1)全局作用域
  • 最外层函数和最外层函数外面定义的变量拥有全局作用域

  • 所有未定义直接赋值的变量自动声明为全局作用域
  • 所有window对象的属性拥有全局作用域
  • 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。

(2)函数作用域

  • 函数作用域声明在函数内部的变零,一般只有固定的代码片段可以访问到
  • 作用域是分层的,内层作用域可以访问外层作用域,反之不行

    2)块级作用域
  • 使用ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由{ }包裹的代码片段)

  • let和const声明的变量不会有变量提升,也不可以重复声明
  • 在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部。


  • 面试题:
  • 2022-01-28_225040.png
  • 不论fn()在哪里调用,其作用域在编码时就已经确定了,不再改变,所以输出的x本身函数作用域没有那就在全局作用域找
  • 2022-01-28_225536.png
  • 输出fn2会报错,函数作用域以及全局作用域都找不到,要想访问fn2,只能通过对象.属性的方法也就是 this.fn2

闭包

如何产生闭包:当一个嵌套的内部函数(子)引用了外部函数(父)的变量(函数)就产生了闭包

理解:

  • 当嵌套的内部函数引用了外部函数的变量时就产生了闭包
  • 当嵌套的内部函数包含了外部被引用变量的对象就产生了闭包(有了这两个条件这个时候只要执行了内部函数定义就会产生闭包(不用调用内部函数))
  • 通过chrome工具得知: 闭包本质是内部函数中的一个对象, 这个对象中包含外部的变量属性
  • 外部函数调用几次就会产生几个闭包,像这样f()调用两次实际上调的是内部fn2(),只有var f = fn1()和fn1()这样产生两个闭包
  • 当执行var f = fn1()时 fn2已经不存在了 只是将fn2函数对象的地址赋值给了f 只剩下了函数体和里面的闭包a,其他都已经释放
  • 如果没有f来接收返回值的话,fn1执行完,内部函数就已经成为垃圾对象
    • 闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

1483916-20190721192642214-1008386261.png1483916-20190721192714523-990772361.png

JS高级 - 图26

这张图足以可以知道什么是闭包了,简单来说,就是全局想要持续访问局部中的变量,但因为函数在执行完后会进行销毁,你就需要给函数设置个儿子,让儿子返回这个变量,然后再将儿子返回,以便达到你可以持续访问这个变量。
为什么要使用闭包?
因为我们想要持续的使用一个变量,放在全局中会造成全局污染,放在函数中,函数执行完后会销毁,变量也随之销毁,因此需要使用闭包。
闭包的好处:就是可以持续访问局部中的变量。
闭包的坏处:会占用更多的内存,不容易被释放。

  • 2022-01-29_175040.png
  • 因为内部函数引用外部msg产生闭包
  • image.png

    闭包到底是什么:

    是一个对象 在内部函数里面 包含被引用的变量(通过chrome调试工具发现的)

    问题:

    image.png

    作用:

    • 使用函数内部的变量在函数执行完之后仍然在内存中(延长局部变量的生命周期)
    • 让函数外部能操作(读写)内部的数据(变量/函数)

      生命周期:

      内部函数定义执行完产生,在内部函数成为垃圾对象时死亡
      image.png
      image.png

      闭包应用:

    • 模块化: 封装一些数据以及操作数据的函数, 向外暴露一些行为(具有特定功能的js文件,将所有数据封装在函数内部,向外暴露一个包含n个方法的对象或者函数)(暴露一个行为用函数(先执行对象),暴露多个行为用对象(直接用:对象.方法))

    • 循环遍历加监听
    • JS框架(jQuery)大量使用了闭包
  • 这是第一种方法,首先要理解要想让数据和功能是私有的就必须放在函数当中,因为只有函数才会创建作用域
  • 2022-01-29_213443.png
  • 2022-01-29_213731.png
  • 这是第二种方法 也是比较常用的,方便我们在外面使用的方法,之所以传进window是因为 最后要进行代码压缩,如果不传的话 内部的window不能被随便的一个字母代替(匿名函数自调用的方式,隐藏实现,避免污染全局空间)2022-01-29_213609.png2022-01-29_213946.png

    缺点:

    • 变量占用内存的时间可能会过长
    • 可能导致内存泄露
    • 解决:
      • 及时释放 : f = null; //让内部函数对象成为垃圾对象

内存溢出与内存泄露

内存溢出

  • 一种程序运行出现的错误
  • 当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误

    内存泄露

  • 占用的内存没有及时释放

  • 内存泄露积累多了就容易导致内存溢出(内存逐渐减少,逐步不够用)
  • 常见的内存泄露:
    • 意外的全局变量(不用var 声明的变量)
      • image.png
    • 没有及时清理的计时器或回调函数
      • image.png
    • 闭包
      • image.png

        面试题

        image.png
        image.png

经典面试题:循环中使用闭包解决 var 定义函数的问题
for (var i = 1; i <= 5; i++)
{ setTimeout(function timer()
{ console.log(i) }, i * 1000)
}
首先因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。解决办法有三种:

  • 第一种是使用闭包的方式

for (var i = 1; i <= 5; i++) { ;(function(j) { setTimeout(function timer() { console.log(j) }, j * 1000) })(i)}
在上述代码中,首先使用了立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。

  • 第二种就是使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。

for (var i = 1; i <= 5; i++) { setTimeout( function timer(j) { console.log(j) }, i * 1000, i ) } 复制代码

  • 第三种就是使用 let 定义 i 了来解决问题了,这个也是最为推荐的方式

for (let i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, i * 1000) }

对象的创建模式

Object构造函数模式

<!—

方式一: Object构造函数模式
* 套路: 先创建空Object对象, 再动态添加属性/方法
* 适用场景: 起始时不确定对象内部数据
* 问题: 语句太多
—>

/*
一个人: name:”Tom”, age: 12
*/
var p=new Object()
p= {}
p.name =’Tom’
p.age =12
p.toString.setName=function (name) {
this.name =name
}
p.toString.getName=function (age) {
this.age =age
}
console.log(p)

对象字面量模式

<!—
方式二: 对象字面量模式
* 套路: 使用{}创建对象, 同时指定属性/方法
* 适用场景: 起始时对象内部数据是确定的
* 问题: 如果创建多个对象, 有重复代码
—>

varp= {
name:’Tom’,
age:23,
setName:function (name) {
this.name =name
}
}
console.log(p.name,p.age)
p.setName(‘JACK’)
console.log(p.name,p.age)
varp2= {
name:’BOB’,
age:24,
setName:function (name) {
this.name =name
}
}

工厂模式(能返回对象的函数就叫工厂模式)

<!—

方式三: 工厂模式
* 套路: 通过工厂函数动态创建对象并返回
* 适用场景: 需要创建多个对象
* 问题: 对象没有一个具体的类型, 都是Object类型
—>

// 工厂函数: 返回一个需要的数据的函数
functioncreatePerson(name,age) {
var p= {
name:name,
age:age,
setName:function (name) {
this.name =name
}
}
return p
}
varp1=createPerson(‘Tom’,12)
varp2=createPerson(‘JAck’,13)
console.log(p1)
console.log(p2)

构造函数模式

<!—

方式四: 自定义构造函数模式
* 套路: 自定义构造函数, 通过new创建对象
* 适用场景: 需要创建多个类型确定的对象
* 问题: 每个对象都有相同的数据, 浪费内存
—>

functionPerson(name,age) {
this.name =name
this.age =age
this.setName=function (name) {
this.name =name
}
}
varp1=newPerson(‘Tom’,12)
varp2=newPerson(‘Tom2’,13)
console.log(p1,p1instanceof Person)

构造函数+原型的组合模式

<!—

方式六: 构造函数+原型的组合模式
* 套路: 自定义构造函数, 属性在函数中初始化, 方法添加到原型上
* 适用场景: 需要创建多个类型确定的对象
—>

functionPerson (name,age) {
this.name =name
this.age =age
}
Person.prototype.setName=function (name) {
this.name =name
}
varp1=newPerson(‘Tom’,12)
varp2=newPerson(‘JAck’,23)
p1.setName(‘TOM3’)
console.log(p1)
Person.prototype.setAge=function (age) {
this.age =age
}
p1.setAge(23)
console.log(p1.age)
Person.prototype = {}
p1.setAge(34)
console.log(p1)
varp3=newPerson(‘BOB’,12)
p3.setAge(12)

继承模式

原型链继承 :

  • 得到方法
    function Parent(){}
    Parent.prototype.test = function(){};
    function Child(){}**Child.prototype = new Parent(); // 子类型的原型指向父类型实例
    Child.prototype.constructor = Child
    var child = new Child(); //有test()

2022-01-30_163745.png

借用构造函数 :

  • 得到属性
    function Parent(xxx){this.xxx = xxx}
    Parent.prototype.test = function(){};
    function Child(xxx,yyy){
    Parent.call(this, xxx);//借用构造函数 this.Parent(xxx)
    }
    var child = new Child(‘a’, ‘b’); //child.xxx为‘a’, 但child没有test()

    组合


  • function Parent(xxx){this.xxx = xxx}
    Parent.prototype.test = function(){};
    function Child(xxx,yyy){
    Parent.call(this, xxx);//借用构造函数 this.Parent(xxx)
    }
    Child.prototype = new Parent(); //得到test()
    var child = new Child(); //child.xxx为‘a’, 也有test()

2022-01-30_180223.png
2022-01-30_180151.png

new一个对象背后做了些什么?

  • 创建一个空对象
  • 给对象设置proto, 值为构造函数对象的prototype属性值 this.proto = Fn.prototype
  • 执行构造函数体(给对象添加属性/方法)
  • 返回对象

    线程与进程

进程:

  • 程序的一次执行, 它占有一片独有的内存空间
  • 可以通过windows任务管理器查看进程

    线程:

  • 是进程内的一个独立执行单元

  • 是程序执行的一个完整流程
  • 是CPU的最小的调度单元

    关系

  • 一个进程至少有一个线程(主)

  • 程序是在某个进程中的某个线程执行的
    • js是单线程的(H5之后为多线程),浏览器是多线程的。浏览器有多进程也有单进程

浏览器内核模块组成

主线程

  • js引擎模块 : 负责js程序的编译与运行
  • html,css文档解析模块 : 负责页面文本的解析
  • DOM/CSS模块 : 负责dom/css在内存中的相关处理
  • 布局和渲染模块 : 负责页面的布局和效果的绘制(内存中的对象)

    • 2022-01-30_222615.png

      分线程

  • 定时器模块 : 负责定时器的管理

  • DOM事件模块 : 负责事件的管理
  • 网络请求模块 : 负责Ajax请求

js线程

  • js是单线程执行的(回调函数也是在主线程)
  • H5提出了实现多线程的方案: Web Workers
  • 只能是主线程更新界面

定时器问题:

  • 定时器并不真正完全定时
  • 如果在主线程执行了一个长时间的操作, 可能导致延时才处理(定时器代码在主线程执行,比如当时间到了1000毫秒之后将里面的回调函数交给分线程也就是定时器管理模块去执行)

当初始化代码执行时将定时器的回调函数交给对应模块管理,当事件发生时再将回调函数添加到分线程的等待队列,然后当初始化代码完全执行完之后,才可能执行分线程队列也就是执行回调代码,不能同时执行 因为js是单线程的。当初始化代码执行的时间超过定时器所设定的时间时,回调代码再执行,定时器就就不准时了
image.png

事件处理机制(图)

代码分类

  • 初始化执行代码: 包含绑定dom事件监听, 设置定时器, 发送ajax请求的代码
  • 回调执行代码: 处理回调逻辑(回调函数)

某些代码必须在所有的初始化代码执行完成之后才有可能执行,叫做异步执行

js引擎执行代码的基本流程:

  • 初始化代码===>回调代码

    模型的2个重要组成部分:

  • 事件管理模块

  • 回调队列

    模型的运转流程

  • 执行初始化代码, 将事件回调函数交给对应模块管理

  • 当事件发生时, 管理模块会将回调函数及其数据添加到回调列队中
  • 只有当初始化代码执行完后(可能要一定时间), 才会遍历读取回调队列中的回调函数执行

在理解Event Loop时,要理解两句话:

  • 理解哪些语句会放入异步任务队列
  • 理解语句放入异步任务队列的时机

    容易答错的题目

    for (var i = 0; i < 3; i++) { setTimeout(function () { console.log(i); }, 1000); }
    很多人以为上面的题目,答案是0,1,2,3。其实,正确的答案是:3,3,3,3。
    分析:for 循环是同步任务,setTimeout是异步任务。for循环每次遍历的时候,遇到settimeout,就先暂留着,等同步任务全部执行完毕(此时,i已经等于3了),再执行异步任务。
    我们把上面的题目再加一行代码。最终代码如下:
    for (var i = 0; i < 3; i++) { setTimeout(function () { console.log(i); }, 1000); } console.log(i);
    如果我们约定,用箭头表示其前后的两次输出之间有 1 秒的时间间隔,而逗号表示其前后的两次输出之间的时间间隔可以忽略,代码实际运行的结果该如何描述?可能会有两种答案:

  • A. 60% 的人会描述为:3 -> 3 -> 3 -> 3,即每个 3 之间都有 1 秒的时间间隔;

  • B. 40% 的人会描述为:3 -> 3,3,3,即第 1 个 3 直接输出,1 秒之后,连续输出 3 个 3。

循环执行过程中,几乎同时设置了 3 个定时器,这些定时器都会在 1 秒之后触发,而循环完的输出是立即执行的,显而易见,正确的描述是 B。

H5 Web Workers

  • 可以让js在分线程执行
  • Worker
    var worker = new Worker(‘worker.js’);
    worker.onMessage = function(event){event.data} : 用来接收另一个线程发送过来的数据的回调
    worker.postMessage(data1) : 向另一个线程发送数据
  • 问题:
    • worker内代码不能操作DOM更新UI(因为dom和window等都在主线程里,而worker是在分线程,分线程时找不到window对象的。也就是说它的全局对象里面找不到window )
    • 不是每个浏览器都支持这个新特性
    • 不能跨域加载JS
    • 慢(其实用它的原因是它不冻结页面,不阻塞主线程。但是它执行还要有接收和发送的过程 所以相对来说它更慢一点)
  • svn版本控制
  • svn server