- 1.什么是闭包
- 2.闭包的用途
- 3.闭包的缺点
- 4.闭包实用场景
- 5. JS 有哪些数据类型?
- 6.基本数据类型和引用数据类型有什么区别?
- 7.判断数据类型的方法有哪些?
- 8.浅拷贝与深拷贝有何区别?如何实现?
- 9. let、const的区别是什么?
- 10. 什么是执行上下文和执行栈?
- 11.什么是作用域和作用域链?
- 12.作用域和执行上下文的区别是什么?
- 13.this指向的各种情况都有什么?
- 14.如何改变this指针的指向?
- 15.如何理解同步和异步?
- 16.JS是如何实现异步的?
- 17.什么是AJAX?如何实现?
- 18.实现异步的方式有哪些?
- 19.怎么理解Promise对象?
- 20.怎么理解宏任务,微任务???
- 21.实现继承的方法有哪些???
- 22.require/import之间的区别?
- 23.原型和原型链
- 24.事件委托
- 25.解释一下变量的提升
- 26. 如何理解高阶函数
- 27.如何区分声明函数和表达式函数
- 28.解释原型继承是如何工作的
能够读取其他函数内部变量的函数。 或简单理解为定义在一个函数内部的函数,内部函数持有外部函数内变量的引用。
1、读取函数内部的变量
2、让这些变量的值始终保持在内存中。不会再f1调用后被自动清除。
3、方便调用上下文的局部变量。利于代码封装。
原因:f1是f2的父函数,f2被赋给了一个全局变量,f2始终存在内存中,f2的存在依赖f1,因此f1也始终存在内存中,不会在调用结束后,被垃圾回收机制回收。
1、由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2、闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
https://juejin.cn/post/6844903619595075592
根据 JavaScript 中的变量类型传递方式,分为基本数据类型和引用数据类型两大类七种。 基本数据类型包括Undefined、Null、Boolean、Number、String、Symbol (ES6新增)六种。
引用数据类型只有Object一种,主要包括对象、数组和函数。 判断数据类型采用typeof操作符,有两种语法:
typeof 123;//语法一
const FG = 123;
typeof FG;//语法二
typeof(null) //返回 object;
null == undefined //返回true,因为undefined派生自null;
null === undefined //返回false。
(1)两者作为函数的参数进行传递时: 基本数据类型传入的是数据的副本,原数据的更改不会影响传入后的数据。 引用数据类型传入的是数据的引用地址,原数据的更改会影响传入后的数据。
(2)两者在内存中的存储位置: 基本数据类型存储在栈中。 引用数据类型在栈中存储了指针,该指针指向的数据实体存储在堆中。
(1)利用typeof可以判断数据的类型; (2)A instanceof B可以用来判断A是否为B的实例,但它不能检测 null 和 undefined; (3)B.constructor == A可以判断A是否为B的原型,但constructor检测 Object与instanceof不一样,还可以处理基本数据类型的检测。
不过函数的 constructor 是不稳定的,这个主要体现在把类的原型进行重写,在重写的过程中很有可能出现把之前的constructor给覆盖了,这样检测出来的结果就是不准确的。
(4)Object.prototype.toString.call()
Object.prototype.toString.call() 是最准确最常用的方式。
浅拷贝只复制指向某个对象的指针,而不复制对象本身。浅拷贝的实现方式有: (1)Object.assign():需注意的是目标对象只有一层的时候,是深拷贝; (2)扩展运算符; 深拷贝就是在拷贝数据的时候,将数据的所有引用结构都拷贝一份。深拷贝的实现方式有: (1)手写遍历递归赋值; (2)结合使用JSON.parse()和JSON.stringify()方法。
let与const都是只在声明所在的块级作用域内有效。
let声明的变量可以改变,值和类型都可以改变,没有限制。
const
声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。
const a ;//报错,一旦声明变量,应该立即赋值!!
const b = 2;
b = 3//报错,因为定义常量之后不能成重新赋值!!
对于复合类型的变量,如数组和对象,变量名不指向数据,而是指向数据所在的地址。const
命令只是保证变量名指向的地址不变,并不保证该地址的数据不变,所以将一个对象声明为常量必须非常小心。
const names = [];
names = [1,2,3] //出错,因为变量names指向的地址不能发生改变,应始终指向[]所在的地址!!![1,2,3]与[]不是同一个地址
//不会报错,因为names指向的地址不变,改变的只是内部数据const names = [];
names[0] = 1
names[1] = 2
names[2] = 3
如果想让定义的对象或数组的内部数据也不能够修改和改变,可以使用object.freeze(names)进行冻结,这样为对象添加新属性就不起作用。
除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
变量或函数的执行上下文,决定了它们的行为以及可以访问哪些数据。每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上(如DOM中全局上下文关联的便是window对象)。
每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个执行栈中。在函数执行完之后,执行栈会弹出该函数上下文,在其上的所有变量和函数都会被销毁,并将控制权返还给之前的执行上下文。 JS的执行流就是通过这个执行栈进行控制的。
作用域可以理解为一个独立的地盘,可以理解为标识符所能生效的范围。作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。ES6中有全局作用域、函数作用域和块级作用域三层概念。 当一个变量在当前块级作用域中未被定义时,会向父级作用域(创建该函数的那个父级作用域)寻找。如果父级仍未找到,就会再一层一层向上寻找,直到找到全局作用域为止。这种一层一层的关系,就是作用域链 。
(1)函数的执行上下文只在函数被调用时生成,而其作用域在创建时已经生成; (2)函数的作用域会包含若干个执行上下文(有可能是零个,当函数未被调用时)。
this的指向只有在调用时才能被确定,因为this是执行上下文的一部分。 (1)全局作用域中的函数:其内部this指向window:
var a = 1;
function fn(){
console.log(this.a)
}
fn() //输出1
(2)对象内部的函数:其内部this指向对象本身:
var a = 1;
var obj = {
a:2,
fn:function(){
console.log(this.a)
}
}
obj.fn() //输出2
(3)构造函数:其内部this指向生成的实例:
function createP(name,age){
this.name = name //this.name指向P
this.age = age //this.age指向P
}
var p = new createP("老李",46)
(4)由apply、call、bind改造的函数:其this指向第一个参数:
function add(c,d){
return this.a + this.b + c + d
}
var o = {a:1,b:2)
add.call(o,5,7) //输出15
(5)箭头函数:箭头函数没有自己的this,看其外层的是否有函数,如果有,外层函数的this就是内部箭头函数的this,如果没有,则this是window。
可以使用apply、call、bind方法改变this指向(并不会改变函数的作用域)。比较如下: (1)三者第一个参数都是this要指向的对象,也就是想指定的上下文,上下文就是指调用函数的那个对象(没有就指向全局window); (2)apply和bind的第二个参数都是数组,call接收多个参数并用逗号隔开; (3)apply和call只对原函数做改动,bind会返回新的函数(要生效还得再调用一次)。
同步:按照代码书写顺序一一执行处理指令的一种模式,上一段代码执行完才能执行下一段代码。
异步:可以理解为一种并行处理的方式,不必等待一个程序执行完,可以执行其它的任务。
JS之所以需要异步的原因在于JS是单线程运行的。常用的异步场景有:定时器、ajax请求、事件绑定。
JS引擎是单线程的,但又能实现异步的原因在于事件循环和任务队列体系。 事件循环: JS 会创建一个类似于 while (true) 的循环,每执行一次循环体的过程称之为 Tick。每次 Tick 的过程就是查看是否有待处理事件,如果有则取出相关事件及回调函数放入执行栈中由主线程执行。待处理的事件会存储在一个任务队列中,也就是每次 Tick 会查看任务队列中是否有需要执行的任务。 任务队列: 异步操作会将相关回调添加到任务队列中。而不同的异步操作添加到任务队列的时机也不同,如 onclick, setTimeout, ajax 处理的方式都不同,这些异步操作是由浏览器内核的 webcore 来执行的,浏览器内核包含3种 webAPI,分别是 DOM Binding、network、timer模块。 onclick 由 DOM Binding 模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中。 setTimeout 由 timer 模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。 ajax 由network 模块来处理,在网络请求完成返回之后,才将回调添加到任务队列中。 主线程: JS 只有一个线程,称之为主线程。而事件循环是主线程中执行栈里的代码执行完毕之后,才开始执行的。所以,主线程中要执行的代码时间过长,会阻塞事件循环的执行,也就会阻塞异步操作的执行。 只有当主线程中执行栈为空的时候(即同步代码执行完后),才会进行事件循环来观察要执行的事件回调,当事件循环检测到任务队列中有事件就取出相关回调放入执行栈中由主线程执行。
ajax是一种能够实现局部网页刷新的技术,可以使网页异步刷新。
ajax的实现主要包括四个步骤:
(1)创建核心对象XMLhttpRequest;
(2)利用open方法打开与服务器的连接;
(3)利用send方法发送请求;("POST"请求时,还需额外设置请求头)
(4)监听服务器响应,接收返回值。
ajax是一种能够实现局部网页刷新的技术,可以使网页异步刷新。
ajax的实现主要包括四个步骤:
(1)创建核心对象XMLhttpRequest;
(2)利用open方法打开与服务器的连接;
(3)利用send方法发送请求;("POST"请求时,还需额外设置请求头)
(4)监听服务器响应,接收返回值。
(1)回调函数模式:将需要异步执行的函数作为回调函数执行,其缺点在于处理复杂逻辑异步逻辑时,会造成回调地狱(回调嵌套层数太多,代码结构混乱);
(2)事件监听模式:采用事件驱动的思想,当某一事件发生时触发执行异步函数,其缺点在于整个代码全部得变为事件驱动模式,难以分辨主流程;
(3)发布订阅模式:当异步任务执行完成时发布消息给信号中心,其他任务通过在信号中心中订阅消息来确定自己是否开始执行;
(4)Promise(ES6):Promise对象共有三种状态pending(初始化状态)、fulfilled(成功状态)、rejected(失败状态)。
(5)async/await(ES7):基于Promise实现的异步函数;
(6)利用生成器实现。
Promise对象有如下两个特点: (1)对象的状态不受外界影响。Promise对象共有三种状态pending、fulfilled、rejected。状态值只会被异步结果决定,其他任何操作无法改变。 (2)状态一旦成型,就不会再变,且任何时候都可得到这个结果。状态值会由pending变为fulfilled或rejected,这时即为resolved。 Promise的缺点有如下三个缺点: (1)Promise一旦执行便无法被取消; (2)不可设置回调函数,其内部发生的错误无法捕获; (3)当处于pending状态时,无法得知其具体发展到了哪个阶段。 Pomise中常用的方法有: (1)Promise.prototype.then():Promise实例的状态发生改变时,会调用then内部的回调函数。then方法接受两个参数(第一个为resolved状态时时执行的回调,第一个为rejected状态时时执行的回调) (2)Promise.prototype.catch():.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
宏任务有:script(整体代码)、setTimeout、setInterval、I/O、页面渲染; 微任务有:Promise.then、Object.observe、MutationObserver。 执行顺序大致如下: 主线程任务——>宏任务——>微任务——>微任务里的宏任务——>.......——>直到任务全部完成
实现继承的方法有: (1)class+extends继承(ES6)
//类模板
class Animal {
constructor(name){
this.name = name
}
}
//继承类
class Cat extends Animal{//重点。extends方法,内部用constructor+super
constructor(name) {
super(name);
//super作为函数调用时,代表父类的构造函数
}//constructor可省略
eat(){
console.log("eating")
}
}
(2)原型继承
//类模板
function Animal(name) {
this.name = name;
}
//添加原型方法
Animal.prototype.eat = function(){
console.log("eating")
}
function Cat(furColor){
this.color = color ;
};
//继承类
Cat.prototype = new Animal()//重点:子实例的原型等于父类的实例
(3)借用构造函数继承
function Animal(name){
this.name = name
}
function Cat(){
Animal.call(this,"CatName")//重点,调用父类的call方法
}
(4)寄生组合式继承(重点)
(1)require是CommonJS语法,import是ES6语法;
(2)require只在后端服务器支持,import在高版本浏览器及Node中都可以支持;
(3)require引入的是原始导出值的复制,import则是导出值的引用;
(4)require时运行时动态加载,import是静态编译;
(5)require调用时默认不是严格模式,import则默认调用严格模式.
1、原型的概念 JavaScript的象中都包含了一个 [proto] 内部属性,这个属性所对应的就是自身的原型
2、原型链的概念 当一个对象调用自身不存在的属性/方法时,就会去自己 [proto] 关联的前辈 prototype 对象上去找,如果没找到,就会去该 prototype 原型 [proto] 关联的前辈 prototype 去找。依次类推,直到找到属性/方法或 undefined 为止。从而形成了所谓的“原型链”。
事件委托,又名事件代理。事件委托就是利用事件冒泡,就是把子元素的事件都绑定到父元素上。如果子元素阻止了事件冒泡,那么委托也就没法实现了。
变量的提升是JavaScript的默认行为,这意味着将所有变量声明移动到当前作用域的顶部,并且可以在声明之前使用变量。初始化不会被提升,提升仅作用于变量的声明。
var x = 1
console.log(x + '——' + y) // 1——undefined
var y = 2
JavaScript中的一切都是对象,包括函数。我们可以将变量作为参数传递给函数,函数也是如此。我们调用接受和或返回另一个函数称为高阶函数的函数。
// 声明函数
function hello() {
return "HELLO"
}
// 表达式函数
var h1 = function hello() {
return "HELLO"
}
两个函数将在不同的时期定义。在解析期间定义声明,在运行时定义表达式;因此,如果我们控制台打印 h1
,它将显示HELLO
。
JavaScript不是一种面向对象的友好编程语言,但它仍然使用继承的思想来实现依赖关系,并使用许多内置函数使其灵活使用。了解原型继承的工作原理将使你很好地理解JavaScript知识,从而避免概念上的误用。
最好在大脑中描绘一下JavaScript的整个机制,以了解原型继承。
https://blog.csdn.net/weixin_47258963/article/details/109458246