1. new操作符的实现原理
- 创建一个对象
- 将构造函数的作用域赋给新对象(也就是将对象的__proto__属性指向构造函数的prototype属性)
- 指向构造函数中的代码,构造函数中的this指向该对象(也就是为这个对象添加属性和方法)
- 返回新的对象
(1)首先创建了一个新的空对象
(2)设置原型,将对象的原型设置为函数的 prototype 对象。
(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
function Fun(age, name) {
this.age = age;
this.name = name;
return 111; //返回源对象
// return [1, 23, 3]; //返回[1,23,3]
}
// new操作符的作用
function create(fn, ...args) {
// 1.创建一个空对象
var obj = {}; //var obj = Object.create({})
// 2.将空对象的原型,指向于构造函数的原型
Object.setPrototypeOf(obj, fn.prototype);
// 3.将空对象作为构造函数的上下文(改变this指向)
var result = fn.apply(obj, args);
// 4.对构造函数的返回值的·1处理判断
return result instanceof Object ? result : obj;
}
console.log(create(Fun, 18, "张三"));
// new操作符
// function Person(age, name) {
// this.age = age;
// this.name = name;
// }
// var p = new Person(18, "John");
// console.log(p);
<script>
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
const car1 = new Car("Eagle", "Talon TSi", 1993);
console.log(car1); //整个Car对象
function Person() {
this.name = "小明";
this.fn = function () {
console.log(`名字是${this.name}`);
};
}
let person1 = new Person();
console.log(person1.name); //小明
person1.fn(); //名字是小明
// new 关键字会进行如下的操作:
// 1.创建一个空的简单 JavaScript 对象(即 {});
// 2.为步骤 1 新创建的对象添加属性 __proto__,将该属性链接至构造函数的原型对象;
// 3.将步骤 1 新创建的对象作为 this 的上下文;
// 4.如果该函数没有返回对象,则返回 this。
// 人话 原理
// 1.创建一个空对象
let obj = new Person ();
// 2.链接到原型
obj.__proto__ = Person.prototype;
// 3.绑定this,执行一次构造函数 新创建的对象作为 this 的上下文;
let result = Person.call(obj);
// 返回一个对象
if (typeof result === "object") {
person1 = result;
} else {
person1 = obj;
}
console.log(obj.name); // 小明
</script>
</script>
function objectFactory() {
let newObject = null;
let constructor = Array.prototype.shift.call(arguments);
let result = null;
// 判断参数是否是一个函数
if (typeof constructor !== "function") {
console.error("type error");
return;
}
// 新建一个空对象,对象的原型为构造函数的 prototype 对象
newObject = Object.create(constructor.prototype);
// 将 this 指向新建对象,并执行函数
result = constructor.apply(newObject, arguments);
// 判断返回对象
let flag = result && (typeof result === "object" || typeof result === "function");
// 判断返回结果
return flag ? result : newObject;
}
// 使用方法
objectFactory(构造函数, 初始化参数);
2. map和Object的区别
键的顺序注意点
- object需要手动计算(Object.keys(obj).length)
let obj = {
name: "贝吉塔",
age: 18,
friend: {
name: "卡卡罗特",
age: 20,
},
};
const length = Object.keys(obj).length;
console.log(length); //3
- JS中Map的遍历
1.forEach遍历
先输出value,再输出key
const map = new Map([['key1','v1'],['key2','v2'],['key3','v3']]);
console.log(map);
map.forEach((val,key) => {
console.log(val,key);
})
/**
v1 key1
v2 key2
v3 key3
*/
for…of…
输出数组格式
for(let i of map) {
console.log(i);
}
/**
[ 'key1', 'v1' ]
[ 'key2', 'v2' ]
[ 'key3', 'v3' ]
*/
for…of…搭配数组解构
//搭配解构赋值
for(let [key,val] of map) {
console.log(key,val);
}
/**
key1 v1
key2 v2
key3 v3
*/
3. map和weakMap的区别
(1)Map
map本质上就是键值对的集合,但是普通的Object中的键值对中的键只能是字符串。而ES6提供的Map数据结构类似于对象,但是它的键不限制范围,可以是任意类型,是一种更加完善的Hash结构。如果Map的键是一个原始数据类型,只要两个键严格相同,就视为是同一个键。
实际上Map是一个数组,它的每一个数据也都是一个数组,其形式如下:
const map = [
["name","张三"],
["age",18],
]
const map = new Map([
["name", 18],
["name", "张三"],
]);
const value = map.get("name");
const num = map.size;
console.log(value, num); // 张三 1
Map数据结构有以下操作方法:
● size: map.size 返回Map结构的成员总数。
● set(key,value):设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用)
● get(key):该方法读取key对应的键值,如果找不到key,返回undefined。
● has(key):该方法返回一个布尔值,表示某个键是否在当前Map对象中。
● delete(key):该方法删除某个键,返回true,如果删除失败,返回false。
● clear():map.clear()清除所有成员,没有返回值。
Map结构原生提供是三个遍历器生成函数和一个遍历方法
● keys():返回键名的遍历器。
● values():返回键值的遍历器。
● entries():返回所有成员的遍历器。
● forEach():遍历Map的所有成员。
const map = new Map([
["foo", 1],
["bar", 2],
]);
for (let key of map.keys()) {
console.log(key); // foo bar
}
for (let value of map.values()) {
console.log(value); // 1 2
}
for (let items of map.entries()) {
console.log(items); // ["foo",1] ["bar",2]
}
map.forEach((value, key, map) => {
console.log(key, value); // foo 1 bar 2
});
for (let [key, val] of map) {
console.log(key, val); // foo 1 bar 2
}
(2)WeakMap
WeakMap 对象也是一组键值对的集合,其中的键是弱引用的。其键必须是对象,原始数据类型不能作为key值,而值可以是任意的。
弱引用:WeakMap是弱引用,map是强引用 强引用指向的不会被GC垃圾回收 如果只用弱引用的引用,也会被垃圾回收
const finalRegistry = new FinalizationRegistry((value) => {
console.log("注册在finalRegistry的对象, 某一个被销毁", value);
});
let obj = { name: "why" };
const map = new WeakMap(); //会销毁 ,会调用函数
// const map = new Map(); //不会销毁 ,不会调用函数
map.set(obj, "aaa");
// WeakRef创建一个弱引用 GC不管弱引用有没有指向,依然会被销毁
finalRegistry.register(obj, "obj");
obj = null;
该对象也有以下几种方法:
● set(key,value):设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用)
● get(key):该方法读取key对应的键值,如果找不到key,返回undefined。
● has(key):该方法返回一个布尔值,表示某个键是否在当前Map对象中。
● delete(key):该方法删除某个键,返回true,如果删除失败,返回false。
其clear()方法已经被弃用,所以可以通过创建一个空的WeakMap并替换原对象来实现清除。
WeakMap的设计目的在于,有时想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,就必须手动删除这个引用,否则垃圾回收机制就不会释放对象占用的内存。
而WeakMap的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
const obj = { name: "obj1" };
// 1.WeakMap和Map的区别二:
// 1.WeakMap是弱引用,map是强引用 强引用指向的不会被GC垃圾回收 如果只用弱引用的引用,也会被垃圾回收
//强引用
const map = new Map();
map.set(obj, "aaa");
//弱引用
const weakMap = new WeakMap();
weakMap.set(obj, "aaa");
总结:
● Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
● WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。但是 WeakMap 只接受对象作为键名( null 除外),不接受其他类型的值作为键名。而且 WeakMap 的键名所指向的对象,不计入垃圾回收机制。
4. JavaScript有哪些内置对象
全局的对象( global objects )或称标准内置对象,不要和 “全局对象(global object)” 混淆。这里说的全局的对象是说在
全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。
标准内置对象的分类:
(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。
例如 Infinity、NaN、undefined、null 字面量
(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。
例如 eval()、parseFloat()、parseInt() 等
(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。
例如 Object、Function、Boolean、Symbol、Error 等
(4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。
例如 Number、Math、Date
(5)字符串,用来表示和操作字符串的对象。
例如 String、RegExp
(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。例如 Array
(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。
例如 Map、Set、WeakMap、WeakSet
(8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。
例如 SIMD 等
(9)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。
例如 JSON 等
(10)控制抽象对象
例如 Promise、Generator 等
(11)反射
例如 Reflect、Proxy
(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。
例如 Intl、Intl.Collator 等
(13)WebAssembly
(14)其他
例如 arguments
总结:
js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js 定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。
5. 常用的正则表达式有哪些?
// (1)匹配 16 进制颜色值
var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;
// (2)匹配日期,如 yyyy-mm-dd 格式
var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
// (3)匹配 qq 号
var regex = /^[1-9][0-9]{4,10}$/g;
// (4)手机号码正则
var regex = /^1[34578]\d{9}$/g;
// (5)用户名正则
var regex = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;
6. 对JSON的理解
JSON 是一种基于文本的轻量级的数据交换格式。它可以被任何的编程语言读取和作为数据格式来传递。
在项目开发中,使用 JSON 作为前后端数据交换的方式。在前端通过将一个符合 JSON 格式的数据结构序列化为
JSON 字符串,然后将它传递到后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递。
因为 JSON 的语法是基于 js 的,因此很容易将 JSON 和 js 中的对象弄混,但是应该注意的是 JSON 和 js 中的对象不是一回事,JSON 中对象格式更加严格,比如说在 JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等,因此大多数的 js 对象是不符合 JSON 对象的格式的。
在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理,
● JSON.stringify 函数,通过传入一个符合 JSON 格式的数据结构,将其转换为一个 JSON 字符串。如果传入的数据结构不符合 JSON 格式,那么在序列化的时候会对这些值进行对应的特殊处理,使其符合规范。在前端向后端发送数据时,可以调用这个函数将数据对象转化为 JSON 格式的字符串。
JSON.stringify({}); // '{}'
JSON.stringify(true); // 'true'
JSON.stringify("foo"); // '"foo"'
JSON.stringify([1, "false", false]); // '[1,"false",false]'
JSON.stringify({ x: 5 }); // '{"x":5}'
JSON.stringify({x: 5, y: 6});
// "{"x":5,"y":6}"
JSON.stringify([new Number(1), new String("false"), new Boolean(false)]);
// '[1,"false",false]'
JSON.stringify({x: undefined, y: Object, z: Symbol("")});
// '{}'
JSON.stringify([undefined, Object, Symbol("")]);
// '[null,null,null]'
JSON.stringify({[Symbol("foo")]: "foo"});
// '{}'
JSON.stringify({[Symbol.for("foo")]: "foo"}, [Symbol.for("foo")]);
// '{}'
JSON.stringify(
{[Symbol.for("foo")]: "foo"},
function (k, v) {
if (typeof k === "symbol"){
return "a symbol";
}
}
);
// undefined
// 不可枚举的属性默认会被忽略:
JSON.stringify(
Object.create(
null,
{
x: { value: 'x', enumerable: false },
y: { value: 'y', enumerable: true }
}
)
);
// "{"y":"y"}"
● JSON.parse() 函数,这个函数用来将 JSON 格式的字符串转换为一个 js 数据结构,如果传入的字符串不是标准的 JSON 格式的字符串的话,将会抛出错误。当从后端接收到 JSON 格式的字符串时,可以通过这个方法来将其解析为一个 js 数据结构,以此来进行数据的访问。
JSON.parse('{}'); // {}
JSON.parse('true'); // true
JSON.parse('"foo"'); // "foo"
JSON.parse('[1, 5, "false"]'); // [1, 5, "false"]
JSON.parse('null'); // null
7. JavaScript脚本延迟加载的方式有哪些?
延迟加载就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度。
一般有以下几种方式:
● defer 属性:给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
● async 属性:给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
● 动态创建 DOM 方式:动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
● 使用 setTimeout 延迟方法:设置一个定时器来延迟加载js脚本文件
● 让 JS 最后加载:将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行
8. JavaScript 类数组对象的定义?
类数组对象
一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有 arguments 和 DOM 方法的返回结果,还有一个函数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数。
组转换为数组中数组方法写在prototype.数组方法名(push,slice,splice…).call等(类数组)
常见的类数组转换为数组的方法有这样几种:
(1)通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);
(2)通过 call 调用数组的 splice 方法来实现转换
Array.prototype.splice.call(arrayLike, 0);
(3)通过 apply 调用数组的 concat 方法来实现转换
Array.prototype.concat.apply([], arrayLike);
(4)通过 Array.from 方法来实现转换
Array.from(arrayLike);
(5).es6以后的方法
[...arrayLike]
类数组应用场景
var arrayLike = {
0: "java",
1: "script",
length: 2,
};
// 格式 Array.prototype.数组方法.call(类数组,对应数组方法的参数...)
Array.prototype.push.call(arrayLike, "jack", "lily");
console.log(typeof arrayLike); // 'object'
console.log(arrayLike);
// {0: "java", 1: "script", 2: "jack", 3: "lily", length: 4}
9. 数组有哪些原生方法?
js数组方法解析
详细必看
(1)数组和字符串的转换方式:toString( )、toLocaleString()、join( )
① toString( ):将数组转换成一个字符串
② toLocaleString( ):返回一个数组元素的字符串
③ join( ):将数组用标识符连接成字符串返回拼接好的字符串,不改变原数组
(2)数组尾部操作的方法:pop( )、push( )
① pop( ):删除并返回数组最后一个元素,改变原数组
② push( ):返回添加完成后的数组的长度,可以传入多个参数,改变原数组
(3)数组首部操作的方法:shift( )、unshift( )
① shift( ):移除并返回数组的第一个元素,改变原数组
② unshift( ):在数组头部插入一个元素slice
(4)数组重排序的方法:reverse( )、sort( )
① reverse( ):翻转数组,改变原数组
② sort( ):自带的数组排序方法,将元素按照字符串进行比较排序
(5)数组连接的方法:concat( ):合并数组返回的是拼接好的数组,不影响原数组
(6)数组截取的方法:slice(下标,个数 ):用于截取数组中的一部分返回数组,不影响原数组。
(7)数组插入的方法:splice( ):插入,删除或替换数组的元素,影响原数组
(8)查找特定项的索引的方法:indexOf( )、lastIndexOf( )
① indexOf( ):用于在数组中查找元素,从左向右查找,并把元素的位置返回来
② lastIndexOf( ):在数组中查找元素,但是方向是从右向左查找,并把元素的位置返回来
(9)数组迭代方法:every( )、some( )、filter( )、map( )、forEach( )
① every( ):监测数组中每个元素是否符合函数的条件,如果其中有一个不符合,则返回false
② some( ):监测数组中每个元素是否符合函数的条件,如果其中一个元素符合条件,则返回true,剩下的元素不会再监测,如果没有满足的条件,则返回false
③ filter( ):筛选数组,将满足条件的元素放入新数组中/
④ map( ):返回一个新数组,新数组的元素为原始数组中的每个元素调用函数处理后的得到的值。
⑤ forEach( ):遍历数组
(10)数组归并方法:reduce( )、reduceRight( )
① reduce( ):接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值
② reduceRight( ):与reduce( )作用相同,但是计算方向相反(从右向左)
10. Unicode、UTF-8、UTF-16、UTF-32的区别?
(1)Unicode
在说Unicode之前需要先了解一下ASCII码:ASCII 码(American Standard Code for Information Interchange)称为美国标准信息交换码。
● 它是基于拉丁字母的一套电脑编码系统。
● 它定义了一个用于代表常见字符的字典。
● 它包含了"A-Z"(包含大小写),数据"0-9" 以及一些常见的符号。
● 它是专门为英语而设计的,有128个编码,对其他语言无能为力
ASCII码可以表示的编码有限,要想表示其他语言的编码,还是要使用Unicode来表示,可以说Unicode是ASCII 的超集。
Unicode全称 Unicode Translation Format,又叫做统一码、万国码、单一码。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。
Unicode的实现方式(也就是编码方式)有很多种,常见的是UTF-8、UTF-16、UTF-32和USC-2。
(2)UTF-8
UTF-8是使用最广泛的Unicode编码方式,它是一种可变长的编码方式,可以是1—4个字节不等,它可以完全兼容ASCII码的128个字符。
注意: UTF-8 是一种编码方式,Unicode是一个字符集合。
UTF-8的编码规则:
● 对于单字节的符号,字节的第一位为0,后面的7位为这个字符的Unicode编码,因此对于英文字母,它的Unicode编码和ACSII编码一样。
● 对于n字节的符号,第一个字节的前n位都是1,第n+1位设为0,后面字节的前两位一律设为10,剩下的没有提及的二进制位,全部为这个符号的Unicode码 。
来看一下具体的Unicode编号范围与对应的UTF-8二进制格式 :
编码范围(编号对应的十进制数) 二进制格式
0x00—0x7F (0-127) 0xxxxxxx
0x80—0x7FF (128-2047) 110xxxxx 10xxxxxx
0x800—0xFFFF (2048-65535) 1110xxxx 10xxxxxx 10xxxxxx
0x10000—0x10FFFF (65536以上) 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
那该如何通过具体的Unicode编码,进行具体的UTF-8编码呢?步骤如下:
● 找到该Unicode编码的所在的编号范围,进而找到与之对应的二进制格式
● 将Unicode编码转换为二进制数(去掉最高位的0)
● 将二进制数从右往左一次填入二进制格式的X中,如果有X未填,就设为0
来看一个实际的例子:
“马” 字的Unicode编码是:0x9A6C,整数编号是39532
(1)首选确定了该字符在第三个范围内,它的格式是 1110xxxx 10xxxxxx 10xxxxxx
(2)39532对应的二进制数为1001 1010 0110 1100
(3)将二进制数填入X中,结果是:11101001 10101001 10101100
(3)UTF-16
- 平面的概念
在了解UTF-16之前,先看一下平面的概念:
Unicode编码中有很多很多的字符,它并不是一次性定义的,而是分区进行定义的,每个区存放65536(216)个字符,这称为一个平面,目前总共有17 个平面。
最前面的一个平面称为基本平面,它的码点从0 — 216-1,写成16进制就是U+0000 — U+FFFF,那剩下的16个平面就是辅助平面,码点范围是 U+10000—U+10FFFF。
2. UTF-16 概念:
UTF-16也是Unicode编码集的一种编码形式,把Unicode字符集的抽象码位映射为16位长的整数(即码元)的序列,用于数据存储或传递。Unicode字符的码位需要1个或者2个16位长的码元来表示,因此UTF-16也是用变长字节表示的。
3. UTF-16 编码规则:
● 编号在 U+0000—U+FFFF 的字符(常用字符集),直接用两个字节表示。
● 编号在 U+10000—U+10FFFF 之间的字符,需要用四个字节表示。
4. 编码识别
那么问题来了,当遇到两个字节时,怎么知道是把它当做一个字符还是和后面的两个字节一起当做一个字符呢?
UTF-16 编码肯定也考虑到了这个问题,在基本平面内,从 U+D800 — U+DFFF 是一个空段,也就是说这个区间的码点不对应任何的字符,因此这些空段就可以用来映射辅助平面的字符。
辅助平面共有 220 个字符位,因此表示这些字符至少需要 20 个二进制位。UTF-16 将这 20 个二进制位分成两半,前 10 位映射在 U+D800 — U+DBFF,称为高位(H),后 10 位映射在 U+DC00 — U+DFFF,称为低位(L)。这就相当于,将一个辅助平面的字符拆成了两个基本平面的字符来表示。
因此,当遇到两个字节时,发现它的码点在 U+D800 —U+DBFF之间,就可以知道,它后面的两个字节的码点应该在 U+DC00 — U+DFFF 之间,这四个字节必须放在一起进行解读。
5. 举例说明
以 “𡠀” 字为例,它的 Unicode 码点为 0x21800,该码点超出了基本平面的范围,因此需要用四个字节来表示,步骤如下:
● 首先计算超出部分的结果:0x21800 - 0x10000
● 将上面的计算结果转为20位的二进制数,不足20位就在前面补0,结果为:0001000110 0000000000
● 将得到的两个10位二进制数分别对应到两个区间中
● U+D800 对应的二进制数为 1101100000000000, 将0001000110填充在它的后10 个二进制位,得到 1101100001000110,转成 16 进制数为 0xD846。同理,低位为 0xDC00,所以这个字的UTF-16 编码为 0xD846 0xDC00
(4) UTF-32
UTF-32 就是字符所对应编号的整数二进制形式,每个字符占四个字节,这个是直接进行转换的。该编码方式占用的储存空间较多,所以使用较少。
比如“马” 字的Unicode编号是:U+9A6C,整数编号是39532,直接转化为二进制:1001 1010 0110 1100,这就是它的UTF-32编码。
(5)总结
Unicode、UTF-8、UTF-16、UTF-32有什么区别?
● Unicode 是编码字符集(字符集),而UTF-8、UTF-16、UTF-32是字符集编码(编码规则);
● UTF-16 使用变长码元序列的编码方式,相较于定长码元序列的UTF-32算法更复杂,甚至比同样是变长码元序列的UTF-8也更为复杂,因为其引入了独特的代理对这样的代理机制;
● UTF-8需要判断每个字节中的开头标志信息,所以如果某个字节在传送过程中出错了,就会导致后面的字节也会解析出错;而UTF-16不会判断开头标志,即使错也只会错一个字符,所以容错能力教强;
● 如果字符内容全部英文或英文与其他文字混合,但英文占绝大部分,那么用UTF-8就比UTF-16节省了很多空间;而如果字符内容全部是中文这样类似的字符或者混合字符中中文占绝大多数,那么UTF-16就占优势了,可以节省很多空间;
11. 常见的位运算符有哪些?其计算规则是什么?
现代计算机中数据都是以二进制的形式存储的,即0、1两种状态,计算机对二进制数据进行的运算加减乘除等都是叫位运算,即将符号位共同参与运算的运算。
常见的位运算有以下几种:
- 按位与运算符(&)
定义: 参加运算的两个数据按二进制位进行“与”运算。
运算规则:
0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1
- 按位与运算符(&)
定义: 参加运算的两个数据按二进制位进行“与”运算。
运算规则:
0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1
总结:两位同时为1,结果才为1,否则结果为0。
例如:3&5 即:
0000 0011
0000 0101
= 0000 0001
因此 3&5 的值为1。
注意:负数按补码形式参加按位与运算。
用途:
(1)判断奇偶
只要根据最未位是0还是1来决定,为0就是偶数,为1就是奇数。因此可以用if ((i & 1) == 0)代替if (i % 2 == 0)来判断a是不是偶数。
(2)清零
如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都为零的数值相与,结果为零。 - 按位或运算符(|)
定义: 参加运算的两个对象按二进制位进行“或”运算。
运算规则:
0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1
总结:参加运算的两个对象只要有一个为1,其值为1。
例如:3|5即:
0000 0011
0000 0101
= 0000 0111
因此,3|5的值为7。
注意:负数按补码形式参加按位或运算。 - 异或运算符(^)
定义: 参加运算的两个数据按二进制位进行“异或”运算。
运算规则:
0 ^ 0 = 0
0 ^ 1 = 1
1 ^ 0 = 1
1 ^ 1 = 0
总结:参加运算的两个对象,如果两个相应位相同为0,相异为1。
例如:3|5即:
0000 0011
0000 0101
= 0000 0110
因此,3^5的值为6。
异或运算的性质:
● 交换律:(ab)c == a(bc)
● 结合律:(a + b)^c == a^b + b^c
● 对于任何数x,都有 xx=0,x0=x
● 自反性: abb=a^0=a; - 取反运算符 (~)
定义: 参加运算的一个数据按二进制进行“取反”运算。
运算规则:
~ 1 = 0
~ 0 = 1
总结:对一个二进制数按位取反,即将0变1,1变0。
例如:~6 即:
0000 0110
= 1111 1001
在计算机中,正数用原码表示,负数使用补码存储,首先看最高位,最高位1表示负数,0表示正数。此计算机二进制码为负数,最高位为符号位。
当发现按位取反为负数时,就直接取其补码,变为十进制:
0000 0110
= 1111 1001
反码:1000 0110
补码:1000 0111
因此,~6的值为-7。
12. 为什么函数的 arguments 参数是类数组而不是数组?如何遍历类数组?
arguments是一个对象,它的属性是从 0 开始依次递增的数字,还有callee和length等属性,与数组相似;但是它却没有数组常见的方法属性,如forEach, reduce等,所以叫它们类数组。
要遍历类数组,有三个方法:
(1)将数组的方法应用到类数组上,这时候就可以使用call和apply方法,如:
function foo(){
Array.prototype.forEach.call(arguments, a => console.log(a))
}
(2)使用Array.from方法将类数组转化成数组:
function foo(){
const arrArgs = Array.from(arguments)
arrArgs.forEach(a => console.log(a))
}
(3)使用展开运算符将类数组转化成数组
function foo(){
const arrArgs = [...arguments]
arrArgs.forEach(a => console.log(a))
}
13. 什么是 DOM 和 BOM?
● DOM 指的是文档对象模型,它指的是把文档当做一个对象,这个对象主要定义了处理网页内容的方法和接口。
● BOM 指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的法和接口。BOM的核心是 window,而 window 对象具有双重角色,它既是通过 js 访问浏览器窗口的一个接口,又是一个 Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。window 对象含有 location 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对象的子对象。
DOM
BOM
14. 对类数组对象的理解,如何转化为数组
一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有 arguments 和 DOM 方法的返回结果,函数参数也可以被看作是类数组对象,因为它含有 length属性值,代表可接收的参数个数。
常见的类数组转换为数组的方法有这样几种:
通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);
通过 call 调用数组的 splice 方法来实现转换
Array.prototype.splice.call(arrayLike, 0);
通过 apply 调用数组的 concat 方法来实现转换
Array.prototype.concat.apply([], arrayLike);
通过 Array.from 方法来实现转换
Array.from(arrayLike);
15. escape、encodeURI、encodeURIComponent 的区别
● encodeURI 是对整个 URI 进行转义,将 URI 中的非法字符转换为合法字符,所以对于一些在 URI 中有特殊意义的字符不会进行转义。
● encodeURIComponent 是对 URI 的组成部分进行转义,所以一些特殊字符也会得到转义。
● escape 和 encodeURI 的作用相同,不过它们对于 unicode 编码为 0xff 之外字符的时候会有区别,escape 是直接在字符的 unicode 编码前加上 %u,而 encodeURI 首先会将字符转换为 UTF-8 的格式,再在每个字节前加上 %。
16. 对AJAX的理解,实现一个AJAX请求
AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。
创建AJAX请求的步骤:
● 创建一个 XMLHttpRequest 对象。
● 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
● 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
● 当对象的属性和监听函数设置完成后,最后调用 send 方法来向服务器发起请求,可以传入参数作为发送的数据体。
send()方法
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", url, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功时
if (this.status === 200) {
handle(this.response);
} else {
console.error(this.statusText);
}
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);
使用Promise封装AJAX:
// promise 封装实现:
function getJSON(url) {
// 创建一个 promise 对象
let promise = new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest();
// 新建一个 http 请求
xhr.open("GET", url, true);
// 设置状态的监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功或失败时,改变 promise 的状态
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
// 设置错误监听函数
xhr.onerror = function() {
reject(new Error(this.statusText));
};
// 设置响应的数据类型
xhr.responseType = "json";
// 设置请求头信息
xhr.setRequestHeader("Accept", "application/json");
// 发送 http 请求
xhr.send(null);
});
return promise;
}
17. JavaScript为什么要进行变量提升,它导致了什么问题?
变量提升的表现是,无论在函数中何处位置声明的变量,好像都被提升到了函数的首部,可以在变量声明前访问到而不会报错。
造成变量声明提升的本质原因是 js 引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象。当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。
首先要知道,JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行。
● 在解析阶段,JS会检查语法,并对函数进行预编译。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。
○ 全局上下文:变量定义,函数声明
○ 函数上下文:变量定义,函数声明,this,arguments
● 在执行阶段,就是按照代码的顺序依次执行。
那为什么会进行变量提升呢?主要有以下两个原因:
● 提高性能
● 容错性更好
(1)提高性能
在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。
在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),并且因为代码压缩的原因,代码执行也更快了。
(2)容错性更好
变量提升可以在一定程度上提高JS的容错性,看下面的代码:
a = 1;
var a;
console.log(a);
如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。
虽然,在可以开发过程中,可以完全避免这样写,但是有时代码很复杂的时候。可能因为疏忽而先使用后定义了,这样也不会影响正常使用。由于变量提升的存在,而会正常运行。
总结:
● 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间
● 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行
变量提升虽然有一些优点,但是他也会造成一定的问题,在ES6中提出了let、const来定义变量,它们就没有变量提升的机制。下面看一下变量提升可能会导致的问题:
var tmp = new Date();
function fn(){
console.log(tmp);
if(false){
var tmp = 'hello world';
}
}
fn(); // undefined
var tmp = new Date();
function fn(){
console.log(tmp);
if(false){
let tmp = 'hello world';
}
}
fn(); // new Date的时间
在这个函数中,原本是要打印出外层的tmp变量,但是因为变量提升的问题,内层定义的tmp被提到函数内部的最顶部,相当于覆盖了外层的tmp,所以打印结果为undefined。
var tmp = 'hello world';
for (var i = 0; i < tmp.length; i++) {
console.log(tmp[i]);
}
console.log(i); // 11
for (var i = 0; i < tmp.length; i++) {
console.log(tmp[i]);
}
console.log(i); // undefined
由于遍历时定义的i会变量提升成为一个全局变量,在函数结束之后不会被销毁,所以打印出来11。
18. 什么是尾调用,使用尾调用有什么好处?
尾调用指的是函数的最后一步调用另一个函数。代码执行是基于执行栈的,所以当在一个函数里调用另一个函数时,会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中。使用尾调用的话,因为已经是函数的最后一步,所以这时可以不必再保留当前的执行上下文,从而节省了内存,这就是尾调用优化。但是 ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。
// 普通递归
function add(num) {
if (num === 1) return 1;
return num + add(num - 1);
}
console.log(add(5));
// 尾调用:结束函数调用只能调用本身,不过可以在调用自己的时候传入一个辅助参数
function add2(num, sum) {
if (num === 1) return sum + num;
return add(num - 1, sum + num); //结束函数调用只能调用本身 ,不能加任何变量或者函数等等
}
console.log(add2(5, 0));//性能优化,避免栈溢出,栈里永远只有一个函数
19. ES6模块与CommonJS模块有什么异同?
模块化(CommonJS、ES6中的模块化)
区别
前者是值的引入,后者是值的拷贝
前者是编译的时候输出接口,后者是运行加载
前者是异步加载,后者是同步加载
ES6 Module和CommonJS模块的区别:
● CommonJS是对模块的浅拷⻉,ES6 Module是对模块的引⽤,即ES6 Module只存只读,不能改变其值,也就是指针指向不能变,类似const;
● import的接⼝是read-only(只读状态),不能修改其变量值。 即不能修改其变量的指针指向,但可以改变变量内部指针指向,可以对commonJS对重新赋值(改变指针指向),但是对ES6 Module赋值会编译报错。
ES6 Module和CommonJS模块的共同点:
● CommonJS和ES6 Module都可以对引⼊的对象进⾏赋值,即对对象内部属性的值进⾏改变。
发展史
-
最初
-
命名空间和IIEF
需要知道正确的依赖顺序
-
node.js中实现了commonJS
commonJS是同步的服务端读取本地硬盘可以很快完成同步加载,但是浏览器要同步读取服务端的模块需要很长的时间,并且造成浏览器假死的状态,所以出现了异步加载模块AMD -
AMD
-
我们必须添加加载所有模块于是出现了CMD规范,不同于AMD的依赖前缀,CMD是就近依赖
-
UMD
UMD用来判断是用AMD还是CommonJS的方式来定义模块
比如下面代码
如果是AMD就用AMD的方式来定义模块
如果是CommonJS就用CommonJS的方式来定义模块
如果都不是就挂载到全局对象上。
6. ES6自带的模块化
ES6模块与CommonJS模块有什么异同
如果是CommonJS utils.count 输出是0 如果是ES6utils.count 输出是1
20. 常见的DOM操作有哪些
1)DOM 节点的获取
DOM 节点的获取的API及使用:
getElementById // 按照 id 查询
getElementsByTagName // 按照标签名查询
getElementsByClassName // 按照类名查询
querySelectorAll // 按照 css 选择器查询
// 按照 id 查询
var imooc = document.getElementById('imooc') // 查询到 id 为 imooc 的元素
// 按照标签名查询
var pList = document.getElementsByTagName('p') // 查询到标签为 p 的集合
console.log(divList.length)
console.log(divList[0])
// 按照类名查询
var moocList = document.getElementsByClassName('mooc') // 查询到类名为 mooc 的集合
// 按照 css 选择器查询
var pList = document.querySelectorAll('.mooc') // 查询到类名为 mooc 的集合
2)DOM 节点的创建
创建一个新节点,并把它添加到指定节点的后面。已知的 HTML 结构如下:
<html>
<head>
<title>DEMO</title>
</head>
<body>
<div id="container">
<h1 id="title">我是标题</h1>
</div>
</body>
</html>
要求添加一个有内容的 span 节点到 id 为 title 的节点后面,做法就是:
// 首先获取父节点
var container = document.getElementById('container')
// 创建新节点
var targetSpan = document.createElement('span')
// 设置 span 节点的内容
targetSpan.innerHTML = 'hello world'
// 把新创建的元素塞进父节点里去
container.appendChild(targetSpan)
3)DOM 节点的删除
删除指定的 DOM 节点,已知的 HTML 结构如下:
<html>
<head>
<title>DEMO</title>
</head>
<body>
<div id="container">
<h1 id="title">我是标题</h1>
</div>
</body>
</html>
需要删除 id 为 title 的元素,做法是:
// 获取目标元素的父元素
var container = document.getElementById('container')
// 获取目标元素
var targetNode = document.getElementById('title')
// 删除目标元素
container.removeChild(targetNode)
或者通过子节点数组来完成删除:
// 获取目标元素的父元素
var container = document.getElementById('container')
// 获取目标元素
var targetNode = container.childNodes[1]
// 删除目标元素
container.removeChild(targetNode)
4)修改 DOM 元素
修改 DOM 元素这个动作可以分很多维度,比如说移动 DOM 元素的位置,修改 DOM 元素的属性等。
将指定的两个 DOM 元素交换位置,已知的 HTML 结构如下:
<html>
<head>
<title>DEMO</title>
</head>
<body>
<div id="container">
<h1 id="title">我是标题</h1>
<p id="content">我是内容</p>
</div>
</body>
</html>
现在需要调换 title 和 content 的位置,可以考虑 insertBefore 或者 appendChild:
// 获取父元素
var container = document.getElementById('container')
// 获取两个需要被交换的元素
var title = document.getElementById('title')
var content = document.getElementById('content')
// 交换两个元素,把 content 置于 title 前面
container.insertBefore(content, title)
21. use strict是什么意思 ? 使用它区别是什么?
use strict 是一种 ECMAscript5 添加的(严格模式)运行模式,这种模式使得 Javascript 在更严格的条件下运行。设立严格模式的目的如下:
● 消除 Javascript 语法的不合理、不严谨之处,减少怪异行为;
● 消除代码运行的不安全之处,保证代码运行的安全;
● 提高编译器效率,增加运行速度;
● 为未来新版本的 Javascript 做好铺垫。
区别:
● 禁止使用 with 语句。
● 禁止 this 关键字指向全局对象。
● 对象不能有重名的属性。
22. 如何判断一个对象是否属于某个类?
● 第一种方式,使用 instanceof 运算符来判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
● 第二种方式,通过对象的 constructor 属性来判断,对象的 constructor 属性指向该对象的构造函数,但是这种方式不是很安全,因为 constructor 属性可以被改写。
● 第三种方式,如果需要判断的是某个内置的引用类型的话,可以使用 Object.prototype.toString() 方法来打印对象的[[Class]] 属性来进行判断。
// JS如何判断一个对象是否属于一个类
// 类本身指向就是构造函数,类的数据类型就是函数
function Person(name) {
this.name = name;
}
// 1.instanceof:判断构造函数的prototype属性是否出现在对象的原型链的任何位置
var obj = new Person("张三");
var obj1 = {};
console.log(obj instanceof Person); // true
console.log(obj1 instanceof Person); //false
// 2.对象的属性constructor来判断,指向该对象的构造函数
console.log(obj.constructor); // Person
console.log(obj.__proto__.constructor); // Person
// 但是方式不是很安全,因为 constructor 属性可以被改写
obj.__proto__.constructor = Array;
console.log(obj.__proto__.constructor);
// 3.检测对象类型
var toString = Object.prototype.toString;
toString.call(new Date()); // [object Date]
toString.call(new String()); // [object String]
toString.call(Math); // [object Math]
//Since JavaScript 1.8.5
toString.call(undefined); // [object Undefined]
toString.call(null); // [object Null]
23. 强类型语言和弱类型语言的区别
● 强类型语言:强类型语言也称为强类型定义语言,是一种总是强制类型定义的语言,要求变量的使用要严格符合定义,所有变量都必须先定义后使用。Java和C++等语言都是强制类型定义的,也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。例如你有一个整数,如果不显式地进行转换,你不能将其视为一个字符串。
● 弱类型语言:弱类型语言也称为弱类型定义语言,与强类型定义相反。JavaScript语言就属于弱类型语言。简单理解就是一种变量类型可以被忽略的语言。比如JavaScript是弱类型定义的,在JavaScript中就可以将字符串’12’和整数3进行连接得到字符串’123’,在相加的时候会进行强制类型转换。
两者对比:强类型语言在速度上可能略逊色于弱类型语言,但是强类型语言带来的严谨性可以有效地帮助避免许多错误。
24. 解释性语言和编译型语言的区别
(1)解释型语言
使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行。是代码在执行时才被解释器一行行动态翻译和执行,而不是在执行之前就完成翻译。解释型语言不需要事先编译,其直接将源代码解释成机器码并立即执行,所以只要某一平台提供了相应的解释器即可运行该程序。其特点总结如下
● 解释型语言每次运行都需要将源代码解释称机器码并执行,效率较低;
● 只要平台提供相应的解释器,就可以运行源代码,所以可以方便源程序移植;
● JavaScript、Python等属于解释型语言。
(2)编译型语言
使用专门的编译器,针对特定的平台,将高级语言源代码一次性的编译成可被该平台硬件执行的机器码,并包装成该平台所能识别的可执行性程序的格式。在编译型语言写的程序执行之前,需要一个专门的编译过程,把源代码编译成机器语言的文件,如exe格式的文件,以后要再运行时,直接使用编译结果即可,如直接运行exe文件。因为只需编译一次,以后运行时不需要编译,所以编译型语言执行效率高。其特点总结如下:
● 一次性的编译成平台相关的机器语言文件,运行时脱离开发环境,运行效率高;
● 与特定平台相关,一般无法移植到其他平台;
● C、C++等属于编译型语言。
两者主要区别在于:前者源程序编译后即可在该平台运行,后者是在运行期间才编译。所以前者运行速度快,后者跨平台性好。
25. for…in和for…of的区别
for…of 是ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,和ES3中的for…in的区别如下
● for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名;
● for… in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for … of 只遍历当前对象不会遍历原型链;
● 对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of 只返回数组的下标对应的属性值;
总结:for…in 循环主要是为了遍历对象而生,不适用于遍历数组;for…of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。
for…in
for…in是为遍历对象属性而构建的,它以任意顺序遍历一个对象的除Symbol以外的可枚举属性,可用break或者throw跳出
let obj = {
name: '张三',
age: 18
}
for(let item in obj) {
console.log(item)
}
// 输出 name age
在JavaScript中,数组也是对象的一种,所以数组也是可以使用for...in遍历
let arr = ['a', 'b', 'c']
for(let item in arr) {
console.log(item)
}
// 输出 0 1 2
for…of
for…of语句在可迭代对象上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句(包括Array,Map,Set,String,TypedArray,arguments等等,不包括Object),可用break或者throw跳出。
let arr = ['a', 'b', 'c']
let obj = {
name: '张三',
age: 18,
sex: '男'
}
for (let i of arr) {
console.log(i)
}
// 输出 a b c
for (let i of obj) {
console.log(i)
}
// 报错 obj is not iterable (obj不是可迭代的)
无论是for…in还是for…of都是迭代一些东西。它们之间的主要区别在于它们的迭代方式
for…in语句以任意顺序迭代对象的可枚举属性
for…of语句遍历可迭代对象定义要迭代的数据
下面列出遍历Array时候,for…in和for…of的区别:
上例,通过Array.prototype添加了ufo属性,由于继承和原型链,所以arr也继承了ufo属性,属于可枚举属性,所以for…in会遍历出来,而for…of则不会。
let arr = ['a', 'b', 'c']
Array.prototype.ufo = '张三'
for(let item in arr) {
console.log(item)
}
// 输出 0 1 2 ufo
for(let item of arr) {
console.log(item)
}
// 输出 a b c
26. 如何使用for…of遍历对象
for…of是作为ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,普通的对象用for…of遍历是会报错的。
如果需要遍历的对象是类数组对象,用Array.from转成数组即可。
var obj = {
0:'one',
1:'two',
length: 2
};
obj = Array.from(obj);
for(var k of obj){
console.log(k)
}
如果不是类数组对象,就给对象添加一个[Symbol.iterator]属性,并指向一个迭代器即可。
//方法一:
var obj = {
a: 1,
b: 2,
c: 3,
};
obj[Symbol.iterator] = function () {
var keys = Object.keys(this);
console.log(keys); //['a', 'b', 'c']
var count = 0;
return {
next() {
if (count < keys.length) {
return { value: obj[keys[count++]], done: false };
} else {
return { value: undefined, done: true };
}
},
};
};
for (var k of obj) {
console.log(k); // 1,2,3
}
// // 方法二
var obj = {
a: 1,
b: 2,
c: 3,
};
obj[Symbol.iterator] = function* () {
var keys = Object.keys(obj);
for (var k of keys) {
yield [k, obj[k]];
}
};
for (var [k, v] of obj) {
console.log(k, v); // a 1 ,b 2,c,3
}
27. ajax、axios、fetch的区别
Ajax、Fetch、Axios三者的区别
ajax是js异步技术的术语,早起相关的api是xhr,它是一个术语。
fetch是es6新增的用于网络请求标准api,它是一个api。
axios是用于网络请求的第三方库,它是一个库。
Ajax 是一个技术统称,是一个概念模型,它囊括了很多技术,并不特指某一技术,它很重要的特性之一就是让页面实现局部刷新。
Fetch 是一个 API,它是真实存在的,它是基于 promise 的。
Axios 是一个基于 promise 封装的网络请求库,它是基于 XHR 进行二次封装。
<body>
<script>
// 1.ajax Ajax 是一个技术统称,是一个概念模型,它囊括了很多技术,并不特指某一技术,它很重要的特性之一就是让页面实现局部刷新。
// function ajax(url) {
// const xhr = new XMLHttpRequest();
// xhr.open("get", url, false);
// xhr.onreadystatechange = function () {
// // 异步回调函数
// if (xhr.readyState === 4) {
// if (xhr.status === 200) {
// console.info("响应结果", xhr.response);
// }
// }
// };
// xhr.send(null);
// }
// ajax("https://smallpig.site/api/category/getCategory");
// 2.fetch Fetch 是一个 API,它是真实存在的,它是基于 promise 的。
// function ajaxFetch(url) {
// fetch(url)
// .then((res) => res.json())
// .then((data) => {
// console.info(data);
// });
// }
// ajaxFetch("https://smallpig.site/api/category/getCategory");
// 3.axios Axios 是一个基于 promise 封装的网络请求库,它是基于 XHR 进行二次封装。
// import axios from axios
axios({
url: "http://123.207.32.32:8000/home/multidata",
params: {
type: "pop",
page: 1,
},
})
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err);
});
</script>
</body>
28. 数组的遍历方法有哪些
在这里插入图片描述
细数JavaScript中那些遍历和循环
29. forEach和map方法有什么区别
这方法都是用来遍历数组的,两者区别如下:
● forEach()方法会针对每一个元素执行提供的函数,对数据的操作会不改变原数组,该方法没有返回值;
forEach 方法用于调用数组的每个元素,并将元素传递给回调函数。
数组中的每个值都会调用回调函数,回调函数有三个参数:
currentValue: 必需。当前元素
index: 可选。当前元素的索引值。
arr: 可选。当前元素所属的数组对象
let arr = [1,2,3,4,5]
arr.forEach((item, index, arr) => {
console.log(index+":"+item)
})
该方法还可以有第二个参数,用来绑定回调函数内部this变量(前提是回调函数不能是箭头函数,因为箭头函数没有this):
let arr = [1,2,3,4,5]
let arr1 = [9,8,7,6,5]
arr.forEach(function(item, index, arr){
console.log(this[index]) // 9 8 7 6 5
}, arr1)
● map()方法不会改变原数组的值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值;
map() 方法会返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。该方法按照原始数组元素顺序依次处理元素。
注意: map() 不会对空数组进行检测,它会返回一个新数组,不会改变原始数组。
该方法的第一个参数为回调函数,他有三个参数:
currentValue: 必须。当前元素的值
index :可选。当前元素的索引值
arr :可选。当前元素属于的数组对象
let arr = [1, 2, 3];
arr.map(item => {
return item+1;
})
// 返回值: [2, 3, 4]
第二个参数用来绑定参数函数内部的this变量,可选:
var arr = ["a", "b", "c"];
let bar = [1, 2].map(function (e) {
console.log(e); //1,2
return this[e];
}, arr);
// 返回值: ['b', 'c']
console.log(bar);
该方法可以进行链式调用:
let arr = [1, 2, 3];
arr.map(item => item+1).map(item => item+1)
// 返回值: [3, 4, 5]
30. addEventListener()方法的参数和使用
EventTarget.addEventListener() 方法将指定的监听器注册到 EventTarget 上,当该对象触发指定的事件时,指定的回调函数就会被执行。 事件目标可以是一个文档上的元素 Element,Document和Window或者任何其他支持事件的对象。
addEventListener()的工作原理是将实现EventListener的函数或对象添加到调用它的EventTarget上的指定事件类型的事件侦听器列表中。
它的使用语法如下:
target.addEventListener(type, listener, options);
target.addEventListener(type, listener, useCapture);
target.addEventListener(type, listener, useCapture, wantsUntrusted);
element.addEventListener("click",function(e)=>{
console.log("点击事件监听");
console.log("点击返回的参数":e);
})
document.getElementById(元素id).addEventListener("click", function(){
console.log("目标元素被点击了");
});
其中参数如下:
(1)type
表示监听事件类型的字符串。
(2)listener
当所监听的事件类型触发时,会接收到一个事件通知(实现了 Event 接口的对象)对象。listener 必须是一个实现了 EventListener 接口的对象,或者是一个函数。
(3)options 可选
一个指定有关 listener 属性的可选参数对象。可用的选项如下:
● capture: Boolean,表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。
● once: Boolean,表示 listener 在添加之后最多只调用一次。如果是 true, listener 会在其被调用之后自动移除。
● passive: Boolean,设置为true时,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。
● signal:AbortSignal,该 AbortSignal 的 abort() 方法被调用时,监听器会被移除。
(4)useCapture 可选
Boolean,在DOM树中,注册了listener的元素, 是否要先于它下面的EventTarget,调用该listener。 当useCapture(设为true) 时,沿着DOM树向上冒泡的事件,不会触发listener。当一个元素嵌套了另一个元素,并且两个元素都对同一事件注册了一个处理函数时,所发生的事件冒泡和事件捕获是两种不同的事件传播方式。事件传播模式决定了元素以哪个顺序接收事件。如果没有指定, useCapture 默认为 false 。
(5)wantsUntrusted
如果为 true , 则事件处理程序会接收网页自定义的事件。此参数只适用于 Gecko(chrome的默认值为true,其他常规网页的默认值为false),主要用于附加组件的代码和浏览器本身。
addEventListener()参数及事件汇总