一、ECMAScript基础
1、JS由哪三部分组成?
ECMAScript:JS的核心内容,描述了语言的基础语法,如var、for、数据类型(数组、字符串等)
DOM,文档对象模型:DOM把整个页面规划为元素构成的文档
BOM,浏览器对象模型:对浏览器窗口进行访问和操作,如弹出/移动/关闭窗口
2、npm 是什么?
npm是Node.js的包管理工具。
3、Babel 是什么?
Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
4、JS 为什么会存在数字精度丢失的问题,以及如何进行解决?
- 原因:由于0.1和0.2在二进制表示中是无限循环小数,因此在进行浮点数运算时会引起舍入误差。产生浮点数计算精度不准确的原因: 在计算机角度,计算机算的是二进制,而不是十进制。二进制后变成了无限不循环的数,而计算机可支持浮点数的小数部分可支持到52位,所有两者相加,在转换成十进制,得到的数就不准确了,加减乘除运算原理一样。
- .解决
使用toFixed来处理小数:
210000 * 10000 * 1000 * 8.2 // 17219999999999.998 parseFloat(17219999999999.998.toFixed(12)); // 17219999999999.998,而正确结果为 17220000000000
化整数运算
该方法的主要思路就是把,小数转化为整数再进行计算。
// 这两个浮点数,转化为整数之后,相乘的结果已经超过了 MAX_SAFE_INTEGER 123456.789 * 123456.789 // 转化为 (123456789 * 123456789)/1000000,结果是 15241578750.19052
- 用第三方库
5、事件传播(三个阶段)
事件捕获:
当事件发生在 DOM 元素上时,该事件并不完全发生在那个元素上。在捕获阶段,事件从window开始,一直到触发事件的元素。
window----> document----> html----> body ---->目标元素
事件或event.target
目标阶段:
事件已达到目标元素
事件冒泡:
事件冒泡刚好与事件捕获相反,
当前元素---->body ----> html---->document ---->window
。当事件发生在DOM元素上时,该事件并不完全发生在那个元素上。在冒泡阶段,事件冒泡,或者事件发生在它的父代,祖父母,祖父母的父代,直到到达window为止。<div id="a"> <div id="b"> <div id="c"> <div id="d">哈哈哈哈哈</div> </div> </div> </div> 1、冒泡 & 捕获 事件捕获:当你触发一个元素的事件的时候,该事件从该元素的祖先元素传递下去a--->d 事件冒泡:而到达此元素之后,又会向其祖先元素传播上去,此过程为冒泡d--->a
6、JS中怎么阻止事件冒泡和默认事件?
(1)addEventListener:为元素绑定事件的方法,他接收三个参数:
第一个参数:绑定的事件名
第二个参数:执行的函数
第三个参数:false:默认,代表冒泡时绑定、true:代表捕获时绑定(2)target & currentTarget
e.target
:触发事件的元素,也就是用户实际点击或者操作的元素。e.currentTarget
:绑向添加(注册)监听事件的元素,也就是调用 addEventListener 或者 on 方法的那个元素const a = document.getElementById('a') const b = document.getElementById('b') const c = document.getElementById('c') const d = document.getElementById('d') a.addEventListener('click', (e) => { const { target, currentTarget } = e console.log(`target是${target.id}`) console.log(`currentTarget是${currentTarget.id}`) }) b.addEventListener('click', (e) => { const { target, currentTarget } = e console.log(`target是${target.id}`) console.log(`currentTarget是${currentTarget.id}`) }) c.addEventListener('click', (e) => { const { target, currentTarget } = e console.log(`target是${target.id}`) console.log(`currentTarget是${currentTarget.id}`) }) d.addEventListener('click', (e) => { const { target, currentTarget } = e console.log(`target是${target.id}`) console.log(`currentTarget是${currentTarget.id}`) }) //target是d currentTarget是d //target是d currentTarget是c //target是d currentTarget是b //target是d currentTarget是a
如何阻止事件冒泡
event.stopPropagation()
来阻止事件冒泡,
event.preventDefault()
来阻止默认事件。这是一个示例: element.addEventListener('click', function(event) { event.stopPropagation(); // 阻止事件冒泡 event.preventDefault(); // 阻止默认事件 });
链接:https://juejin.cn/post/7069569810220187678
7、事件委托
事件委托指的是把一个元素的事件委托到另外一个元素上。一般来讲,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。
8、e.target 和 e.currentTarget 有什么区别?
- e.target:触发事件的元素
- e.currentTarget:绑定事件的元素
9、什么是变量提升?
变量提升是JavaScript中的一种行为,它指的是在代码执行前,JavaScript引擎会将变量和函数声明提升到其作用域的顶部,而不是在代码中实际定义的位置。
10、什么是作用域链?
在JavaScript中,作用域链是一种用于查找和访问变量的机制。当代码在执行过程中引用一个变量时,JavaScript引擎会根据当前执行上下文的作用域链来寻找该变量。
作用域链是由多个执行上下文对象组成的链式结构。每个执行上下文对象都有一个指向其父级执行上下文的引用。当在当前执行上下文中查找变量时,如果当前执行上下文中没有该变量,JavaScript引擎会沿着作用域链向上查找,直到找到该变量或到达全局执行上下文。
11、script 标签的 defer 与 async 属性⭐⭐⭐⭐/JS延迟加载的方式
(1)延迟加载的方式
JavaScript会阻塞DOM的解析,因此也就会阻塞DOM的加载。所以有时候我们希望延迟JS的加载来提高页面的加载速度。
- 把JS放在页面的最底部
- script标签的defer属性:脚本会立即下载但延迟到整个页面加载完毕再执行。该属性对于内联脚本无作用 (即没有 「src」 属性的脚本)。
- Async是在外部JS加载完成后,浏览器空闲时,Load事件触发前执行,标记为async的脚本并不保证按照指定他们的先后顺序执行,该属性对于内联脚本无作用 (即没有 「src」 属性的脚本)。
- 动态创建script标签,监听dom加载完毕再引入js文件
(2) defer 与 async 属性区别:
defer
- 不阻塞浏览器解析 HTML,等解析完 HTML 之后,才会执行 script。
- 会并行下载 JavaScript 资源。
- 会按照 HTML 中的相对顺序执行脚本。
- 会在脚本下载并执行完成之后,才会触发 DOMContentLoaded 事件。
- 在脚本执行过程中,一定可以获取到 HTML 中已有的元素。
- defer 属性对模块脚本无效。
- 适用于:所有外部脚本(通过 src 引用的 script)
async
- 不阻塞浏览器解析 HTML,但是 script 下载完成后,会立即中断浏览器解析 HTML,并执行此 script。
- 会并行下载 JavaScript 资源。
- 互相独立,谁先下载完,谁先执行,没有固定的先后顺序,不可控。
- 由于没有确定的执行时机,所以在脚本里面可能会获取不到 HTML 中已有的元素。
- DOMContentLoaded 事件和 script 脚本无相关性,无法确定他们的先后顺序。
- 适用于:独立的第三方脚本。
- 另外:async 和 defer 之间最大的区别在于它们的执行时机。
原文链接:https://blog.youkuaiyun.com/mrlmx/article/details/127581208
【精选】script 标签的 defer 与 async 属性_script标签的defer和async属性_mrlmx的博客-优快云博客
12、说说JS的运行机制
考点:事件循环 =执行栈+微任务队列+宏任务队列(promise对象,箭头函数包裹,resolve状态值的改变,链式调用)
JavaScript运行机制
(1)同步异步
(2)单线程:
(3)事件循环:运行栈+任务队列
从上往下执行,将同步代码----->运行栈,异步代码----->任务队列;
先执行同步发布代码,之后会把任务队列满足条件的微任务执行完,然后在执行宏任务
注意:Promise内部的代码为同步执行(promise 构造函数-->同步,.then为微任务)
微任务:Promise.then
宏任务 :setTimeout、ajax、文件读取
(4) 异步种类:宏任务(3种)和微任务(1种)
(5)promise和aysnc
Promise内部的代码为同步执行(promise 构造函数-->同步,.then为微任务)
aysnc:返回promise对象resolve携带的参数
aysnc+await:await后面加一个promise对象,可以直接拿到resolve参数值
// 异步和DOM渲染 // 假设HTML有一个按钮id为btn,经过以下操作最终会变成什么颜色? document.getElementById('btn').style = 'background: blue'; document.getElementById('btn').style = 'background: red'; Promise.resolve().then(() => { document.getElementById('btn').style = 'background: black'; }) // 明确UI渲染是宏任务,而按照事件循环的模型,先执行的是整体的主干代码,这期间style频繁变化但是还没有被渲染所以不会有颜色的变化 // ,同时因为有个微任务也就是promise回调函数,最终改变了style为black,之后才执行的UI渲染,也就是最后被改变的颜色了。 // 知识点:Javascript // JS执行会阻塞渲染 // 异步和 块级作用域 for(var i = 0; i < 5; i++){ setTimeout(function(){ console.log(i); }, 1000 * i); } // setTimeout()是一个异步函数,由于js会先执行所有同步任务,再执行异步任务, // 所以当开始执行setTimeout()异步任务时,for循环早已结束, // 并且由var声明的变量i不具有块级作用域的特点,当for循环结束时,i值为5, // 故再执行setTimeout()函数时,输出结果为5 5 5 5 5 // 将for循环的var换成let后,输出就会变成0 1 2 3 4
推荐阅读探索JavaScript执行机制
13.call与apply、bind的区别?
实际上call与apply的功能是相同的,只是两者的传参方式不一样,而bind传参方式与call相同,但它不会立即执行,而是返回这个改变了this指向的函数。
二、JS数据类型及特点
2.1、JS有哪些数据类型以及其存储方式
(1)JS数据类型
基本类型:number、string、boolean、undefined、null、symbol、bigint(包含7种类型)
引用类型:object(函数,普通对象,数组,正则,日期,Math数学都属于Object。)
(2)储存方式
基本数据类型和引用数据类型它们在内存中的存储方式不同。
基本数据类型是直接存储在栈中的简单数据段,占据空间小,属于被频繁使用的数据。
引用数据类型是存储在堆内存中,占据空间大。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址,当解释器寻找引用值时,会检索其在栈中的地址,取得地址后从堆中获得实体。基本数据类型保存在栈内存中,保存的是一个具体的值
引用数据类型,保存在堆内存中,保存的是引用数据类型的地址
2.2 什么是BigInt?
BigInt是一种新的数据类型,用于当整数值大于Number数据类型支持的范围时。
typeof 1n // 'bigint' typeof 1 // 'number'
2.3 NaN 是什么,用 typeof 会输出什么?
NaN:Not a Number,一个数值类型,但是不是一个具体的数字。
typeof NaN //'number'
2.4 null 与 undefined的异同
相同点:
- Undefined 和 Null 都是基本数据类型
不同点:
null 代表的含义是空对象;undefined 代表的含义是未定义。
null转为数值时为0;undefined转为数值时为NaN。
typeof null 返回'object';typeof undefined 返回'undefined'
null == undefined // true,null === undefined // false
使用场景:
2.5 typeof null为什么是‘object’ / null是对象吗?
其实 null 不是对象,虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
2.6 JS有哪些内置对象?
JS里有一句话叫万物皆对象,内置对象是指这个语言自带的一些对象,供开发者使用,这些对象提供了一些常用的或是最基本而必要的功,如属性和方法。
- 对象的属性用来反映该对象某些特定的性质,如:字符串的长度、图像的长宽等;
- 对象的方法是能够在对象上执行的动作,如:表单的“提交”(submit),时间的“获取”(getYear)等
Math: abs()、sqr()、max()、min()
Date: new Date() 、geyYear()
Array:
String: concat() 、length() 、slice()、split()
Number:
Boolen:
Object:
RegExp
Function
参考链接1:https://blog.youkuaiyun.com/weixin_43606158/article/details/94912023
2.7、 如何判断一个对象是不是空对象?
1、判断对象的键数组长度是否为0
Object.keys(obj).length === 0
2、转化成JSON格式,判断是否为“{}”
JSON.stringify(obj) === '{}'
2.8、 new调用构造函数创建的实例对象
// 题一 J原始值类型和对象类型。new 调用构造函数
// 原始值类型包括 number, string, boolean, null 和 undefined;
// 对象类型即 object。首先原始值类型它就不是对象。
// 要注意 'hello' 和 new String('hello') 的区别,前者是字符串字面值,属于原始类型,而后者是对象。// 用 typeof 运算符返回的值也是完全不一样的:
typeof 'hello'; // 'string' //字符串字面值
typeof new String('hello'); // 'object' //String 对象
// 之所以很多人分不清字符串字面值和 String 对象,归根结底就是 JS 的语法对你们太过纵容了。
// 当执行 'hello'.length 时,发现可以意料之中的返回 5,你们就觉得 'hello' 就是 String 对象,不然它怎么会有 String 对象的属性。
// 其实,这是由于 JS 在执行到这条语句的时候,内部将 'hello' 包装成了一个 String 对象,执行完后,再把这个对象丢弃了,
// 这种语法叫做 “装箱”,在其他面向对象语言里也有(如 C#)。不要认为 JS 帮你装箱了,你就可以在写代码的时候不分箱里箱外了!
// 链接:https://www.nowcoder.com/exam/test/74619983/submission?pid=53206715
例题一:
var x = new Boolean(false);//new出来的都是对象,用boolean判断时,为true
if (x) {
alert('hi');
}
例题二:
var y = Boolean(0);//没有new,进行类型转化,成false( 0,-0,NaN,"",null,undefined)
if (y) {
alert('hello');
}
2.9 对象的引用
(1)连等式
let a = { n: 1 };
let b = a;
a.x = a = { n: 2 };
console.log(a);
console.log(a.x);
console.log(b);
console.log(b.x);
// { n: 2 }
// undefined
// { n: 1, x: { n: 2 } }
// { n: 2 }
(2)对象重新赋值和引用
let obj1 = {name: 'Alice'};
let obj2 = obj1;
obj2.age = '18';//obj2['name'] = 'Bob';
console.log(obj1);
console.log(obj2);
obj2 = {
name:'ss'
};
console.log(obj1);
console.log(obj2);
// { name: 'Alice', age: '18' }
// { name: 'Alice', age: '18' }
// { name: 'Alice', age: '18' }
// { name: 'ss' }
2.10 对象的属性名(字符串或symbol)不可重复性
(1) 对象属性名:不可重复、自动转换成字符串(字符串或者symbol)
let obj = {}, b = '1', c = 1 obj[b] = 'b'; obj[c] = 'c' console.log(obj[b])//c // // 由于b是一个字符串,它被保留为'1',而c是一个数字,它被转换为字符串'1'。 // // 因此,obj[b]和obj[c]实际上是同一个属性,最后一个赋值'c'会覆盖之前的赋值'b'。 // // 所以最后console.log(obj[b])的结果是'c'。
(2) symbol:唯一作用,作为对象的属性名
- 创建symbol对象时不需要new操作符
- symbol对象不能用于数据运算,包括+、-、*、/等;
- Reflect.ownKeys(obj)可遍历对象包括symbol在内的自身可枚举属性
// symbol的唯一作用,作为对象的属性名 var s = Symbol('key'); //...①创建symbol对象不需要用new // console.log(s + '123'); //...②symbol对象只能作为对象的属性名,防止属性名被覆盖,不能做运算[ Cannot convert a Symbol value to a string] var obj = { "a":1, [s]:function(){console.log(1);} //...③sybmol作为属性名时,必须使用[] } var a=Object.keys(obj)//[ 'a' ] var b = Reflect.ownKeys(obj); // [ 'a', Symbol(key) ] //...④Reflect.ownKeys()可以遍历对象的属性,包括symbol属性 console.log(a,b) // Symbol是不完整的构造函数,创建symbol对象时不需要new操作符,①式不会抛出异常,A选项不符合题意; // symbol对象不能用于数据运算,包括+、-、*、/等,②式会抛出异常,B选项符合题意; // symbol对象的唯一作用是作为对象的属性名,这样可以防止属性因重名而覆盖,使用时必须用[], // ③式不会抛出异常,C选项不符合题意;使用Reflect.ownKeys()可以遍历对象的属性,包括symbol属性, // ④式不会抛出异常,D选项不符合题意。
(3)生成新的 Symbol的两种方式:Symbol.for()与Symbol()
- Symbol.for()会先检查给定的key是否已经存在如果不存在才会新建一个值。比如,调用Symbol.for("cat")30 次,每次都会返回同一个 Symbol 值,
- 但是调用Symbol("cat")30 次,会返回 30 个不同的 Symbol 值。
Symbol.for("bar") === Symbol.for("bar")// true//实际上是同一个值 Symbol("bar") === Symbol("bar")// false
Symbol.keyFor()方法返回一个已登记的 Symbol 类型值的key。
let s1 = Symbol.for("foo"); Symbol.keyFor(s1) // "foo" let s2 = Symbol("foo"); Symbol.keyFor(s2) // undefined // // 上面代码中,变量s2属于未登记的 Symbol 值,所以返回undefined。 // // 需要注意的是,Symbol.for()为 Symbol 值登记的名字,是全局环境的,可以在不同的 iframe 或 service worker 中取到同一个值。
2.11、 对象的可枚举性
for ...in ...:可以把原型对象身上的属性遍历出来
Object.keys():遍历自身可枚举属性
defineProperty为对象设置属性后,默认情况下,使用Object.defineProperty()添加的属性是不可写、不可枚举和不可配置的; 即该属性的描述符writable、configurable以及enumberable默认为false。
1)属性描述对象:
JavaScript 提供了一个内部数据结构,
用来描述对象的属性,控制它的行为,比如该属性是否可写、可遍历等等。
这个内部数据结构称为“属性描述对象”(attributes object)
Object.defineProperty()是JavaScript中的一个方法,它可以直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,并返回此对象。
2)这个方法接收三个参数:
obj:需要定义属性的对象。
prop:需要定义或修改的属性的键,可以是一个字符串或Symbol。
descriptor:要定义或修改的属性的描述符。
3)属性描述对象的6个元属性:value:设置该属性的属性值,默认值为undefined
writable:表示能否修改属性的值,也就是该属性是可写的还是只读,默认为false(不可写) enumerable:表示改属性是否可遍历,默认为false(不可遍历)
configurable:表示能否通过 delete 删除属性、能否修改属性的特性,或者将属性修改为访问器属性,默认为false(不可配置)
get:get是一个函数,表示该属性的取值函数(getter),默认为undefined
set:get是一个函数,表示该属性的存值函数(setter),默认为undefined
var obj = {brand:'华为',price:1999};
console.log(Object.keys(obj),Object.keys(obj).length);// ...①
Object.defineProperty(obj,'id',{value:1})
Object.defineProperty(obj,'price',{configurable:false})
console.log(Object.keys(obj),Object.keys(obj).length);// ...①
for (var k in obj){
console.log(obj[k]);// ...②
}
obj.price = 999;
delete obj['price']
console.log(obj); //...③
// [ 'brand', 'price' ] 2 // [ 'brand', 'price' ] 2
// 华为
// 1999
// { brand: '华为', price: 999 }
2.12 、Map和Object区别
(1) 键的类型: 对于Object来说,键必须是字符串或者Symbol。Map的键可以是任意类型,包括函数、对象、基本类型。
Object
let obj = {};
let key = function() {};// 设置对象的key为函数
obj[key] = 'Object';
console.log(obj,obj[key]); // { 'function() {}': 'Object' } Object
Map:set和get
let map = new Map();
let key = function() {};// 设置对象的key为函数
//(1)添加key和value值
map.set('key', 'Map');
//(2)根据key获取value
console.log(map,map.get(key)); //Map(1) { [Function: key] => 'Map' } Map
//(3)是否存在key,存在返回true,反之为false
map.has('Amy') //false
//(4)删除 key为key的value
map.delete('key')
(2)键的顺序:在Map中,元素的顺序是插入的顺序。但是在Object中,字符串键被排序,而Symbol键则保持插入的顺序。
let obj = {
'2': 'a',
'3': 'b',
'1': 'c'
};
let map = new Map([
['2', 'a'],
['3', 'b'],
['1', 'c']
]);
for(let key in obj){console.log(key)}// 输出 ['1', '2', '3']
console.log(Object.keys(obj)); // 输出 ['1', '2', '3']
console.log(map.keys()); // 输出[Map Iterator] { '2', '3', '1' }
(3)性能:在频繁增删键值对的情况下,Map有更好的性能。
(4)遍历:Map直接提供了迭代器遍历(Map.keys(), Map.values(), Map.entries()),而Object需要先将键名或键值放在一个数组中。
对象键、值、键值对的获取:
let obj = { 'a': 1, 'b': 2, 'c': 3 };
obj["name"]="李四"
console.log(Object.entries(obj))//返回键值对数组,[ [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ], [ 'name', '李四' ] ]
console.log(Object.keys(obj))//返回键数组,[ 'a', 'b', 'c', 'name' ]
console.log(Object.values(obj))//返回值数组,[ 1, 2, 3, '李四' ]
map键、值、键值对的获取:
let map = new Map(Object.entries(obj));
map.set("name", "张三");
console.log(map)// Map(4) { 'a' => 1, 'b' => 2, 'c' => 3, 'name' => '张三' }
console.log(map.entries())//返回值迭代器对象, [Map Entries] { [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ], [ 'name', '张三' ] }
console.log([...map.entries()])//[ [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ], [ 'name', '张三' ] ]
console.log(map.keys())//返回键值对迭代器对象, [Map Iterator] { 'a', 'b', 'c', 'name' }
console.log(map.values())//返回键迭代器对象, [Map Iterator] { 1, 2, 3, '张三' }
object和map遍历键值对的方式:
Object.keys(obj).forEach(key => console.log(key, obj[key])); // 对象需要获取键数组进行遍历//a 1;b 2;c 3;name 李四
for (let [key, value] of map) {
console.log(key, value); // Map可以直接进行遍历//a 1;b 2;c 3;name 张三
}
(5)大小:可以直接获取Map的长度(size),但是要获取Object的长度(size)需要将键转化为数组,再获取长度。
let obj = { 'a': 1, 'b': 2, 'c': 3 };
let map = new Map(Object.entries(obj));
let objSize = Object.keys(obj).length; // 获取对象的键的数量
let mapSize = map.size; // 获取Map的大小
console.log(objSize); // 输出 3
console.log(mapSize); // 输出 3
(6)应用场景:当需要存储大量的键值对,且键值对会经常修改或删除时,Map显得更优秀。当只需要存储少量的键值对,且键是简单类型时,Object更有优势。
2.13、Map和Set的区别
(1)具有极快的查找速度
(2) 初始化需要的值不一样,Map需要的是一个二维数组,而Set 需要的是一维 Array 数组
(3) Map 和 Set 都不允许键重复
(4) Map的键是不能修改,但是键对应的值是可以修改的;Set不能通过迭代器来改变Set的值,因为Set的值就是键。
(5) Map 是键值对的存在,值也不作为健;而 Set 没有 value 只有 key,value 就是 key;
Set常用语法如下
//初始化一个Set ,需要一个Array数组,要么空Set
var set = new Set([1,2,3,5,6])
console.log(set) // {1, 2, 3, 5, 6}
//添加元素到Set中
set.add(7) //{1, 2, 3, 5, 6, 7}
//删除Set中的元素
set.delete(3) // {1, 2, 5, 6, 7}
//检测是否含有此元素,有为true,没有则为false
set.has(2) //true
三、JS对数据类型的检测方式有哪些?各自的优缺点?
3.1 typeof根据二进制判断基本数据类型
- 判断基本数据类型,除了判断null会输出"object",其它都是正确的
- 判断引用数据类型时,除了判断函数会输出"function",其它都是输出"object"
console.log(`0是: ${typeof(0)}`) // 0:正确判断为number console.log(`'0'是: ${typeof('0')}`) //‘0’:正确判断为string console.log(`[0]是: ${typeof([0])}`) // [0]:判断为object console.log('{0}是:'+typeof({0:0})) // {0:0}:判断为object
(1)typeof null为什么是‘object’
其实 null 不是对象,虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
(2) typeof 是否能正确判断类型?
- 对于原始类型来说,除了 null 都可以调用typeof显示正确的类型。
- 但对于引用数据类型,除了函数之外,都会显示"object"。
3.2 instaceof⭐⭐⭐⭐⭐
根据原型链判断引用数据类型,无法判断基本数据类型
原理:检测右侧构造函数的
prototype
属性是否在左侧实例对象的原型链上console.log(6 instanceof Number); // false console.log(true instanceof Boolean); // false console.log('nanjiu' instanceof String); // false console.log([] instanceof Array); // true console.log(function(){} instanceof Function); // true console.log({} instanceof Object); // true
instanceof原理⭐⭐⭐⭐⭐
function myInstance(L, R) {//L代表instanceof左边,R代表右边 var RP = R.prototype var LP = L.__proto__ while (true) { if(LP == null) { return false } if(LP == RP) { return true } LP = LP.__proto__ } } console.log(myInstance({},Object));
3.3 constructor 根据构造器判断,几乎可以判断所有数据类型;
原理:当一个函数被定义时,JS引擎会为函数添加
prototype
属性,然后在prototype
属性上添加一个constructor
属性,并让其指向该函数当执行
let f = new F()
时,F被当成了构造函数,f是F的实例对象,此时F原型上的constructor属性传递到了f上,所以f.constructor===F
如果声明了一个构造函数,并把它的原型指向了Array,则无法判断
new Number(1).constructor === Number //true (1).constructor === Number true.constructor === Boolean //true ''.constructor === String // true new Function().constructor === Function // true new Date().constructor === Date // true [].constructor === Array
⚠️注意:
- null和undefined是无效的对象,所以他们不会有constructor属性
- 函数的construct是不稳定的,主要是因为开发者可以重写prototype,原有的construction引用会丢失,constructor会默认为Objec
function F(){} F.prototype = {} let f = new F() f.constructor === F // false console.log(f.constructor) //function Object(){..} 为什么会变成Object? 因为prototype被重新赋值的是一个{},{}是new Object()的字面量,因此 new Object() 会将 Object 原型上的 constructor 传递给 { },也就是 Object 本身。 因此,为了规范开发,在重写对象原型时一般都需要重新给 constructor 赋值,以保证对象实例的类型不被篡改。
3.4 Object.prototype.toString.call()完美解决方案
对于Object.prototype.toString()方法,如果对象的toString()方法未被重写,就会返回一个形如[object XXX]的字符串。但是,大多数对象,toString()方法都是重写了的,这时,需要用call()
toString()方法未被重写,就会返回一个形如[object XXX]的字符串1
var opt=Object.prototype.toString
console.log(opt.call(0))
console.log(opt.call([0]))
console.log(opt.call({0:0}))
console.log(opt.call(true))
Object.prototype.toString.call('') ; // [object String] Object.prototype.toString.call(0) ; // [object Number] Object.prototype.toString.call(true) ; // [object Boolean] Object.prototype.toString.call(Symbol()); //[object Symbol] Object.prototype.toString.call(undefined) ; // [object Undefined] Object.prototype.toString.call(null) ; // [object Null] Object.prototype.toString.call(new Function()) ; // [object Function] Object.prototype.toString.call(new Date()) ; // [object Date] Object.prototype.toString.call([]) ; // [object Array] Object.prototype.toString.call(new RegExp()) ; // [object RegExp] Object.prototype.toString.call(new Error()) ; // [object Error] Object.prototype.toString.call(document) ; // [object HTMLDocument] Object.prototype.toString.call(window) ; //[object global] window 是全局对
3.5 如何判断一个值是否是数组类型/如何区分数组和对象?
- Array.isArray
- instanceof
- Object.prototypeOf.toString.call()
- Object.prototype.isPrototypeOf
- Object.getPrototypeOf
判断变量是不是数组 var arr = [1,2,3]; 方式一:isArray console.log( Array.isArray( arr ) ); 方式二:instanceof console.log( arr instanceof Array ); 方式三:原型prototype console.log( Object.prototype.toString.call(arr).indexOf('Array') > -1 ); 方式四:isPrototypeOf() console.log( Array.prototype.isPrototypeOf(arr) ) 方式五:constructor var arr = [1,2,3]; console.log( arr.constructor.toString().indexOf('Array') > -1 )
四 、数据比较方式,Object.is和“==”、“===”有什么区别?
4.1 == (类型不同时会发生隐式类型转换)
-
先检查两个操作数的数据类型,如果相同,就进行 === 的比较,无需进行类型转换
转换遵循一些特定的规则:
(1)**null和undefined是相等的**:一个操作数是 null 或者 undefined ,那么另一个操作数必须是 null 或者 undefined 才会返回 true ,否则返回 false 。
null == undefined; // true(2,) null == 0; // false (2,) '' == null; // false(2,)
(2) **数字和字符串**:将字符串转换为 number。
'55' == 55; //true ( 4,55==55 ) '' == 0; // true( 4,0==0 ) "wise" == 3; //false (4, NaN==3? )
(3)**NaN**:`NaN`不等于任何值,包括它自己,即`NaN == NaN`会返回`false`。NaN == NaN; //false (NaN非数字)
(4)**布尔值和非布尔值**:一个操作数是 boolean 类型(`true`转换为1,`false`转换为0)
true == 'saa' //false( 5, 1== 'saa'? ) 0 == false; // true(5, 0== 0 ) ''==false//(5,''==0? 0==0) 1 == true; // true( 1== 1 )
(5)**非同个Symbol **:Symbol类型的值是唯一的,没有任何两个Symbol值是相等的,返回 false
let sym1 = Symbol("hello"); let sym2 = Symbol("hello"); console.log(sym1 == sym2); // 输出:false console.log(sym1 === sym2); // 输出:false let sym = Symbol("hello"); let symAlias = sym; console.log(sym == symAlias); // 输出:true console.log(sym === symAlias); // 输出:true
(6)**对象与对象**:创建新Object时,会在内存中创建一个新的空间来存储。两对象的内容完全相同,但内存中地址是不同的。比较它们是不是同一个对象,若指向同个对象,则返回 true
[] == [] //false// 两数组内容完全相同,但内存中地址是不同的 {} == {} //false
(7)“对象”与“!对象”:逻辑非 (!)的优先级高于相等操作符 ( == ),![]为false,!{}为false;
另一个对象转化为数字
//逻辑非 (!) 的优先级高于相等操作符 ( == ) [] == ![]; //true //[]==false;[]==0;0==0 //![]为false;false转换为0;空数组[]与数字比较时,Number([])为0 {} == !{}; //false //{}==false;{}==0;NaN==0//!{}为false;然后false转换为0;Number({})为NaN;
(8)**数组、对象和函数在与其他基本数据类型**:通过调用toString()将数组转换为字符串,然后再转换为相应的数据类型
[] == 0; //true ([].toString() -->''; Number('') -> 0 )//(5,7,‘’==1,0==0) [1]==1; // true ["1"]==1; // true [1,2]==1; // false [1,2,3] == '1,2,3' // true //(8,'[1,2,3].toString()== '1,2,3' ) [true]==1; // false //[true]转换为字符串"true", Number(['true'])是NaN [] == true; //false Number(['a']) // NaN;
(9)对象与对象/非对象经典
let a = [1,2,3] let b = [1,2,3] let c = "1,2,3" a == b // false a == c // true b == c // true
a == b
:比较数组a和数组b是否相等。由于==
运算符比较的是两个对象的引用,而不是它们的值,所以即使a和b包含相同的元素,它们也被认为是不相等的,因为它们指向的是内存中的两个不同位置。因此,a == b
的结果是false。a == c
和b == c
:比较数组a(或b)和字符串c是否相等。由于==
运算符会进行类型转换,所以在比较数组和字符串时,数组会被转换为字符串。在JavaScript中,当数组转换为字符串时,数组的元素会被转换为字符串并用逗号连接。因此,数组[1,2,3]
转换为字符串后的结果是"1,2,3",这与字符串c的值相同。因此,a == c
和b == c
的结果都是true。
4.2 === (代码严格相同,类型、值相等为true;不会发生类型转换)
是严格比较运算符,不做类型转换,类型不同就是不等。
(1)两个值都是数值,并且是同一个值,那么相等;
(2)有NaN,那么不相等;
(3)两个值都是同样的字符串或同样的Boolean值,那么相等;
(4)两个值都是null,或者都是undefined,那么相等。
(5)两个值都引用同一个对象或函数,那么相等,即两个对象的物理地址也必须保持一致;
// 案例 1:评估结果和使用 === 相同 Object.is(25, 25); // true Object.is("foo", "foo"); // true Object.is("foo", "bar"); // false Object.is(null, null); // true Object.is(undefined, undefined); // true Object.is(window, window); // true Object.is([], []); // false const foo = { a: 1 }; const bar = { a: 1 }; const sameFoo = foo; Object.is(foo, foo); // true Object.is(foo, bar); // false Object.is(foo, sameFoo); // true
4.3 Object.is()
是ES6新增的用来比较两个值是否严格相等的方法,其行为与===基本一致,不过有两处不同:+0不等于-0;NaN等于自身。
// Object.is() 与 == 运算符并不等价。 == 运算符在测试相等性之前,会对两个操作数进行类型转换(如果它们不是相同的类型), // 这可能会导致一些非预期的行为,例如 "" == false 的结果是 true, // 但是 Object.is() 不会对其操作数进行类型转换。 // Object.is() 也不等价于 === 运算符。Object.is() 和 === 之间的唯一区别在于它们处理带符号的 0 和 NaN 值的时候。 // === 运算符(和 == 运算符)将数值 -0 和 +0 视为相等,但是会将 NaN 视为彼此不相等。 Object.is(3,3.0) //true Object.is(3,"3") //false
(1)例如+0 === -0 // true
Object.is(+0, -0) // false// 案例 2: 带符号的 0 Object.is(0, -0); // false Object.is(+0, -0); // false Object.is(-0, -0); // true
(2)例如NaN === NaN // false
Object.is(NaN, NaN) // true// 案例 3: NaN Object.is(NaN, 0 / 0); // true Object.is(NaN, Number.NaN); // true '1' == 1 // true '1' === 1 // false NaN == NaN //false +0 == -0 //true +0 === -0 // true Object.is(+0,-0) //false Object.is(NaN,NaN) //true
五、 【深拷贝 浅拷贝】引用数据类型的几种深拷贝方式?
深拷贝是指完全拷贝一份新的对象,会在堆内存中开辟新的空间,当拷贝对象被修改后,原对象不受影响
5.1 ...obj 扩展运算符:实现第一层深拷贝
5.2 JSON:JSON字符串化,然后解析成JS对象;
缺点:该方法无法拷贝内部函数
5.3 Object.assign(target, ...sources):用于对象合并,第一层为深拷贝
object.assign()主要用于对象合并,将源对象中的属性复制到目标对象中,他将返回目标对象。
如果有同名属性的话,后面的属性值会覆盖前面的属性值,
如果有多个源对象,没有同名的属性会直接复制到目标对象上,还可以进行对象的深浅拷贝,
当对象中只有一级属性,没有二级属性的时候,此方法为深拷贝,但是对象中有对象的时候,此方法,在二级属性以后就是浅拷贝。
var object1 = { a: 1, b: { c: 2, d: 3 } }; var object2 = Object.assign({}, object1); var object3=Object.assign({},object1) object2.a = 10; //第一层深拷贝 object2.b.c = 20; //第二层浅拷贝 console.log(object1); // { a: 1, b: { c: 20, d: 3 } } console.log(object2) //{ a: 10, b: { c: 20, d: 3} } console.log(object3) //{ a: 1, b: { c: 20, d: 3} }
5.4 递归函数:完美解决方案
// 数组、对象的深拷贝
function deepCopy(obj) {
//判断基本数据类型或者null,返回( typeof)
if (typeof obj !== "object" || obj === null) {
return obj;
}
// 创建新的对象或数组(Array.isArray)
const newObj = Array.isArray(obj) ? [] : {};
// 递归深拷贝每个属性值
for (let key in obj) {
//只遍历对象本身的属性,不包括继承的属性
if (obj.hasOwnProperty(key)) {
//属性值obj[key]是否是对象,若是递归;若否,赋值
newObj[key] =
typeof obj[key] === "object" ? deepCopy(obj[key]) : obj[key];
}
}
return newObj;
}
六、js数据类型转换
在JavaScript中类型转换有三种情况:
- 转换为数字(调用Number(),parseInt(),parseFloat()方法)
- 转换为字符串(调用.toString()或String()方法)
- 转换为布尔值(调用Boolean()方法)
(1)隐式: JavaScript 在运行时自动将一种类型转换为另一种类型。例如,在数学运算中,JavaScript 会将字符串转换为数字
(2)显示:Number()、String() 、parseInt() 和 parseFloat()
6.1 转换为数字
-
Number():
可以把任意值转换成数字,如果要转换的字符串中有不是数字的值,则会返回NaN
字符串
Number('1') // 1
Number('123s') // NaN
布尔值
Number(true) // 1
Number(!NaN) // 1;
Number(!null) // 1;
Number(!undefined) // 1;
Number(!{}) // 0;
Number(![]) // 0;
空对象和未定义
Number(null)//0
Number(undefined)//NaN
数组
Number([])//0
Number(![]) // 0;
Number([2])// 2
Number([1,2])// NaN
对象
Number({}) //NaN
Number(!{}) // 0;
-
parseInt(string,radix):
解析一个字符串并返回指定基数的十进制整数,radix是2-36之间的整数,表示被解析字符串的基数
parseInt('2') //2
parseInt('2',10) // 2
parseInt('2',2) // NaN
parseInt('a123') // NaN 如果第一个字符不是数字或者符号就返回NaN
parseInt('123a') // 123
// 十进制转化
console.log(parseInt('123','12a3b','123b'));
// 123 12 123(default base-10)
console.log(parseInt('123', 10));
// 123 (explicitly specify base-10)
console.log(parseInt(' 123 '));
// 123 (whitespace is ignored)
console.log(parseInt('077'));
// 77 (leading zeros are ignored)
console.log(parseInt('1.9'));
// 1 (decimal part is truncated)
console.log(parseInt('xyz'));
// NaN (input can't be converted to an integer)
// 十六进制
console.log(parseInt('ff', 16));
// 255 (lower-case hexadecimal)
console.log(parseInt('0xFF', 16));
// 255 (upper-case hexadecimal with "0x" prefix)
// 以下例子均返回15:
parseInt("0xF", 16);
parseInt("F", 16);
parseInt("17", 8);
parseInt(021, 8);//十六进制超过了十五,故返回15
parseInt("015", 10);//parseInt(015, 8); 返回 13
parseInt(15.99, 10);
parseInt("15,123", 10);
parseInt("FXX123", 16);
parseInt("1111", 2);
parseInt("15 * 3", 10);
parseInt("15e2", 10);
parseInt("15px", 10);
parseInt("12", 13);
parseInt与map结合
[1,2,3].map(function(item, index, arr){
// return parseInt(item, index, arr);
return parseInt
})
parseInt(1, 0, [1,2,3])//被解析字符串的基数为0,则默认十进制,返回1
parseInt(2, 1, [1,2,3])//被解析字符串的基数为1,返回NaN
parseInt(3, 2, [1,2,3])//被解t析字符串的基数为2,3不属于二进制0或1,故排除,返回NaN
[2,3,1].map(parseInt)//[2, NaN, 1]
- parseFloat(string):解析一个参数并返回一个浮点数
parseFloat('123a')
//123
parseFloat('123a.01')
//123
parseFloat('123.01')
//123.01
parseFloat('123.01.1')
//123.01
- 隐式转换(字符串+-)
let str = '123'
let res = str - 1 //122
str+1 // '1231'
+str+1 // 124
6.2 转换为字符串
- .toString() ⚠️注意:null,undefined不能调用
Number(123).toString() //'123'
(123).toString() //'123'
数组
[].toString() //''
[1].toString() //'1'
[1,2].toString() //'1,2'
true.toString() //'true'
- String() 都能转
String(123)
//'123'
String(true)
//'true'
String([])
//''
String(null)
//'null'
String(undefined)
//'undefined'
String({})
//'[object Object]'
- 隐式转换:当+两边有一个是字符串,另一个是其它类型时,会先把其它类型转换为字符串再进行字符串拼接,返回字符串
let a = 1
a+'' // '1'
6.3 转换为布尔值
0, ''(空字符串), null, undefined, NaN会转成false,其它都是true
- Boolean()
0, ''(空字符串), null, undefined, NaN会转成false,其它都是true
Boolean('') //false
Boolean(0) //false
Boolean(null) //false
Boolean(undefined) //false
Boolean(NaN) //false
Boolean(1) //true
重点:数组和对象
Boolean({}) //true
Boolean([]) //tru
- 条件语句
let a
if(a) {
//... //这里a为undefined,会转为false,所以该条件语句内部不会执行
}
let b=[]//let b={}
if(b){
//...//这里的b为数组或对象,会转为true,所以该条件语句内部一定执行
}
- 隐式转换 !!
let str = '111'
console.log(!!str) // true
6.4 valueOf和toString
-
valueOf:返回指定对象的原始值,通常内部自动调用.
num = new Number(5);console.log(num.valueOf()); // 输出 5
var str = new String('Hello');console.log(str.valueOf()); // 输出 'Hello'
var bool = new Boolean(false);console.log(bool.valueOf()); // 输出 false
// // 日期对象的 toString() 和 valueOf() 方法:
var date = new Date();console.log(date.valueOf()); // 输出一个时间戳```
数组和对象的valueOf------->本身
[].valueOf() //[]
[1,2,3].valueOf() // [1, 2, 3]
- toString:返回一个表示对象的字符串。默认情况下,
toString()
方法被每个Object
对象继承。如果此方法在自定义对象中未被覆盖,toString()
返回 "[object type]",其中type
是对象的类型。
({}).toString() //'[object Object]'
{"a":1,"b":2}..toString()); // 输出: [object Object]
[].toString() //''
[1,2,3].toString() //'1,2,3'
// // 日期对象的 toString() 和 valueOf() 方法:
const date = new Date();
console.log(date.toString()); // 输出: "Wed Sep 29 2021 19:20:26 GMT+0800 (中国标准时间)"
console.log(date.valueOf()); // 输出: 1632907226445 (时间戳)
七、变量声明和函数声明
7.1、let, const, var的区别?
- 变量提升:let,const定义的变量不会出现变量提升,而var会
- 块级作用域:let,const 是块作用域,即其在整个大括号 {} 之内可见,var:只有全局作用域和函数作用域概念,没有块级作用域的概念。
- 重复声明:同一作用域下let,const声明的变量不允许重复声明,而var可以
- 暂时性死区:let,const声明的变量不能在声明之前使用,而var可以
- const 声明的是一个只读的常量,不允许重新赋值,可以修改属性值
7.2 JavaScript为什么要进行变量提升,它导致了什么问题?
- 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间
- 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行
- 函数是一等公民,当函数声明与变量声明冲突时,变量提升时函数优先级更高,会忽略同名的变量声明
7.3 函数和变量声明提升 + 函数作用域 + 作用域链
在JavaScript中,变量声明和函数声明都会被提升,但是函数声明的优先级高于变量声明。也就是说,如果在同一个作用域中同时有变量声明和函数声明,那么函数声明会被提升到变量声明的前面。这是因为JavaScript的编译器在解析代码时,会先处理函数声明,然后再处理变量声明。
需要注意的是,只有用var声明的变量和用function声明的函数才会被提升,用let或const声明的变量以及函数表达式(即非function开头的函数声明)都不会被提升。
作用域:
简单来说,作用域是指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限。
函数内部可以直接读取全局变量,但是,函数外部无法读取函数内部声明的变量。
作用域链:
当可执行代码内部访问变量时,会先查找当前作用域下有无该变量,有则立即返回,没有的话则会去父级作用域中查找...一直找到全局作用域。我们把这种作用域的嵌套机制称为
作用域链
考点一:var变量声明和function函数声明提升
console.log(v1);
var v1 = 100;
function foo() {
console.log(v1);
var v1 = 200;
console.log(v1);
}
foo();
console.log(v1);
//undefined
//undefined
//200
//100
// //相当于
// function foo() {
// var v1
// console.log(v1); //undefined
// v1 = 200;
// console.log(v1); //200
// }
// var v1;
// //函数和变量声明提升,但是函数还没有执行,从下面开始执行
// console.log(v1); //undefined
// v1 = 100;
// foo()//调用函数再去执行函数里面的内容,函数里面先后打印undefined 200
// console.log(v1); //100
考点二:连等赋值
(1)var a=b=10;等价于:
var a;
a=10;
b=10;(b没有用var定义)
fn(); console.log(c); console.log(b); console.log(a); function fn() { var a = b = c = 9;//相当于:var a;c=9;b=9;a=9;//a是局部变量//其他未声明,为全局变量 console.log(a); console.log(b); console.log(c); } //9 //9 //9 //9 //9 //Uncaught ReferenceError: a is not defined
(2)var a=10,b=10,c=10;等价于:
var a=10;
var b=10;
var c=10;
考点三:块级作用域和函数作用域的区别
(1) let/const块级作用域中,同名变量视为不同的两个变量
// 由于let/const声明的变量具有块级作用域 // 在花括号内部的a和外部的a虽然同名,但实际上是两个不同的变量。 let a=1 //var a = 1; { let a = 2; console.log(a); // 输出:2 } console.log(a); // 输出:1
(2) var声明的变量没有块级作用域,同名变量视为重复声明(覆盖)
// // 有坑,var 声明的变量没有块级作用域 var a = 1; { var a = 2; console.log(a); // 输出:2 }//这里不是函数,没有函数作用域,var可以重复声明 console.log(a); // 输出:2
考点四:立即执行函数(函数作用域和块级作用域的区别)
(1)非块级作用域中,var声明的变量提升
var name = 'a'; (function(){ console.log("此处的name:",name,",数据类型",typeof name) if( typeof name == 'undefined' ){ var name = 'b';//块级作用域:var声明的变量没有块级作用域,变量提升到函数内顶部 console.log('111'+name); }else{ console.log('222'+name); } })() console.log(name)// 函数作用域:局部变量不影响全局变量 // 此处的name: undefined ,数据类型 undefined // 111b // a
(2)let具有块级作用域中,声明的变量不提升
var name = 'a'; (function(){ console.log("此处的name:",name,",数据类型",typeof name)//函数找不到变量name,到上一层寻找 if( typeof name == 'undefined' ){ let name = 'b';//let声明的变量有块级作用域,变量不提升 console.log('111'+name); }else{ console.log('222'+name); } })() console.log(name)// 函数局部变量不影响全局变量 // 此处的name: a ,数据类型 string // 222a // a
(3)全局变量,作用域链
var name = 'a'; (function(){ console.log("此处的name:",name,",数据类型",typeof name)//函数找不到变量name,到上一层寻找 if( typeof name == 'undefined' ){ // var name = 'b';//var声明的变量没有块级作用域,变量提升到函数内顶部 console.log('111'+name); }else{ console.log('222'+name); } })() console.log(name)// 函数局部变量不影响全局变量 // 此处的name: a ,数据类型 string // 222a // a
考点五: 同名变量和函数声明的覆盖(同名变量和函数声明提升)
(1) 同名function声明函数覆盖
var m = 1,
j = (k = 0);
function add(n) {
return (n = n + 1);
}
y = add(m);
function add(n) {
return (n = n + 3);
}
z = add(m);
console.log(y);
console.log(z);
// //以上代码相当于
// function add(n) {
// return (n = n + 1);
// }
// function add(n) {
// return (n = n + 3);
// }
// var m = 1,
// j = (k = 0);
// y = add(m);
// z = add(m);
// console.log(y);
// console.log(z);
// //输出4 4
(2)函数提升优先级高于变量提升,且不会被同名变量声明时覆盖,但会被同名变量赋值后覆盖
函数被同名变量赋值后覆盖
// 函数提升----》同名变量赋值覆盖
// 函数提升优先级高于变量提升,且不会被同名变量声明时覆盖,但是会被同名变量赋值后覆盖
var a = 100;//被同名变量赋值后覆盖
function a() {
// var a = 200;
console.log(a);
}
a();//TypeError: a is not a function
function a() {
var a = 200;
console.log(a);
}
a();//200
function a() {
console.log(a);
var a = 200;
}
a();//undefined
函数不被未赋值的同名变量声明覆盖
function a() {
console.log(a);
}
a();
var a = 100;//被同名变量赋值后覆盖
//ƒ a() {
// console.log(a);
//}
function a() {
console.log(a);
}
a();
//ƒ a() {
// console.log(a);
//}
(3)函数表达式视为变量声明提升
(function() {
var x = foo();
var foo = function foo() {
return "foobar"
};
return x;
})();
// Uncaught TypeError: foo is not a function
//解析
// (function() {
// var x;
// var foo;
// x = foo();//foo不是函数,变量值为undefined
// foo = function foo() { //看成是一个函数赋值给变量foo,只有函数声明会提升
// return "foobar"
// };
// return x;
// })();
考点六:函数表达式和函数声明的优先级
a(); // 2
var a = function(){ // 看成是一个函数赋值给变量a
console.log(1)
}
a(); // 1
function a(){
console.log(2)
}
a(); // 1
// 解析:
// function a(){ // 函数提升
// console.log(2)
// }
// var a; // 变量提升
// a(); // 2
// a = function(){ // 变量a赋值后覆盖上面的函数a
// console.log(1)
// }
// a(); // 1
// a(); // 1
考点七:函数作为参数【作用域链/上下文环境】
var a = 1
function fn() {
function fn3(){
console.log(a)
}
fn3()
};
function fn2(f) {
var a = 2
f()
};
fn2(fn)//1
newbing解析:
在JavaScript中,函数可以访问它们自己的作用域(局部作用域)以及任何包含它们的作用域(外部作用域)中的变量。这就是所谓的作用域链。
当我们尝试访问一个变量时,JavaScript引擎首先会在当前作用域中查找该变量。如果在当前作用域中找不到该变量,它就会去上一级作用域中查找,依此类推,直到找到该变量或者到达全局作用域。如果在全局作用域中仍然找不到该变量,JavaScript引擎就会报错,说该变量未定义。
fn3
函数在fn
函数的作用域中定义,因此它可以访问fn
函数的作用域以及全局作用域中的变量。当fn3
函数尝试访问变量a
时,由于在fn
函数的作用域中找不到变量a
,所以它会去全局作用域中查找,而在全局作用域中,变量a
的值被设置为1。虽然
fn
函数是在fn2
函数的作用域中被调用的,但是这并不改变fn
函数和fn3
函数的作用域链。换句话说,即使fn
函数是在fn2
函数的作用域中被调用的,fn3
函数也无法访问fn2
函数的作用域中的变量。这就是为什么fn3
函数打印的是全局作用域中的变量a
的值,而不是fn2
函数作用域中的变量a
的值。
var a = 1
function fn() {
var a=3
function fn3(){
console.log(a)
}
fn3()
};
function fn2(f) {
var a = 2
fn()
};
fn2(fn)//3//作用域链
八、JS遍历方式
8.1 数组遍历方式
// 一、数组遍历方法
// (1)基本for循环,使用索引访问数组元素。
// let arr = [1,3,9]
for(let i =0;i<arr.length;i++){
console.log(arr[i])
}
// (2)for in:适用于遍历可枚举的属性(包括数组和对象),但不推荐用于数组遍历,因为它会遍历到原型链上的属性,而且遍历的顺序是不确定的。
for(let index in arr){
console.log(index+ ":"+arr[index])
}
// (3)for of:用于遍历可迭代对象(例如数组、字符串、Map、Set 等),直接访问数组元素。
for (let item of arr) {
console.log(item);
}
// 接受一个回调函数,遍历数组中的每个元素,向回调函数传递元素的值、索引和数组本身。
// (4)forEach
// 对数组的每一个元素执行一次提供的函数(不能使用return、break等中断循环),不改变原数组,无返回值undefined。
arr.forEach((element ,index,arr)=> {
console.log(element,index,arr)
});
// (5)map数组遍历方法
// 作用类似于forEach,但创建一个新数组,将回调函数的返回值作为新数组的元素,不改变原数组。
let newArr =arr.map((element ,index,arr)=>{
return index+"-----"+ element*2+"-----"+arr
})
console.log("map",newArr)
// (6) fliter数组遍历方法
// 过滤器: 回调函数针对每个元素都会运行。如果回调函数返回 true,那么元素就会被放入新数组,不会改变原数组
let newArr1 =arr.filter((element,index,arr)=>{
console.log("Element:", element,"Index:", index,"Array:", arr);
return element%3===0
})
console.log("fliter",newArr1)
forEach中return有效果吗?如何中断forEach循环?
在forEach中用return不会返回,函数会继续执行。
官方推荐方法(替换方法):用every和some替代forEach函数。
(1) every在碰到return false的时候,中止循环
(2) some在碰到return true的时候,中止循环
(3) 使用 for…of 函数
8.2 对象遍历方式
对象遍历方法
(1) for(let key in obj) 遍历 --------> obj.hasOwnProperty(key) / Object.hasOwn(obj,key)过滤
(2) Object.keys(obj).forEach( (item)=>{} )
8.4 字符串遍历方法
JavaScript遍历字符串_js循环字符串_allway2的博客-优快云博客
(1)for(let i=0;i<str.length;i++)
(2) for(let i in str) 接收索引遍历
(3)for(let char of str) { } 接收字符
遍历方式总结
// for…in
// 循环遍历的值都是数据结构的键值:key--->对象的键,数组的下标
// 总结: for in也可以循环数组但是特别适合遍历对象
let arr = [1,3,9,[2,3]]
let obj = {a: '1', b: '2', c: '3', d: '4'}
for(let key in obj){
console.log("for…in遍历对象"+key+":"+obj[key])
}
for(let key in arr){
console.log("for…in遍历数组:",key+" "+arr[key])
}
// for…in遍历对象a:1// for…in遍历对象b:2
// for…in遍历对象c:3// for…in遍历对象d:4
// for…in遍历数组: 0 1// for…in遍历数组: 1 3
// for…in遍历数组: 2 9// for…in遍历数组: 3 2,3
// for…of
// ES6中新增加的语法,用来循环获取一对键值对中的值
// 一个数据结构只有部署了 Symbol.iterator 属性, 才具有 iterator接口可以使用 for of循环。
// obj对象没有Symbol.iterator属性 所以会报错。
// 数组 Array //Map //Set //String //arguments对象 // Nodelist对象, 就是获取的dom列表集合
for(let value of arr){
console.log("for…of遍历arr:",value)
}
let str="string"
for(let value of str){
console.log("for…of遍历str:",value)
}
let obj2 = {a: '1', b: '2', c: '3', d: '4'}
for(let value of Object.keys(obj2)){
console.log("for…of遍历obj2键:",value) // a,b,c,d
}
for(let value of Object.values(obj2)){
console.log("for…of遍历obj2值:",value) // a,b,c,d
}
let map1 = new Map([["a", 1], ["b", 2], ["c", 3]]);
for (let [key, value] of map1) {
console.log(value);//1 //2 // 3
}
for (let entry of map1) {
console.log(entry);
}
// [a, 1]// [b, 2]// [c, 3]
let iterable = new Set([1, 1, 2, 2, 3, 3]);
for (let value of iterable) {
console.log(value);
}
// 总结一下这些遍历方法的区别:
// for 循环是最基本的遍历方法,需要手动控制索引;
// for in 主要用于遍历对象的属性,不推荐用于数组遍历;
// for of 是 ES6 引入的新特性,适用于可迭代对象,直接访问数组元素;
// forEach 是数组的内置方法,对每个元素执行回调函数,但没有返回值;
// map 也是数组的内置方法,创建一个新数组,将回调函数的返回值作为新数组的元素,不改变原数组。
九、数组、字符高级函数
9.1 说说JavaScript数组常用方法
截取:
- slice------原数组不会被修改
对数组:
实现浅拷贝数组与截取数组,返回的是截取拷贝后的新数组,原数组不会改变
对字符串:
还可以提取某个字符串的一部分,并返回一个新的字符串,且不会改动原字符串。
const fruits = ['apple', 'banana', 'cherry', 'orange', 'grape']; const subFruits1 = fruits.slice(1, 3);console.log(subFruits1); // ["banana", "cherry"] const subFruits2 = fruits.slice(2);console.log(subFruits2); // ["cherry", "orange", "grape"] const subFruits3 = fruits.slice();console.log(subFruits3); // ["apple", "banana", "cherry", "orange", "grape"]
添加元素:
- push:末尾追加 返回值是添加数据后数组的新长度,改变原有数组
let arr = [1, 2, 3]; arr.unshift(-1,0) console.log(arr);// [ -1, 0, 1, 2, 3 ]
- unshift:开头添加 返回值是添加数据后数组的新长度,改变原有数组
let arr = [-1, 0,1, 2, 3]; arr.push(4,5) console.log(arr);// [-1, 0, 1, 2, 3, 4, 5 ]
- splice:向数组的指定index处插入 返回的是被删除掉的元素的集合,会改变原有数组
const fruits = ['apple', 'banana', 'cherry', 'orange', 'grape']; fruits.splice(2, 0, 'mango'); console.log(fruits); // ['apple', 'banana', 'mango', 'cherry', 'orange', 'grape']
删除元素:
- pop():从尾部删除一个元素 返回被删除掉的元素----------改变原有数组
- shift():从头部删除一个元素 返回被删除掉的元素----------改变原有数组
- splice:在index处删除howmany个元素 返回的是被删除掉的元素的集合----------改变原有数组
排序:
- reverse():原数组颠倒顺序,返回倒序后的数组----------修改原数组
let arr = [1, 2, 3, 4, 5]; console.log(arr.reverse() ); // [5, 4, 3, 2, 1] console.log(arr); // [5, 4, 3, 2, 1]
- sort():按指定规则排序 -------修改原数组
let arr = [3, 1, 5, 2, 4]; console.log(arr.sort()); // [1, 2, 3, 4, 5] console.log(arr); // [1, 2, 3, 4, 5] arr.sort((a, b) => b - a); // 使用比较函数进行逆序排序 console.log(arr); // [5, 4, 3, 2, 1]
迭代方法:
- every():
对数组中的每一运行给定的函数,如果该函数对每一项都返回true,则该函数返回true
var arr = [10,30,25,64,18,3,9] var result = arr.every((item,index,arr)=>{ return item>3 }) console.log(result) //false
- some() :
对数组中的每一运行给定的函数,如果该函数有一项返回true,就返回true,所有项返回false才返回false
var arr2 = [10,20,32,45,36,94,75] var result2 = arr2.some((item,index,arr)=>{ return item<10 }) console.log(result2) //false
- filter():
对数组中的每一运行给定的函数,会返回满足该函数的项组成的数组
// filter 返回满足要求的数组项组成的新数组 var arr3 = [3,6,7,12,20,64,35] var result3 = arr3.filter((item,index,arr)=>{ return item > 3 }) console.log(result3) //[6,7,12,20,64,35]
- map():
对数组中的每一元素运行给定的函数,返回每次函数调用的结果组成的数组
// map 返回每次函数调用的结果组成的数组 var arr4 = [1,2,3,4,5,6] var result4 = arr4.map((item,index,arr)=>{ return `<span>${item}</span>` }) console.log(result4) /*[ '<span>1</span>', '<span>2</span>', '<span>3</span>', '<span>4</span>', '<span>5</span>', '<span>6</span>' ]*/
- forEach():
对数组中的每一元素运行给定的函数,没有返回值,常用来遍历元素
// forEach var arr5 = [10,20,30] var result5 = arr5.forEach((item,index,arr)=>{ console.log(item) }) console.log(result5) /* 10 20 30 undefined 该方法没有返回值 */
- reduce():
reduce()
方法对数组中的每个元素执行一个由你提供的reducer函数(升序执行),将其结果汇总为单个返回值const array = [1,2,3,4] const reducer = (accumulator, currentValue) => accumulator + currentValue; // 1 + 2 + 3 + 4 console.log(array1.reduce(reducer));
9.2 Javascript字符串的常用方法有哪些?
(1)增:
concat:用于将一个或多个字符串拼接成一个新字符串。
+:字符串拼接
(2)删:
slice()
subStr()
subString()
(3)改:toLowerCase()、 toUpperCase()
trim()
repeat()
(4)查:
chatAt()
indexOf()
startWith()
includes()
(5)转换成数组:
split(),把字符串按照指定的分割符,拆分成数组中的每一项
(6)模板匹配方法
match
search
replace
slice和splice的区别,以及二者较详细的解析_一个大萝北的博客-优快云博客
十、promise
为什么常把promise放在一个函数里
创建一个promise对象,他会立即执行
const promise = new Promise((resolve, reject) => { console.log(1); resolve("-success"); console.log(2); }); promise状态变成resolve,故执行then promise.then((res) => { console.log(3 + res); }); console.log(4); 1 2 4 3-success
promise的内部原理是什么?它的优缺点?
Promise对象,封装了一个异步操作,还可以获取成功或失败的结果
Promise主要是解决回调地狱问题,之前如果异步任务比较多,同时他们之间有相互依赖的关系,就只能使用回调函数处理,就容易形成回调地狱,代码的可读性差,可维护性差,
有三种状态:pending初始状态,fufillled成功状态,rejected失败状态
状态改变的两种状态:fufillled---------->fufillled ; fufillled---------->rejected
一旦发生,状态就会凝固,无法改变
首先是我们没法子取消Promise,一旦创建就不行立即执行,不能中途取消
如果不设置回调,Promise内部抛出的错误无法反馈到外面
原理:
const promise = new Promise((resolve, reject) => { console.log(1); console.log(2); }); //promise状态仍为pending,故不执行 promise.then(() => { console.log(3); }); console.log(4); 1 2 4
const promise1 = new Promise((resolve, reject) => { console.log("promise-1"); resolve("resolve1"); }); const promise2 = promise1.then((res) => { console.log(res); }); console.log("1", promise1); console.log("2", promise2); //执行promise1里面的内容,此时状态为resolved;然后将promise2放入微任务队列中,其状态为pending //打印后面两行代码;最后执行微任务队列里面的内容 //promise-1 // 1 Promise { 'resolve1' } //2 Promise { <pending> } // resolve1
如何中断 promise?
使用 Promise.all()
Promise中,resolve后面的语句是否还会执行?
会被执行。如果不需要执行,需要在 resolve 语句前加上 return。
Promise.all 和 Promise.allSettled 有什么区别?
共同点:处理多个并行的Promise
最大的区别:Promise.allSettled永远不会被reject。
Promise.all
:这个方法接受一个Promise对象的数组作为参数,当所有的Promise对象都成功地完成(fulfilled)时,返回一个新的Promise对象。这个新的Promise对象的解析值(resolve value)是一个数组,数组中的元素是每个Promise对象的解析值。但是,如果参数中的任何一个Promise失败(rejected),Promise.all
返回的Promise对象就会立即失败,其失败的理由是第一个失败的Promise的理由。//一、成功案例 const delay = n => new Promise(resolve => setTimeout(resolve, n)); const promises = [ delay(100).then(() => 1), delay(200).then(() => 2), ] Promise.all(promises).then(values=>console.log(values)) // 最终输出: [1, 2] //二、失败案例 const promises = [ delay(100).then(() => 1), delay(200).then(() => 2), Promise.reject(3) ] Promise.all(promises).then(values=>console.log(values)) // 最终输出: Uncaught (in promise) 3 Promise.all(promises) .then(values=>console.log(values)) .catch(err=>console.log(err)) // 加入catch语句后,最终输出:3
Promise.allSettled
:这个方法也接受一个Promise对象的数组作为参数,但是与Promise.all
不同,Promise.allSettled
不关心Promise是否成功或失败。无论Promise对象是成功还是失败,Promise.allSettled
都会等待所有的Promise都完成。Promise.allSettled
返回的新的Promise对象的解析值是一个数组,数组中的每个元素都是一个对象,表示对应的Promise的状态(fulfilled或rejected)和值(解析值或拒绝理由)。const promises = [ delay(100).then(() => 1), delay(200).then(() => 2), Promise.reject(3) ] Promise.allSettled(promises).then(values=>console.log(values)) // 最终输出: // [ // {status: "fulfilled", value: 1}, // {status: "fulfilled", value: 2}, // {status: "rejected", value: 3}, // ]
作者:上线前夕来源:稀土掘金参考链接:https://juejin.cn/post/6855129005792854029
Promise与async、await有什么区别?
(1)都是处理异步请求的方式
(2)Promise是ES6语法,async和await是ES7语法
(3)async和await是基于Promise实现的,都是非阻塞的
(4)优缺点
Promise
面试题:JS微任务和宏任务
1. js是单线程的语言。
2. js代码执行流程:同步执行完==》事件循环
同步的任务都执行完了,才会执行事件循环的内容
进入事件循环:请求、定时器、事件....
3. 事件循环中包含:【微任务、宏任务】
微任务:promise.then
宏任务:setTimeout..
要执行宏任务的前提是清空了所有的微任务
流程:同步==》事件循环【微任务和宏任务】==》微任务==》宏任务=》微任务...
- Number():可以把任意值转换成数字,如果要转换的字符串中有不是数字的值,则会返回
NaN
十一、this关键字
11.1 var和let声明的变量
var
声明的变量会被提升并附加到全局对象上,
var a="a"
window.a//a
let
声明的变量不会被附加到全局对象上
let b="b"
window.b
//undefined
11.2 this关键字的指向
JavaScript this
关键词指的是它所属的对象。
它拥有不同的值,具体取决于它的使用位置:
(1) 全局上下文或函数调用
当在全局上下文(不在任何函数内部)或者直接调用一个函数(即不通过对象或者类调用)时,`this`通常指向全局对象window。
全局定义的函数都是挂载到window对象,上面的test()相当于window.test();所以this在这里就代表了window。
案例一: console.log(this); // 在全局上下文中,this指向window对象 案例二: var name="JS"//若改为let 第二个输出为undefined function test() { let name="Python" console.log(this); // 在函数调用中,this同样指向window对象 console.log(this.name); //JS } test();//window.test();
补充:setTimeout、setInterval计时器的this指向window
(2)对象方法中
当函数作为对象的方法被调用时,`this`指向调用该方法的对象
let name="A" let obj = { name: 'Bing', sayHello: function() { let name="B" console.log('Hello, ' + this.name); } }; obj.sayHello(); // 在对象方法中,this指向调用方法的对象obj //Hello, Bing
(3) 事件中的this
当函数作为DOM事件处理函数时,`this`指向触发事件的元素
(4) 构造函数中的this [面试题new的作用]
构造函数:跟普通函数没区别,函数名首字母大写,用来创建对象
new:new会创建对象,将构造函数中this指向创建出来的对象
(5)箭头函数没有this,里面的this,指向定义函数上下文的this。
11.2、怎么改变this指向的改变
(1)使用call、apply、bind
this指向传入的对象,若后面有接收的多个参数将赋值给调用的方法sayName
(2)call、apply、bind区别
call:参数形式为字符串
apply:参数形式为数组
bind:不会立刻执行,作为返回值被接收
let dog ={ eat(food1,food2){ console.log("我爱吃:",food1,food2)} } let cat={name:'喵喵'}
(3)call、apply、bind实际应用
继承:
将父类的this指向子类,使子类可以使用父类的方法
(4)箭头函数
箭头函数没有this,里面的this,指向定义函数上下文的this。
十二、原型和原型链
12.1 构造函数
构造函数分为 实例成员 和 静态成员:
实例成员: 实例成员就是在构造函数内部,通过this添加的成员。实例成员只能通过实例化的对象来访问。
静态成员: 在构造函数本身上添加的成员,只能通过构造函数来访问
实例化:通过构造函数创建对象,该过程也称作实例化
// 定义构造函数
function Star(name,age) {
//实例成员
this.name = name;
this.age = age;
}
//静态成员
Star.sex = '男';
// 定义类的属性
Star.prototype.say = function () {
console.log("My name is", this.name);
};
//通过构造函数创建一个对象
let stars = new Star('小明',20);
console.log(stars); // Star {name: "小明", age: 20}
console.log(stars.sex); // undefined 实例无法访问sex属性
console.log(Star.name); //Star 通过构造函数无法直接访问实例成员
console.log(Star.sex); //男 通过构造函数可直接访问静态成员
stars .say();
12.2 new一个新对象的过程,发生了什么?
- 创建一个新的空对象
- 将空对象的
__proto__
指向构造函数的prototype
- 执行构造函数, 并将新创建的空对象绑定为构造函数的this对象
- 如果构造函数有返回一个对象,则返回这个对象,否则返回新创建的那个对象
// 题一 J原始值类型和对象类型。new 调用构造函数
typeof 'hello'; // 'string' //字符串字面值
typeof new String('hello'); // 'object' //String 对象
// 当执行 'hello'.length 时,由于 JS 在执行到这条语句的时候,内部将 'hello' 包装成了一个 String 对象,执行完后,再把这个对象丢弃了,
// 这种语法叫做 “装箱”,
例题一:
var x = new Boolean(false);//new出来的都是对象,用boolean判断时,为true
if (x) {
alert('hi');
}
例题二:
var y = Boolean(0);//没有new,进行类型转化,成false( 0,-0,NaN,"",null,undefined)
if (y) {
alert('hello');
}
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
toString() {
return `Person: ${this.name}, ${this.age} years old`;
}
}
const person = new Person("Alice", 30);
console.log(person); // Person {name: 'Alice', age: 30}
console.log(person.__proto__); //{constructor: ƒ, toString: ƒ}
console.log(person.toString()); // 输出: Person: Alice, 30 years old
12.2.1、模拟实现一个 new 函数
function myNew (constructorFn, ...args) {
// 1. 创建一个空对象
const obj = {};
// 2. 将空对象的__proto__指向constructor的prototype
obj.__proto__ = constructorFn.prototype;
// 3. 执行 constructor, 并将新对象绑定为 constructor 的 this 对象
const res = constructorFn.apply(obj, args);
// 4. 如果构造函数有返回值则返回res, 否则返回新对象
return typeof res === 'object' ? res : obj
}
const p1 = myNew(Person, "jack");
p1.say();
12.3 解释JavaScript中的原型(prototype)是什么?
在JavaScript中,每个函数都有一个特殊的属性叫做原型(prototype)。这个原型是一个对象,它包含了由特定类型的所有实例共享的属性和方法。也就是说,我们可以在原型对象上添加属性和方法,让所有的对象实例都可以使用。
(1)原型的规则
①所有
引用类型(
函数实例出来的对象)
都有一个__proto__(隐式原型)
属性,属性值是一个普通的对象
②所有函数
都有一个prototype(显式原型)
属性,属性值是一个普通的对象
③所有引用类型的__proto__
属性指向
它构造函数的prototype;
对象的隐式原型的值为其对应构造函数的显示原型的值④当试图得到一个对象的属性时,如果这个对象本身不存在这个属性,那么就会去它的’_ _ proto_ _'属性(也就是它的构造函数的’prototype’属性)中去寻找。
var a = [1,2,3]; a.__proto__ === Array.prototype; // true
(2)原型的作用:
1.数据共享 节约内存内存空间
2.每一个对象都会从原型中“继承”属性,实现继承(3)案例
//这是一个构造函数 function Foo(name,age){ this.name=name; this.age=age; } /*所有的函数都有一个prototype属性*/ Foo.prototype={ // prototype对象里面又有其他的属性 showName:function(){ console.log("I'm "+this.name);//this是什么要看执行的时候谁调用了这个函数 }, showAge:function(){ console.log("And I'm "+this.age); } } var fn=new Foo('小黄',18) console.log("实例对象本身:",fn) console.log("构造函数的显式原型:",fn.__proto__) /*当试图得到一个对象的属性时,如果这个对象本身不存在这个属性,那么就会去它 构造函数的'prototype'属性中去找*/ fn.showName(); //I'm 黄 fn.showAge(); //And I'm 18
console.log("实例对象本身:",fn)
console.log("构造函数的显式原型:",fn.__proto__)
12.4 什么是原型链?
(1)当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到就会再在构造函数的prototype的__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链。
(2)原型链是JavaScript中实现继承的主要方法。每个对象都有一个指向它的原型(prototype)的内部链接。这个原型对象又有自己的原型,直到某个对象的原型为null,根据定义,null没有原型,并作为这个原型链中的最后一个环节。
// 构造函数 function Foo(name,age){ this.name=name; this.age=age; } Object.prototype.toString=function(){ //this是什么要看执行的时候谁调用了这个函数。 console.log("I'm "+this.name+" And I'm "+this.age); } var fn=new Foo('小明',19); fn.toString(); //I'm 小明 And I'm 19 console.log(fn.toString===Foo.prototype.__proto__.toString); //true console.log(fn.__proto__ ===Foo.prototype)//true console.log(Foo.prototype.__proto__===Object.prototype)//true console.log(Object.prototype.__proto__===null)//true
解析:
首先,fn的构造函数是Foo()。所以:
fn._ _ proto _ _=== Foo.prototype
又因为Foo.prototype是一个普通的对象,它的构造函数是Object,所以:
Foo.prototype._ _ proto _ _=== Object.prototype
通过上面的代码,我们知道这个toString()方法是在Object.prototype里面的,当调用这个对象的本身并不存在的方法时,它会一层一层地往上去找,一直到null为止。
所以当fn调用toString()时,JS发现fn中没有这个方法,于是它就去Foo.prototype中去找,发现还是没有这个方法,然后就去Object.prototype中去找,找到了,就调用Object.prototype中的toString()方法。
这就是原型链,fn能够调用Object.prototype中的方法正是因为存在原型链的机制。另外,在使用原型的时候,一般推荐将需要扩展的方法写在构造函数的prototype属性中,避免写在_ _ proto _ _属性里面。
12.5 如何创建原型链?
使用new操作符或者Object.create方法来创建一个新对象,新对象会自动关联到构造函数的原型对象上,从而创建出一条原型链。
12.5 原型链有什么缺点?
原型链的主要问题是引用类型的值。原型链上的所有实例共享这些属性,一旦有一个实例改变了这个属性,其他实例的这个属性也会跟着改变。
12.6 如何实现继承?
原型链、借用构造函数、组合继承、原型式继承和寄生式继承等。每种方式都有其优点和缺点,需要根据具体情况来选择。
12.7 编写一个函数,检查一个对象是否存在于另一个对象的原型链上。
九、说说什么是模块化开发?
9.1 CommonJS
CommonJS规范是Node.js的主要实践者,它是服务器端的模块加载方案。由于服务器端的文件都存储在本地磁盘,读取速度非常快,因此它采用同步的方式来引入模块。但在浏览器端,由于模块的加载需要通过网络请求,因此异步加载的方式更加合适。
// 定义模块a.js var x= '前端'; function say(name, age) { console.log(`我是${name},今年${age}岁`); } module.exports = { //在这里写上需要向外暴露的函数、变量 say: say, x: x } // 引用自定义的模块时,参数包含路径,可省略.js var a = require('./a'); a.say('小白', 18); //
9.2 AMD与require.js
AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
9.3 CMD与sea.js
CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。
9.4 ES6 Module
其模块功能主要由两个命令构成:export和import。
export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
// 定义模块a.js var title = '前端'; function say(name, age) { console.log(`我是${name},今年${age}岁`); } export { //在这里写上需要向外暴露的函数、变量 say, title } // 引用自定义的模块时,参数包含路径,可省略.js import {say,title} from "./a" say('小白', 18);
9.5 CommonJS 与 ES6 Module 的差异
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。而ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。
CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
推荐阅读前端模块化理解
read://https_juejin.cn/?url=https%3A%2F%2Fjuejin.cn%2Fpost%2F7049164339630047245
十、闭包
10.1 说说你对闭包的理解,以及它的原理和应用场景?
一个函数和对其周围(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样一个组合就是闭包(「closure」)
闭包原理
函数执行分成两个阶段(预编译阶段和执行阶段)。
- 在预编译阶段,如果发现内部函数使用了外部函数的变量,则会在内存中创建一个“闭包”对象并保存对应变量值,如果已存在“闭包”,则只需要增加对应属性值即可。
- 执行完后,函数执行上下文会被销毁,函数对“闭包”对象的引用也会被销毁,但其内部函数还持用该“闭包”的引用,所以内部函数可以继续使用“外部函数”中的变量
利用了函数作用域链的特性,一个函数内部定义的函数会将包含外部函数的活动对象添加到它的作用域链中,函数执行完毕,其执行作用域链销毁,但因内部函数的作用域链仍然在引用这个活动对象,所以其活动对象不会被销毁,直到内部函数被烧毁后才被销毁。
优点
- 可以从内部函数访问外部函数的作用域中的变量,且访问到的变量长期驻扎在内存中,可供之后使用
- 避免变量污染全局
- 把变量存到独立的作用域,作为私有成员存在
缺点
- 对内存消耗有负面影响。因内部函数保存了对外部变量的引用,导致无法被垃圾回收,增大内存使用量,所以使用不当会导致内存泄漏
- 对处理速度具有负面影响。闭包的层级决定了引用的外部变量在查找时经过的作用域链长度
- 可能获取到意外的值(captured value)
应用场景
- 模块封装,防止变量污染全局
var Person = (function(){
var name = '南玖'
function Person() {
console.log('work for qtt')
}
Person.prototype.work = function() {}
return Person
})()
- 循环体中创建闭包,保存变量
for(var i=0;i<5;i++){
(function(j){
setTimeOut(() => {
console.log(j)
},1000)
})(i)
}
18.说说你了解哪些前端本地存储?
防抖节流
防抖定义:
如果一个事件被频繁地触发,只让它在最后一次触发后的一段时间后再执行
而在这段时间内,如果有新的事件触发,则重新计时。这个函数经常用于减少连续频繁的函数调用
场景:
1.搜索框搜索输入。只需用户最后一次输入完,再发送请求
2.手机号、邮箱验证输入检测
3.窗口大小resize。只需窗口调整完成后,计算窗口大小。防止重复渲染
// 防抖的简单版本: function debounce(func, delay) { let timer = null; return function () { clearTimeout(timer); //每次新函数被调用,先清除上一次的定时器,然后设置新的定时器,从而达到防抖的效果。 timer = setTimeout(() => { func.apply(this, arguments); //更改fn的this指向并传入参数的,保证fn的行为和直接调用时一样。 }, delay); }; }
节流(throttle):每次触发定时器后,直到这个定时器结束之前无法再次触发。不管在这个中间有多少次触发这个事件,执行函数的频率总是固定的;
一般用于可预知的用户行为的优化,比如为scroll事件的回调函数添加定时器
鼠标移动事件 王者荣耀攻击键, 点击再快也是以一定攻速(频率)进行攻击等等
主要算出每次点击后剩余的时间
时间差 = 当前时间 - 上次执行时间
剩余时间 = 时间间隔 - 时间差 const remainTime = interval - (nowTime - lastTime) 判断当剩余时间 <= 0 时执行函数
// 节流(throttle):每次触发定时器后,直到这个定时器结束之前无法再次触发。不管在这个中间有多少次触发这个事件,执行函数的频率总是固定的; // 一般用于可预知的用户行为的优化,比如为scroll事件的回调函数添加定时器 // 鼠标移动事件 王者荣耀攻击键, 点击再快也是以一定攻速(频率)进行攻击等等 function throttle(fn, delay) { let timer = null; return function () { if (timer) { return; } timer = setTimeout(() => { fn.apply(this, [...arguments]); }, delay); }; }