目录
0. 请简单说一说你对JavaSccript的理解以及有哪些优势和劣势
6. 请说明JavaScript中原生对象和宿主对象的区别?
10. JSON格式的数据和XML格式的数据相比,有哪些优势?
20. document.write() 和 innerHTML 有哪些区别
21. 在 DOM 中,事件对象的两个属性 target 和 currentTarget 有什么区别
23. new Object() 和 Object.create()的区别
24. JS中的load事件和 和Jquery中的ready事 的区别
27. attr(attribute)和property的区别
6. 数组的 pop push unshift shift 分别是什么
20. 判断字符串以字母开头,后面字母数字下划线,长度6-30
33. 介绍一下 RAF requestAnimationFrame
第一部分:
0. 请简单说一说你对JavaSccript的理解以及有哪些优势和劣势
JavaScript 是一种通过解释执行的高级编程语言,同时也是弱类型语言,适合面向对象(基于原型)和函数式的编程风格。主要运行在一个内置 JavaScript 解释器的客户端中(例如WEB浏览器),能处理复杂的计算、操控文档的内容、样式和行为。由ECMAScript、DOM、BOM三部分组成
优势:
- 可在客户端代替服务器分摊掉一些工作(例如数据验证、数学计算等),从而可以减少和服务器的交换次数,降低服务器的压力
- 比较容易上手
- 用户能快速得到页面上的反馈
- 跨平台。JavaScript 不会依赖操系统,只要有浏览器就能正常执行
- 丰富界面、增强交互。JavaScript 可以控制文档中的任何元素,定制元素的内容、样式或行为,用JavaScript 代替CSS可以实现复杂而多样的动画或特效(例如点击元素改变背景色等)
劣势:
兼容性低。各个浏览器对JavaScript 的支持程度不同,同一套脚本在不同浏览器中执行结果可能会不同
安全性低。由于JavaScript 是在客户端运行,用户不仅可以查看JavaScript 源代码,还能嵌入恶意代码、替换或者禁用脚本
中断运行。因为JavaScript是一种直译语言,因此只要有一条出错,那么就会直接停止运行
1. JavaScript 中,字面量指什么?
字面量即常量,是一种在程序中可以直接使用的数据值,通常它的值是固定的。例如数字字面量、字符串字面量、对象字面量等
2.什么是严格模式?严格模式有哪些限制?
ECMAScript5引入了严格模式的概念。严格模式对JavaScript的语法和行为做了一些更改,消除了语言中一些不合理、不确定、不安全之处;提供高效严谨的差错机制,保证代码安全运行。
在脚本文件第一行或函数内第一行引入“use strict”这条指令就能触发严格模式,这是一条没有副作用的指令,老旧的浏览器会将其作为一行字符串直接忽略
限制:
- 所有的变量要先声明
- 函数中this对象的默认值是undefined,而不是全局对象window
- 试图使用delete运算符删除不可删除的属性将会抛出异常
- 函数声明中定义两个或多个同名参数时将产生一个语法错误
- 禁止使用以O为前缀的八进制数字,以OX为前缀还是支持的
- 禁止使用with语句
- 不能将eval和arguments用作变量、函数或参数的名称
拓展:eval()可以执行一段字符串中的脚本,只有一个参数;如果参数是表达式,则 eval() 计算表达式。如果参数是一个或多个 JavaScript 语句,则 eval() 执行这些语句;在eval()中创建的变量或函数具有当前执行时所处的作用域,并且声明不能被提升,严格模式下不能改变作用域即不能定义新的变量或函数
3. undefined 和 null 有哪些异同?
同:
- 都有空缺的意思
- 不包含方法和属性
- 都是假值
- 都只有一个值
异:
- 含义不同,undefined 表示一个未定义的值,null 表示一个空对象
- 类型不同,将 typeof 运算符用于 undefined 得到的是 undefined ;用于 null 得到的是 object
- 数字转换不同,将 undefined 和 null 用全局函数 Number() 转换为数字,前者得到的是 NaN,后者得到的是 0
- 在非严格模式下的表现不同,undefined 可以是一个标识符,能被当做变量来使用和赋值,而null不行
4. 值类型和引用类型的区别
栈:从上往下
堆:从上往下
值类型(基本类型):字符串(string)、数值(number)、布尔值(boolean)、undefined、null (这5种基本数据类型是按值访问的,因为可以操作保存在变量中的实际的值)(ECMAScript 2016新增了一种基本数据类型:symbol)
除了0、NaN、''、null、undefined、flase之外其余全是true
引用类型:对象(Object)、数组(Array)、函数(Function)
区别:
值类型占用空间固定,保存在栈中;引用类型占用空间不固定,保存在堆中
值类型保存与复制的是值本身;引用类型保存与复制的是指向对象的一个指针即地址
值类型使用typeof检测数据的类型;引用类型使用instanceof检测数据类型
基本类型数据是值类型;使用new()方法构造出的对象是引用型
5. typeof 与 instanceof 的区别?
typeof 主要用于检测数据类型,而instanceof用于检测对象之间的关联性
typeof 执行完后会返回一个小写字母类型的字符串,例如 string、number;而 instanceof 会返回一个布尔值
typeof 只需一个操作数,这个操作数可以是基本类型也可是函数,例如typeof(1);而 instanceof 需要两个操作数,并且左操作数不能是基本类型,右操作数必须是函数,否则运算结果没有意义
6. 请说明JavaScript中原生对象和宿主对象的区别?
原生对象是由ECMAScript规范中定义的对象,所有内置对象都是原生对象,例如Array、Date、Math、RegExp等。
宿主对象是由环境(例如浏览器)定义的对象用于完善ECMAScript的执行环境,例如document、Location、navigator等
7. 如何识别浏览器的类型
- navigator:浏览器的信息
- screen:屏幕的信息
- location:地址信息
- history:前进后退的信息
//navigator
const ua = navigator.userAgent;
const isChrome = ua.indexOf('Chrome');
console.log(isChrome);
//screen
console.log(screen.width);
console.log(screen.height);
//location---拆解url各个部分
console.log(location.href);//拿到整个网址
console.log(location.protocol);//得到是http还是https协议
console.log(location.host);//得到域名
console.log(location.pathname);//取得浏览器的文件是什么
console.log(location.search);//取得网址里传递的一些参数
console.log(location.hash);//取得网址里 # 后面的内容
//history
history.back();
history.forward();
8. 原型和原型链
所有的构造方法中都会有一个属性--prototype属性,该属性会指向一个对象,该对象被称为原型对象简称原型
原型中存储的是该构造方法创建出来的所有实例可以共享的内容,原型上可以添加属性和方法,能够被(由类创建的对象)共同拥有
原型prototype是js为函数提供的一个对象类型的属性。原型归函数所有,不用创建,是默认存在的。通过该构造函数产生的对象,可以继承该原型的属性和方法。原型也是对象。
注意:
- 本属性不需要手动添加,prototype属性自动被所有函数拥有,直接使用即可
- 利用原型特点和概念,可以提取公有属性
- 对象如何查看原型?隐式属性 __proto__
- 对象如何查看对象的构造函数? constructor
- isPrototypeOf()方法用于判断调用此方法的对象是否存在于指定对象的原型链中
js中提供了一种机制:
- 如果是通过类创建的对象,当访问的属性在对象中如果没有找到;则会去【创建对象的类】的原型中查找。如果能找到,也相当于对象拥有这个属性。反之便会一层一层向上查找,这就是原型链。原型链的顶端是Object对象。Object对象没有__proto__属性,或者说Object对象的__proto__属性指向了自身
原型是一个对象,在原型中通常拥有两个属性:
(1)构造器(constructor):该属性指向了这个类本身 (指明了原型归哪个类所有),可以手动修改。
(2)原型指向(__proto__):该属性指向原型本身,提供给通过(类创建的对象)使用。
<script>
function People() {}
console.log(People.prototype);
var beixi = new People();
console.log(beixi.__proto__);
</script>
先来看一下未使用原型时:
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.speak = () => {
console.log('hello');
}
}
const p1 = new Person('张三', 10, '男');
const p2 = new Person('李四', 12, '女');
console.log(p1.name + p1.age + p1.gender);
p1.speak();
console.log(p2.name + p2.age + p2.gender);
p2.speak();
我们可以看到在未使用原型的情况下也可以正常输出和访问,但是我们有没有想过这样一个问题,既然speak()方法中执行输出的都是同一个,那么有没有必要在每次访问的时候都要为其开辟一个内存空间去存储?我们可以看下面这张图:
因此,在此基础之上我们可以利用原型来解决这个问题,将其存储在该对象的原型上,便可以实现实例共享,而不用每次都需要为其开辟一个内存空间
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
Person.prototype.speak = () => {
console.log('hello');
}
const p1 = new Person('张三', 10, '男');
const p2 = new Person('李四', 12, '女');
console.log(p1.name + p1.age + p1.gender);
p1.speak();
console.log(p2.name + p2.age + p2.gender);
p2.speak();
原型链:
对象之间通过原型关联到一起,就好比一条锁链将一个个对象连接在一起,在将各个对象挂钩之后,最终形成了一条原型链。在读取对象的以一个属性时,会先在对象中查询自有属性,如果不存在,那么就会沿着原型链向上搜索匹配的继承属性,知道找到或到达原型链顶端,才停止搜索
更详细的资料可参考:原型和原型链
9. 如何判断对象中的某个属性是继承而来的
将 in 运算符和 Object 对象里的 hasOwnProperty() 方法组合使用,即可检测一个属性是否是继承属性
只要 in 运算符返回 true,hasOwnProperty() 返回false,即可确定这是个继承属性
示例代码如下:
function isInheritProperty(obj, name) {
return name in obj && !obj.hasOwnProperty(name);
}
let obj1 = {"name": "strict"};
let obj2 = Object.create(obj1);
console.log(isInheritProperty(obj2,"name"));// true
10. JSON格式的数据和XML格式的数据相比,有哪些优势?
- 语法格式更简单
- 层次结构更清晰
- 所用字符数更少
- 数据解析更直接
什么是JSON:
json是一种数据格式,本质是一段字符串
json格式和JS对象结构一致,对JS语言更友好
window.JSON是一个全局对象:JSON.stringify JSON.parse
{
"name":"zhangsan",
"info":{
"single":true,
"age":30,
"city":"重庆"
},
"like":["篮球","音乐"]
}
11. 怎么用 JSON 对象执行深拷贝?
用 JSON 对象执行深拷贝需要几个前置条件,首先属性值不能是 undefined、NaN、Infinity;其次属性值不能是函数、变量、对象实例或正则表达式
因此用JSON对象实现深拷贝时,只能使用一些简单的数据类型。
function deepCopy(obj){
return JSON.parse(JSON.stringify(obj));
}
12. 函数声明和函数表达式有哪些区别?
函数通常有2种创建方式:函数声明和函数表达式
区别如下:
函数声明必须包含名称,而函数表达式可省略名称
函数声明有位置限制,不能出现在条件语句、循环语句或其他语句中,而函数表达式没有位置限制,可以出现在语句中实现动态编程
函数声明会先于函数表达式被提升至作用域的顶部,因此用函数声明创建的函数可以在声明之前被调用,而函数表达式必须在表达式之后才能被调用
示例:
// 函数表达式
var fun = function(aru) {
console.log('我是函数表达式');
console.log(aru);
}
//注意
// (1) fun是变量名 不是函数名
// (2) 函数表达式声明方式跟声明变量差不多,只不过变量里面存的是值 而 函数表达式里面存的是函数
// (3) 函数表达式也可以进行传递参数
// 函数声明
function fn() {
// 函数体
}
// function构造函数(很少使用)
var fn = new Function();
13. Function 构造器有哪些功能
利用 Function 构造器能创建函数,这是第三种创建函数的方式。构造器通过动态编译字符串代码来实现函数的创建,其实现方式和使用全局函数 eval() 类似。构造函数 Function() 能接收任意多个参数(在调用函数时传入的参数),最后一个函数是新函数的函数体,其他都是新函数的形参
示例:
var fn = new Function("a", "b", "return a + b");
// 相当于如下:
var fn = function (a, b) {
return a + b;
};
用 Function 构造器创建新函数不但写法比较晦涩、性能比较低效,而且新函数使用的还是全局作用域,示例代码如下:
var name = "freedom"; // 全局变量
function fn(){
var name="strick";
return new Function("return name");
}
fn()(); // "freedom"
14. 对闭包的理解
作用域应用的特殊情况,有两种表现:
- 函数作为参数被传递
- 函数作为返回值被传递
影响:变量会常驻内存,得不到释放
什么是闭包:
当一个函数能够访问和操作另一个函数作用域中的变量时,就构成了一个闭包。例如:函数a的内部有一个函数b,在函数a的外部有一个变量引用函数a的时候,就创建了一个闭包
在实际开发中,闭包最大的作用就是用来 变量私有。
function a() {
var i = 0;
function b() {
alert(++i);
}
return b;
}
//调用外部函数,返回值为闭包函数
var c = a();
//调用闭包函数
c();
闭包的特性:
- 封闭性:外界无法访问闭包内部的数据,如果在闭包内声明变量,外界是无法访问的,除非闭包主动向外界提供访问接口;
- 持久性:一般的函数,调用完毕之后,系统自动注销函数,而对于闭包来说,在外部函数被调用之后,闭包结构依然保存在
- 系统中,闭包中的数据依然存在,从而实现对数据的持久使用。
优点:
① 减少全局变量。
② 减少传递函数的参数量
③ 封装;
缺点:
使用闭包会占有内存资源,过多的使用闭包会导致内存溢出等.
自由变量示例 -- 内存会被释放,
// 自由变量示例 -- 内存会被释放
let a = 0;
function fn1() {
let a1 = 100;
function fn2() {
let a2 = 200;
function fn3() {
let a3 = 300;
return a + a1 + a2 + a3;
}
fn3()
}
fn2()
}
fn1()
闭包 函数作为返回值 -- 内存不会被释放,
//闭包 函数作为返回值 -- 内存不会被释放
function create() {
let a = 100;
return function() {
console.log(a);
}
}
let fn = create();
let a = 200;
fn(); //100
闭包 函数作为参数
function print(fn) {
let a = 200;
fn();
}
let a = 100;
function fn() {
console.log(a);
}
print(fn); //100
总结:自由变量的查找是在函数定义的地方,向上级作用域查找,不是在执行的地方
闭包的应用场景:
隐藏数据:
//闭包隐藏数据,只提供API
function createCache() {
const data = {}; //闭包中的数据,被隐藏,不被外界访问
return {
set: function(key, value) {
data[key] = value;
},
get: function(key) {
return data[key];
}
}
}
const c = createCache();
c.set('a', 100);
console.log(c.get('a')); //100
15. 什么是事件循环
运行在宿主环境中的 JavaScript 引擎针对单线程,提供了一种机制来更高效地处理多个任务,这个机制就被称为事件循环(Event Loop)。事件循环具体的执行过程分为一下4步:
- 先执行主线程中的任务
- 当主线程空闲时,再从任务队列中读取任务,继续执行
- 即使主线程阻塞了,任务队列还是会继续接收任务
- 主线程不断地重复第(2)步操作
16. 如何用class实现继承(ES6)
原型:
每个class都有显示原型prototype
每个实例都有隐式原型__proto__
实例的__proto__指向对应的class的prototype
寻找时现在自身寻找,若找不到则去__proto__中查找
原型链:
类:
//类
class Student {
constructor(name, number) {
this.name = name;
this.number = number;
}
sayHi() {
console.log(
`姓名:${this.name}`, `学号:${this.number}`
);
}
}
//通过类 new 对象/实例
const xiaoxiao = new Student('xx', 100);
console.log(xiaoxiao.name);
console.log(xiaoxiao.number);
xiaoxiao.sayHi();
实现继承:
//父类
class Person {
constructor(name) {
this.name = name;
}
eat() {
console.log(
`${this.name} eat something`
);
}
}
//子类
class Student extends Person {
constructor(name, number) {
super(name);
this.number = number;
}
sayHi() {
console.log(
`姓名:${this.name}`, `学号:${this.number}`
);
}
}
//通过类 new 对象/实例
const xiaoxiao = new Student('xx', 100);
console.log(xiaoxiao.name);
console.log(xiaoxiao.number);
xiaoxiao.sayHi();
xiaoxiao.eat();
17. 关于this的场景题
注意:this取什么值是在函数被执行的时候确定的不是在函数定义的时候确定的
用法:
0.全局作用域中 this 指向:window对象
1.html事件中:指向window对象---on+事件=‘函数();函数();函数();……
2.dom0事件中:指向函数的调用者 --- on+事件名
3.dom2事件中:非IE下,指向函数的调用者;IE下,指向window对象 ---元素.addEventListener(type,listener,useCapture)
非IE下:
<script>
var but = document.querySelector('button');
but.addEventListener('click', function() {
console.log(this);
})
</script>
<button>点我</button>
IE下:
<button>点我</button>
<script>
var but = document.querySelector('button');
but.attachEvent('onclick', function() {
console.log(this);
})
</script>
4.间隔调用和延迟调用:指向window对象---setInterval()/setTimeout()
5.call/apply/bind:第一个参数是谁,this就指向谁
6.闭包:闭包中this指向window对象
7.自执行函数:this指向window对象
<script>
(function() {
console.log(this);
}());
</script>
8.this在正常函数中(非箭头函数),谁调用了函数,this就指向谁;
注意:
- 严格模式指向 undefined
- 有在函数调用的时候 this 指向才确定,不调用的时候,不知道指向谁
-
this 指向和函数在哪儿调用没关系,只和谁在调用有关
-
没有具体调用对象的话,this 指向 undefined,在非严格模式下,转向 window
最后上一个案例:
<script>
const obj = {
fn1: function() {
console.log(this);
},
fn2: test
}
function test() {
console.log(this);
setTimeout(function() {
console.log(this);
})
}
const res = obj.fn1;
res();
obj.fn2();
</script>
9.原型中:
10.作为对象方法被调用:指向当前这个对象
11.在class中被调用:constructor中的this和方法里的指向当前正在创建的这个实例,
12.箭头函数:this指向父级作用域中的this,不是调用者
18. bind、apply和call函数
描述:bind、apply和call函数都是window中提供的,用来改变this指向的函数
语法:
- bind:函数.bind(函数实际的调用者,函数参数1,函数参数2,...)
- call: 函数.call(函数实际的调用者,函数参数1,函数参数2,...)
- apply: 函数.apply(函数实际的调用者,【函数参数1,函数参数2,...】)
a.bind方法
修改函数或者方法中的this为指定的对象, 并且会返回一个修改之后的新函数给我们
注意点: bind方法除了可以修改this以外, 还可以传递参数, 只不过参数必须写在this对象的后面
let obj = {
name: "zs"
}
function test(a, b) {
console.log(a, b);
console.log(this);
}
test(10, 20);// window.test();
let fn = test.bind(obj, 10, 20);
fn();
手写bind
Function.prototype.myBind = function() {
//将参数解析为数组
const args = Array.prototype.slice.call(arguments);
//获取 this (取出数组第一项,数组剩余的就是传递的参数)
const t = args.shift();
const self = this; //当前函数
//返回一个函数
return function() {
//执行原函数,并返回结果
return self.apply(t, args);
}
};
function fn1(a, b, c) {
console.log('this', this);
console.log(a, b, c);
return 'this is fn1';
}
const fn2 = fn1.myBind({
x: 100
}, 20, 30, 40);
const res = fn2();
console.log(res);
b.call方法
修改函数或者方法中的this为指定的对象, 并且会立即调用修改之后的函数
注意点: call方法除了可以修改this以外, 还可以传递参数, 只不过参数必须写在this对象的后面
let obj = {
name: "zs"
}
function test(a, b) {
console.log(a, b);
console.log(this);
}
test(10, 20);// window.test();
test.call(obj, 10, 20);
c.apply方法
修改函数或者方法中的this为指定的对象, 并且会立即调用修改之后的函数
注意点: apply方法除了可以修改this以外, 还可以传递参数, 只不过参数必须通过数组的方式传递
let obj = {
name: "zs"
}
function test(a, b) {
console.log(a, b);
console.log(this);
}
test(10, 20);// window.test();
test.apply(obj, [10, 20]);
<script>
function Person(name, age) {
this.name = name;
this.age = age;
}
var person = new Person('zhang', 20);
var obj = {
}
Person.call(obj, 'wang', 50);
</script>
19. 什么是作用域?什么是自由变量?
作用域:
- 全局作用域
- 函数作用域
- 块级作用域
自由变量:
一个变量在当前作用域没有定义,但被使用了
那么就会向上级作用域一层一层向上找,直到找到为止,若找不到则报 xx is not defined 的错
关于作用域和自由变量的场景题:
let i;
// for (let i = 1; i <= 3; i++) {
for (i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i);
}, 0)
}
let a = 100;
function test() {
alert(a);//100
a = 10;
alert(a);//10
}
test();
alert(a);//10
20. document.write() 和 innerHTML 有哪些区别
document.write() 和 innerHTML 都能将 HTML 字符串解析为 DOM 树,再将 DOM 树插入到某个位置,但两者在执行细节上还是有许多的不同:
- write() 方法存在于 Document 对象中,innerHTML 属性存在于 Element 对象中
- document.write() 会将解析后的 DOM 树插入到文档中调用它的脚本元素(script)的位置,而 innerHTML 会将 DOM 树插入到指定元素内
- document.write() 会将多次调用的字符串参数自动连接起来, innerHTML 要拼接的话需要用赋值运算符“+=”
// 将两个参数拼接后输出
document.write("<p>strick!</p>");
document.write("<p>freedom!</p>");
// 将两个 HTML 字符串拼接后输出
var container = document.getElementById("container");
container .innerHTML += "<p>strick!</p>";
container .innerHTML += "<p>freedom!</p>";
- 只有当文档还在解析时,才能使用 document.write() ,否则会将当前文档覆盖掉。而 innerHTML 属性则没有这个限制。像下面这个例子,整个文档就会被替换为“freedom”
window.onload = function () {
document.write("freedom");
};
21. 在 DOM 中,事件对象的两个属性 target 和 currentTarget 有什么区别
target 属性指向的是事件的目标,currentTarget 指向的是正在处理当前事件的对象。
在发生事件传播时,target 指向的可能不是定义时的事件目标。例如只给按钮的容器元素注册点击事件,当点击按钮时,target 指向的是 <button> 元素,而不是 <div> 元素;而 currentTarget 始终指向的是 <div> 元素:
<div>
<button type="button" id="btn">按钮</button>
</div>
<script>
var btn = document.getElementById("btn");
btn.parentNode.addEventListener("click", function(event){
console.log(event.target); // <button>元素
console.log(event.currentTarget); // <div>元素
}, false);
</script>
22. 如何捕获JS程序中的异常
//方式1
try{
//todo
}catch(ex){
console.error(ex);//手动捕获
}finally{
//todo
}
//方式2 自动捕获
window.onerror = function(message,source,lineNum,colNum,error){
//第一,对跨域的js,如cdn的,不会有详细的报错信息
//第二,对于压缩的js,还要配合sourceMap反查到未压缩代码的行、列
}
23. new Object() 和 Object.create()的区别
- {} 等同于 new Object(),原型Object.prototype
- Object.create(null)没有原型
- Object.create({...})可指定原型
const obj1 = {
a: 10,
b: 20,
sum() {
return this.a + this.b;
}
};
console.log(obj1);
const obj2 = new Object({
a: 10,
b: 20,
sum() {
return this.a + this.b;
}
});
console.log(obj2);
console.log(obj1 === obj2);
const obj3 = new Object(obj1);
console.log(obj1 === obj3);
const obj4 = Object.create(null);
const obj5 = new Object();
console.log(obj4);
console.log(obj5);
const obj6 = Object.create({
a: 10,
b: 20,
sum() {
return this.a + this.b;
}
});
console.log(obj6);
const obj7 = Object.create(obj1);
console.log(obj7);
24. JS中的load事件和 和Jquery中的ready事 的区别
区别1:如果页面中同一元素被添加了多个load事件,那么后面添加的事件会覆盖前面的;而jquery中的ready事件可以同时存在
//当页面加载完毕后触发
window.onload = function() {
console.log(1);
}
window.onload = function() {
console.log(2);
}
window.onload = function() {
console.log(3);
}
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
$(document).ready(function() {
console.log('ready1');
});
$(document).ready(function() {
console.log('ready2');
})
区别2:ready事件的执行时间比load事件早;因为ready事件是在DOM树渲染完成后执行(此时可能图片、视频等还未加载完毕),而load事件是在页面中的元素加载完毕后(页面的全部资源加载完毕执行,包括图片、视频等)再执行
25. 同步和异步的区别是什么?
JS是单线程语言,只能同时做一件事,即同步
JS和DOM渲染共用同一个线程,因为JS可修改DOM结构
遇到等待(网络请求、定时任务等)时不能卡住,这就需要异步
区别:
- 异步是基于JS单线程语言
- 同步会阻塞代码执行
- 异步不会阻塞代码的执行
异步:
//异步(callback 回调函数) 不会阻塞后面的代码执行
console.log(100);
setTimeout(function() {
console.log(200);
}, 1000);
console.log(300);
同步:
//同步
console.log(100);
alert(200);
console.log(300);
前端使用异步的场景:
网络请求:如ajax图片加载
示例1,
//ajax
console.log('start');
$.get('./data.json', function(data) {
console.log(data);
})
console.log('end');
示例2,
//图片加载
console.log('start');
let img = document.createElement("img");
img.onload = function() {
console.log('loaded');
}
img.src = "./xxx.png";
console.log('end');
定时任务,如setTimeout
示例1,
//setTimeout
console.log(100);
setTimeout(function() {
console.log(200);
}, 1000);
console.log(300);
示例2,
//setInterval
console.log(100);
setInterval(function() {
console.log(200);
}, 1000);
console.log(400);
26. DOM操作常用的API
获取DOM节点API:
- document.getElementById()
- document.getElementsByTagName()
- document.getElementsByClassName()
- document.querySelectorAll()
- document.querySelector()
创建型API:
- document.createElement()
createTextNode()
cloneNode()
createDocumentFragment()
它们创建的节点只是一个孤立的节点,要通过appendChild
添加到文档中
cloneNode
要注意如果被复制的节点是否包含子节点以及事件绑定等问题
使用createDocumentFragment
来解决添加大量节点时的性能问题
修改页面型API:
appendChild
()insertBefore()
removeChild()
replaceChild()
节点关系型api:
- parentNode:Element的父节点可能是Element,Document或DocumentFragment。
- parentElement:与parentNode的区别在于,其父节点必须是一个Element,如果不是,则返回null
- previousSibling:节点的前一个节点,如果该节点是第一个节点,则为null。注意有可能拿到的节点是文本节点或注释节点,与预期的不符,要进行处理一下。
- previousElementSibling:返回前一个元素节点,前一个节点必须是Element,注意IE9以下浏览器不支持。
- nextSibling:节点的后一个节点,如果该节点是最后一个节点,则为null。注意有可能拿到的节点是文本节点,与预期的不符,要进行处理一下。
- nextElementSibling:返回后一个元素节点,后一个节点必须是Element,注意IE9以下浏览器不支持。
- childNodes:返回一个即时的NodeList,表示元素的子节点列表,子节点可能会包含文本节点,注释节点等。
- children:一个即时的HTMLCollection,子节点都是Element,IE9以下浏览器不支持。
- firstNode:第一个子节点
- lastNode:最后一个子节点
- hasChildNodes方法:可以用来判断是否包含子节点。
元素属性型api:
- setAttribute:根据名称和值修改元素的特性eg:element.setAttribute(name, value);
- getAttribute返回指定的特性名相应的特性值,如果不存在,则返回null或空字符串
元素样式型api:
window.getComputedStyle
是用来获取应用到元素后的样式,假设某个元素并未设置高度而是通过其内容将其高度撑开,这时候要获取它的高度就要用到getComputedStyle
,用法如下:
var style = window.getComputedStyle(element[, pseudoElt]);
element是要获取的元素,pseudoElt指定一个伪元素进行匹配。
返回的style是一个CSSStyleDeclaration对象。
通过style可以访问到元素计算后的样式
- getBoundingClientRect用来返回元素的大小以及相对于浏览器可视窗口的位置,用法如下:
var clientRect = element.getBoundingClientRect();
clientRect是一个DOMRect对象,包含left,top,right,bottom,它是相对于可视窗口的距离,滚动位置发生改变时,它们的值是会发生变化的。除了IE9以下浏览器,还包含元素的height和width等数据
27. attr(attribute)和property的区别
- property其实是通过获取或者修改js的属性,例如:p.style.width=""、p.className=""、p.innerHTML=""、p.nodeName、p.nodeType,去改变/修改页面的一种形式,因此property只是一种形式。修改对象属性,不会体现到HTML结构中
- 包括setAttribute()/getAttribute(),直接去修改HTML的结构,例如:p.setAttribute('data-img','img')、p.getAttribute('data-img'),它能够真正作用到DOM结构里,我们能够真正的看到
- 两者都有可能会引起DOM重新渲染
28. 一次性插入多个DOM节点,考虑性能
- 对DOM查询做缓存
//不缓存 DOM 查询结果
for (let i = 0; i < document.getElementsByTagName('p'); i++) {
//每次循环,都会计算 length,频繁进行 DOM 查询
}
//缓存 DOM 查询结果
const pList = document.getElementsByTagName('p');
const length = pList.length;
for (let i = 0; i < length; i++) {
//缓存 length,只进行一次 DOM 查询
}
- 将频繁操作改为一次性操作
const listNode = document.getElementById("list");
//创建一个文档片段,此时还没有插入到DOM树中
const frag = document.createDocumentFragment()
//执行插入
for (let i = 0; i < 10; i++) {
const li = document.createElement('li');
li.innerHTML = "List item" + i;
frag.appendChild(li);
}
//都完成后,再插入到DOM树中
listNode.appendChild(frag);
29. 解释一下什么是变量提升
变量提升(hoisting),是负责解析执行代码的 JavaScript 引擎的工作方式产生的一个特性
JS引擎在运行一份代码的时候,会按照下面的步骤进行工作:
-
首先,对代码进行预解析,并获取声明的所有变量
-
然后,将这些变量的声明语句统一放到代码的最前面
-
最后,开始一行一行运行代码
我们通过一段代码来解释这个运行过程:
console.log(a)
var a = 1
function b() {
console.log(a)
}
b() // 1
上⾯这段代码的实际执⾏顺序为:
-
JS引擎将
var a = 1
分解为两个部分:变量声明语句var a = undefined
和变量赋值语句a = 1
-
JS引擎将
var a = undefined
放到代码的最前面,而a = 1
保留在原地
也就是说经过了转换,代码就变成了:
var a = undefined
console.log(a) // undefined
a = 1
function b() {
console.log(a)
}
b() // 1
变量的这一转换过程,就被称为变量的声明提升
30. JS的参数是以什么方式进行传递的?
基本数据类型和复杂数据类型的数据在传递时,会有不同的表现。
基本类型:是值传递!
基本类型的传递方式比较简单,是按照 值传递
进行的。
let a = 1
function test(x) {
x = 10 // 并不会改变实参的值
console.log(x)
}
test(a) // 10
console.log(a) // 1
复杂类型: 传递的是地址! (变量中存的就是地址)
来看下面的代码:
let a = {
count: 1
}
function test(x) {
x.count = 10
console.log(x)
}
test(a) // { count: 10 }
console.log(a) // { count: 10 }
从运行结果来看,函数内改变了参数对象内的 count
后,外部的实参对象 a
的内容也跟着改变了,所以传递的是地址。
思考题:
let a = {
count: 1
};
function test(x) {
x = { count: 20 };
console.log(x);
}
test(a); // { count: 20 }
console.log(a); // { count: 1 }
我们会发现外部的实参对象 a
并没有因为在函数内对形参的重新赋值而被改变!
因为当我们直接为这个形参变量重新赋值时,其实只是让形参变量指向了别的堆内存地址,而外部实参变量的指向还是不变的。
下图展示的是复杂类型参数传递后的状态:
下图展示的是重新为形参赋值后的状态:
31. JavaScript 中垃圾回收是怎么做的?
JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收。
正因为垃圾回收器的存在,许多人认为JS不用太关心内存管理的问题,
但如果不了解JS的内存管理机制,我们同样非常容易成内存泄漏(内存无法被回收)的情况。
31.1 内存的生命周期
-
内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
-
内存使用:即读写内存,也就是使用变量、函数等
-
内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
全局变量一般不会回收, 一般局部变量的的值, 不用了, 会被自动回收掉
// 为变量分配内存
let i = 11
let s = "ifcode"
// 为对象分配内存
let person = {
age: 22,
name: 'ifcode'
}
// 为函数分配内存
function sum(a, b) {
return a + b;
}
31.2 垃圾回收算法说明
所谓垃圾回收, 核心思想就是如何判断内存是否已经不再会被使用了, 如果是, 就视为垃圾, 释放掉
下面介绍两种常见的浏览器垃圾回收算法: 引用计数 和 标记清除法
引用计数:
IE采用的引用计数算法, 定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。
如果没有任何变量指向它了,说明该对象已经不再需要了
// 创建一个对象person, person指向一块内存空间, 该内存空间的引用数 +1
let person = {
age: 22,
name: 'ifcode'
}
let p = person // 两个变量指向一块内存空间, 该内存空间的引用数为 2
person = 1 // 原来的person对象被赋值为1,对象内存空间的引用数-1,
// 但因为p指向原person对象,还剩一个对于对象空间的引用, 所以对象它不会被回收
p = null // 原person对象已经没有引用,会被回收
如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露
function cycle() {
let o1 = {}
let o2 = {}
o1.a = o2
o2.a = o1
return "Cycle reference!"
}
cycle()
标记清除法:
现代的浏览器已经不再使用引用计数算法了。
现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。
标记清除法:
-
标记清除算法将“不再使用的对象”定义为“无法达到的对象”。
-
简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。
-
凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
从这个概念可以看出,无法触及的对象包含了没有引用的对象这个概念(没有任何引用的对象也是无法触及的对象)。
function cycle() {
let o1 = {}
let o2 = {}
o1.a = o2
o2.a = o1
return "Cycle reference!"
}
cycle()
根据这个概念,上面的例子可以正确被垃圾回收处理了。
参考文章:JavaScript内存管理
32. 谈谈对作用域链的理解
JavaScript 在执⾏过程中会创建一个个的可执⾏上下⽂。 (每个函数执行都会创建这么一个可执行上下文)
每个可执⾏上下⽂的词法环境中包含了对外部词法环境的引⽤,可通过该引⽤来获取外部词法环境中的变量和声明等。
这些引⽤串联起来,⼀直指向全局的词法环境,形成一个链式结构,被称为作⽤域链。
简而言之: 函数内部 可以访问到 函数外部作用域的变量, 而外部函数还可以访问到全局作用域的变量,
这样的变量作用域访问的链式结构, 被称之为作用域链
let num = 1
function fn () {
let a = 100
function inner () {
console.log(a)
console.log(num)
}
inner()
}
fn()
下图为由多个可执行上下文组成的调用栈:
-
栈最底部为
全局可执行上下文
-
全局可执行上下文
之上有多个函数可执行上下文
-
每个可执行上下文中包含了指向外部其他可执行上下文的引用,直到
全局可执行上下文
时它指向null
js全局有全局可执行上下文, 每个函数调用时, 有着函数的可执行上下文, 会入js调用栈
每个可执行上下文, 都有着对于外部上下文词法作用域的引用, 外部上下文也有着对于再外部的上下文词法作用域的引用
=> 就形成了作用域链
33. 如何判断是否是数组
方法一:使用 toString
方法
function isArray(arg) {
return Object.prototype.toString.call(arg) === "[object Array]";
}
let arr = [1, 2, 3];
console.log(isArray(arr));// true
方式二:使用 instanceof
let arr = [1,2]
console.log(arr instanceof Array); // true
方式三:使用 ES6 新增的 Array.isArray
方法
let arr = [1, 2, 3];
console.log(Array.isArray(arr)); // true
第二部分:
1.var和let、const的区别
var是ES5语法,let、const是ES6的语法;var有变量提升
var a = 100;
console.log(a); //100
// 变量提升
console.log(b); //undefined
var b = 150;
console.log(c);
let c = 200;
var和let是变量,可修改;const是常量,不可修改
let、const有块级作用域,var没有
for (var i = 0; i < 10; i++) {
var j = i + 1;
}
console.log(i, j);
//块级作用域
for (let i = 0; i < 10; i++) {
let j = i + 1;
}
console.log(i, j);
2. 列举强制类型转换和隐式类型转换
强制:parseInt parseFloat toString toNumber等
隐式:if、逻辑运算、==、+拼接字符串
const a = 100 + 10;
const b = 100 + '10';
const c = true + '10';
if (a) {}
if (b) {}
!!0 == false;
!!NaN == false;
!!'' == false;
!!null == false;
!!undefined == flase;
!!flasw == false;
console.log(10 && 0); //0
console.log('' || 'abc'); //'abc'
3. ==和===的区别
== 会尝试类型转换
=== 严格相等(数值、类型都相同)
console.log(100 == '100'); //true
console.log(100 === '100'); //false
console.log(0 == false); //true
console.log(0 === false); //false
console.log(false == ''); //true
console.log(null == undefined); //true
除了 == null/undefined 之外,其他一律用 ===,例如:
const obj = {
a: 100
};
if (obj.a == null) {}
//相当于
if (obj.a === null || obj.a === undefined) {}
4. 手写深拷贝
注意: Object.assign
不是深拷贝!只拷贝了一层
/**
* 深拷贝
*/
const obj1 = {
age: 20,
name: 'xxx',
address: {
city: 'beijing'
},
arr: ['a', 'b', 'c']
}
const obj2 = deepClone(obj1)
obj2.address.city = 'shanghai'
obj2.arr[0] = 'a1'
console.log(obj1.address.city)
console.log(obj1.arr[0])
/**
* 深拷贝
* @param {Object} obj 要拷贝的对象
*/
function deepClone(obj = {}) {
if (typeof obj !== 'object' || obj == null) {
// obj 是 null ,或者不是对象和数组,直接返回
return obj
}
// 初始化返回结果
let result
if (obj instanceof Array) {
result = []
} else {
result = {}
}
for (let key in obj) {
// 保证 key 不是原型的属性
if (obj.hasOwnProperty(key)) {
// 递归调用!!!
result[key] = deepClone(obj[key])
}
}
// 返回结果
return result
}
class Person {
name = "xxx";
cat = {
age: 3
};
scores = [1, 3, 5];
}
let p1 = new Person();
let p2 = new Object();
depCopy(p2, p1);
console.log(p2);
p2.cat.age = 666;
console.log(p1.cat.age);
console.log(p2.cat.age);
function depCopy(target, source) {
// 1.通过遍历拿到source中所有的属性
for (let key in source) {
// console.log(key);
// 2.取出当前遍历到的属性对应的取值
let sourceValue = source[key];
// console.log(sourceValue);
// 3.判断当前的取值是否是引用数据类型
if (sourceValue instanceof Object) {
// console.log(sourceValue.constructor);
//.constructor拿到对象的构造函数,进而判断是数组还是对象
// console.log(new sourceValue.constructor);
// new sourceValue.constructor 创建对象或数组
let subTarget = new sourceValue.constructor;
target[key] = subTarget;
//再次拷贝,将当前的对象或数组以及其取值拷贝进去
depCopy(subTarget, sourceValue);
} else {
target[key] = sourceValue;
}
}
}
5. split()和join()的区别
console.log('1-2-3'.split('-')); //以...进行拆分
console.log([1, 2, 3].join('-')); //以...进行连接
6. 数组的 pop push unshift shift 分别是什么
const arr = [10, 20, 30, 40];
//pop 删除数组最后的一个元素,并返回删除的元素
const popRes = arr.pop();
console.log(popRes, arr);
//push 在数组的最后追加一个元素,并返回新数组的长度
const pushRes = arr.push(50);
console.log(pushRes, arr);
// unshift 在数组的最前面添加一个元素,并返回新数组的长度
const unshiftRes = arr.unshift(5);
console.log(unshiftRes, arr);
//shift 将数组最前面的元素删除,并返回删除的元素
const shiftRes = arr.shift();
console.log(shiftRes, arr);
7. 数组的API,有哪些是纯函数
纯函数:1.不改变源数组;2.返回一个数组
const arr = [10, 20, 30, 40];
// concat
const arr1 = arr.concat([50, 60]);
console.log(arr, arr1);
arr.pop();
console.log(arr, arr1);
//map
const arr2 = arr.map(num => num * 10);
console.log(arr, arr2);
//filter 过滤
const arr3 = arr.filter(num => num > 25);
console.log(arr, arr3);
//slice 类似于深拷贝或者复制
const arr4 = arr.slice();
arr.unshift(0);
console.log(arr, arr4);
非纯函数:
- pop push unshift shift splice
- forEach
- some every
- reduce
8. 数组slice和splice的区别
功能区别(slice--切片;splice--剪切)
const arr = [10, 20, 30, 40];
//slice 纯函数 含头不含尾
const arr1 = arr.slice();
console.log(arr1);
const arr2 = arr.slice(1, 4);
console.log(arr2);
const arr3 = arr.slice(2);
console.log(arr3);
const arr4 = arr.slice(-2);
console.log(arr4);
const arr = [10, 20, 30, 40];
//splice 非纯函数
const spliceRes = arr.splice(1, 2, 'a', 'b');
console.log(spliceRes, arr);
const spliceRes1 = arr.splice(1, 2);
console.log(spliceRes1, arr);
9. 数组去重
关于数组去重的方法有很多,这里就介绍如下两种:
- 传统方式,遍历元素,挨个比较,去重
// 传统方式
function unique(arr) {
const res = []
//遍历原数组,判断结果中(res数组里)是否有arr数组里的元素,没有就放入结果中
arr.forEach(item => {
if (res.indexOf(item) < 0) {
res.push(item)
}
})
return res
}
const res = unique([30, 10, 20, 30, 40, 10])
console.log(res) //[30, 10, 20, 40]
- 使用Set
// 使用 Set (无序,不能重复)
function unique(arr) {
const set = new Set(arr)
return [...set]
}
const res = unique([30, 10, 20, 30, 40, 10])
console.log(res) //[30, 10, 20, 40]
- 考虑计算效率
建议: 能使用Set就使用Set。 Set比较快,传统方式需要循环。兼容性和速度看需求
感兴趣的可以参考:常用的数组去重的几种方法
10. 降低数组维度
方式1:
const arr = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
//定义函数,降低数组维度
function fn(myArr) {
const newArr = [];
//遍历外层
for (let i = 0; i < myArr.length; i++) {
//遍历内层
for (let j = 0; j < myArr[i].length; j++) {
newArr.push(myArr[i][j]);
}
}
return newArr;
}
console.log(fn(arr));
方式2:
const arr = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
//定义函数,降低数组维度
function fn(myArr) {
/**
* concat 拼接数组
* apply 改变this指向
*
* [].concat([],[],[])
*/
return Array.prototype.concat.apply([], myArr);
}
console.log(fn(arr));
方式3:
const arr = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
let [
[a, b, c],
[d, e, f],
[g, h, j]
] = arr;
console.log([].concat(a, b, c, d, e, f, g, h, j));
11. 斐波拉契数列
function fn(n) {
if (n == 1 || n == 2) {
return 1;
} else {
/**
* arguments.callee():返回正被执行的 Function 对象,也就是所指定的 Function 对象的正文,有利于匿名函数的递归或者保证函数的封装性
* 在这里 arguments.callee() 就指向了 fn,这样做可以降低其耦合度
*/
return arguments.callee(n - 1) + arguments.callee(n - 2);
}
}
console.log(fn(8));
12. 统计字符串中出现次数最多的字符以及出现的次数
const str = 'adgawgffbdncdjkfeiie';
const obj = {};
//遍历字符串
for (let i = 0; i < str.length; i++) {
//如果obj里不存在该字符
if (!obj[str.charAt(i)]) {
/**
* obj={
* a:1,
* d:1,
* g:1,
* ...
* }
*/
obj[str.charAt(i)] = 1;
} else { //obj里存在该字符
/**
* obj={
* a:2,
* d:1,
* g:1,
* ...
*/
obj[str.charAt(i)] = obj[str.charAt(i)] + 1;
}
}
//2.
let max = 0;
let index = '';
//遍历obj对象
for (let i in obj) {
if (obj[i] > max) {
max = obj[i];
index = i;
}
}
console.log('字符串中出现次数最多的字符是:' + index + ',出现的次数为:' + max);
let max = 0;
let index = '';
//遍历obj对象
for (let i in obj) {
if (obj[i] > max) {
max = obj[i];
index = i;
}
}
console.log('字符串中出现次数最多的字符是:' + index + ',出现的次数为:' + max);
或
const str = 'xxxxxyyysnwdkji';
const length = str.length
let obj = {};
let current = null;
//1.
for(let i = 0; i < length; i++) {
current = str[i];
// 如果 obj 里面不存在该字符
if(!obj[current]){
obj[current] = 1;
continue;
}
obj[current]++;
}
//2.
let max = 0;
let index = '';
//遍历obj对象
for (let i in obj) {
if (obj[i] > max) {
max = obj[i];
index = i;
}
}
console.log('字符串中出现次数最多的字符是:' + index + ',出现的次数为:' + max);
13. 如何获取多个数字中的最大值
function getMax() {
const nums = Array.prototype.slice.call(arguments); //变为数组
let max = 0;
nums.forEach(n => {
if (n > max) {
max = n;
}
})
return max;
}
Math.max(x,y,z);
14. 手写深度比较 lodash.isEqual
//判断是否是对象或数组
function isObject(obj) {
return typeof obj === 'object' && obj != null;
}
//判断是否全相等
function isEqual(obj1, obj2) {
if (!isObject(obj1) || (!isObject(obj2))) {
//值类型(注意,参与 equal 的一般不会是函数)
return obj1 === obj2;
}
if (obj1 === obj2) {
return true;
}
//若两个都是对象或数组,而且不全相等
// 1.先取出 obj1和obj2的keys,比较个数是否一样
const obj1Keys = Object.keys(obj1);
const obj2Keys = Object.keys(obj2);
if (obj1Keys.length !== obj2Keys.length) {
return false;
}
//2.以 obj1 为基准,和obj2依次递归比较
for (let key in obj1) {
//比较当前key的value
const res = isEqual(obj1[key], obj2[key]);
if (!res) {
return false;
}
}
//3.全相等
return true;
}
//测试
const obj1 = { a: 10, b: { x: 100, y: 200, z: 10 } };
const obj2 = { a: 10, b: { x: 100, y: 200 } };
//调用函数
console.log(isEqual(obj1, obj2));
15. 手写简易的jQuery考虑插件和扩展性
<p>123</p>
<p>456</p>
<p>789</p>
<script>
class jQuery {
constructor(selector) {
const result = document.querySelectorAll(selector);
const length = result.length;
for (let i = 0; i < length; i++) {
this[i] = result[i];
}
this.length = length;
this.selector = selector;
}
get(index) {
return this[index];
}
each(fn) {
for (let i = 0; i < this.length; i++) {
const elem = this[i];
fn(elem);
}
}
on(type, fn) {
return this.each(elem => {
elem.addEventListener(type, fn, false);
});
}
//......
}
//插件机制
jQuery.prototype.dialog = function(info) {
alert(info);
};
const $p = new jQuery('p');
$p.dialog('abc');
//复写机制--造轮子
class myJQuery extends jQuery {
constructor(selector) {
super(selector);
}
//扩展自己的方法--造新轮子
addClass(className) {
//...
}
style(data) {
//...
}
}
// const $p = new jQuery('p');
// console.log($p);
// console.log($p.get(1));
// $p.each((elem) => console.log(elem.nodeName));
16. 创建10个'a'标签,点击时弹出对应的序号
块级作用域和全局作用域
let a;
for (let i = 0; i < 10; i++) {
a = document.createElement("a");
a.innerHTML = i + '<br/>';
a.addEventListener('click', function(e) {
e.preventDefault(); //阻止默认事件
alert(i);
});
document.body.appendChild(a);
}
17. [10,20,30].map(parseInt)
map()返回数组
parseInt(string, radix)
string 必需,要被解析的字符串。
radix 可选,表示要解析的数字的基数。
该值介于 2 ~ 36 之间。
如果省略该参数或其值为 0,则数字将以 10 为基础来解析。
如果它以 “0x” 或 “0X” 开头,将以 16 为基数,也就是16进制。
如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。
const res = [10, 20, 30].map(parseInt);
console.log(res);
//拆解
[10, 20, 30].map((num, index) => {
return parseInt(num, index);
})
18. 手写数组flatern,考虑多层级
一层数组可以用如下方法,
多层数组需要递归,
function flat(arr) {
// 验证 arr 中,还有没有深层数组 [1, 2, [3, 4]]
const isDeep = arr.some(item => item instanceof Array); // arr数组中只要有一个元素符合Array类型
if (!isDeep) {
return arr; // 已经是 flatern [1, 2, 3, 4]
}
const res = Array.prototype.concat.apply([], arr);
return flat(res); // 递归
}
const res = flat([
[1, 2], 3, [4, 5, [6, 7, [8, 9, [10, 11]]]]
]);
console.log(res);
19. 手写字符串trim方法,保证浏览器兼容性
if (!!String.prototype.trim) {
String.prototype.trim = function() {
//\s--匹配任意的空白字符
return this.replace(/^\s+/, '').replace(/\s+$/, '');
}
}
20. 判断字符串以字母开头,后面字母数字下划线,长度6-30
// \w--字母数字下划线;{5,29}--长度6-30
const reg = /^[a-zA-z]\w{5,29}$/;
代码 | 说明 |
---|---|
. | 匹配除换行符以外的任意字符 |
\w | 匹配字母或数字或下划线或汉字 |
\s | 匹配任意的空白符 |
\d | 匹配数字 |
\b | 匹配单词的开始或结束 |
^ | 匹配字符串的开始 |
$ | 匹配字符串的结束 |
代码/语法 | 说明 |
---|---|
* | 重复零次或更多次 |
+ | 重复一次或更多次 |
? | 重复零次或一次 |
{n} | 重复n次 |
{n,} | 重复n次或更多次 |
{n,m} | 重复n到m次 |
代码/语法 | 说明 |
---|---|
\W | 匹配任意不是字母,数字,下划线,汉字的字符 |
\S | 匹配任意不是空白符的字符 |
\D | 匹配任意非数字的字符 |
\B | 匹配不是单词开头或结束的位置 |
[^x] | 匹配除了x以外的任意字符 |
[^aeiou] | 匹配除了aeiou这几个字母以外的任意字符 |
代码/语法 | 说明 |
---|---|
*? | 重复任意次,但尽可能少重复 |
+? | 重复1次或更多次,但尽可能少重复 |
?? | 重复0次或1次,但尽可能少重复 |
{n,m}? | 重复n到m次,但尽可能少重复 |
{n,}? | 重复n次以上,但尽可能少重复 |
更多更详细的资料可以参考:30分钟入门正则表达式
21. 获取当前页面url参数
- 传统方式:查找 ·
location.search
- 新API,
URLSearchParams
代码示例:
浏览器输入:http://127.0.0.1:8080/index.html?a=10&b=20&c=30
// 传统方式
function query(name) {
//?a=10&b=20&c=30 substr(1) 去掉 ?
const search = location.search.substr(1); // 类似 array.slice(1)
// search: 'a=10&b=20&c=30'
const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i');
const res = search.match(reg); // search字符串来匹配reg表达式
if (res === null) {
return null;
}
return res[2];
}
console.log(query('a')); // 10
// URLSearchParams
function query(name) {
const search = location.search
const p = new URLSearchParams(search)
return p.get(name)
}
console.log( query('b') ) // 20
22. 将url参数解析为JS对象
//传统方式,分析 search
function queryToObj() {
const res = {};
//去掉前面的 ?
const search = location.search.substr(1);
search.split('&').forEach(paramStr => {
const arr = paramStr.split('=');
const key = arr[0];
const val = arr[1];
res[key] = val;
})
return res;
}
//使用 URLSearchParams
function queryToObj() {
const res = {};
const pList = new URLSearchParams(location.search);
pList.forEach((val, key) => {
res[key] = val;
})
return res;
}
23. 手写用promise加载一张图片
promise的三个状态: pending(默认) resolve(成功) rejected(失败)
-
resolve函数被执行时, 会将promise的状态从 pending 改成 resolve成功
-
reject函数被执行时, 会将promise的状态从pending 改成 rejected 失败
Promise.reject()
new Promise((resolve, reject) => {
reject()
})
Promise.resolve()
new Promise((resolve, reject) => {
resolve()
})
Promise.all([promise1, promise2, promise3]) 等待原则, 是在所有promise都完成后执行, 可以用于处理一些并发的任务
// 后面的.then中配置的函数, 是在前面的所有promise都完成后执行, 可以用于处理一些并发的任务
Promise.all([promise1, promise2, promise3]).then((values) => {
// values 是一个数组, 会收集前面promise的结果 values[0] => promise1的成功的结果
})
Promise.race([promise1, promise2, promise3]) 赛跑, 竞速原则, 只要三个promise中有一个满足条件, 就会执行.then(用的较少)
promise解决回调地狱问题(callback hell)
function loadImg(src) {
const p = new Promise(
(resolve, reject) => {
const img = document.createElement("img");
//图片加载完后
img.onload = () => {
resolve(img);
}
img.onerror = () => {
reject(new Error(`图片加载失败 ${src}`));
}
img.src = src;
}
)
return p;
}
const url1 = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fscimg.jianbihuadq.com%2F202012%2F20201204222811179.jpg&refer=http%3A%2F%2Fscimg.jianbihuadq.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1637589352&t=ef0267f955a157bd288746ab85f70b29";
const url2 = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Ffile02.16sucai.com%2Fd%2Ffile%2F2014%2F0829%2F372edfeb74c3119b666237bd4af92be5.jpg&refer=http%3A%2F%2Ffile02.16sucai.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1637589435&t=843a4c1ef3aaf8360b5b46e21c3b6d1b";
//加载一张图片
// loadImg(url1).then(img => {
// console.log(img.width);
// return img;
// }).then(img => {
// console.log(img.height);
// }).catch(error => {
// console.log(error);
// });
//加载多张
loadImg(url1).then(img1 => {
console.log(img1.width);
return img1; //返回普通对象
}).then(img1 => {
console.log(img1.height);
return loadImg(url2); //返回 promise 实例
}).then(img2 => {
console.log(img2.width);
return img2;
}).then(img2 => {
console.log(img2.height);
}).catch(error => {
console.log(error);
});
24. 编写一个通用的事件监听/绑定函数
event.stopPropagation() 阻止事件冒泡
event.preventDefault() 阻止事件的默认行为
// selector 要筛选的那个元素
function bindEvent(elem, type, selector, fn) {
//进行是传入3个参数还是传入4个参数的判断
if (fn == null) {
fn = selector;
selector = null;
}
elem.addEventListener(type, event => {
const target = event.target;
if (selector) {
//代理绑定
//matches 判断一个DOM元素是不是符合css选择器
if (target.matches(selector)) {
fn.call(target, event);
}
} else {
//普通绑定
fn.call(target, event);
}
});
};
//普通绑定
const btn = document.getElementById('link1');
bindEvent(btn, 'click', function(e) {
e.preventDefault(); //阻止默认行为
alert('btnClicked');
});
//代理绑定
const a = document.getElementById('link1');
bindEvent(a, 'click', 'a', function(e) {
e.preventDefault(); //阻止默认行为
alert('clicked');
});
25. 描述事件冒泡的流程
const p1 = document.getElementById('p1');
const body = document.body;
addEventListener(p1, 'click', e => {
e.stopPropagation(); //阻止事件冒泡
alert("1");
});
addEventListener(body, 'click', e => {
alert('2');
});
26. 事件代理/委托
事件委托简单来说就是将事件添加到祖先元素上,然后利用事件冒泡的原理并结合事件对象使容器(祖先元素)中的后代元素具有同类事件的一种机制
公共部分:
<ul>
<li>我是第1个li</li>
<li>我是第2个li</li>
<li>我是第3个li</li>
<li>我是第4个li</li>
<li>我是第5个li</li>
</ul>
我们先来看一下不利用事件委托时的一个demo
//获取元素
const lis = document.getElementsByTagName("li");
const ul = document.getElementsByTagName("ul")[0];
//循环遍历每一个li,并为其添加点击事件
for (let i = 0; i < lis.length; i++) {
lis[i].onclick = function() {
console.log(this.innerHTML);
}
}
//向ul里新添加一个li,检验其是否也具备点击事件
const newLi = document.createElement("li");
newLi.innerHTML = "我是新添加的li";
ul.appendChild(newLi);
这里我们不难发现,当在ul里新添加一个li时,新添加的li元素并不具备点击事件,其实这个原因很简单,js代码是从上到下执行的,因此新添加了li就不会具备点击事件
接下来我们针对上面这个demo利用事件委托来做
//获取元素
const ul = document.getElementsByTagName("ul")[0];
//利用事件冒泡机制---给ul添加点击事件
ul.onclick = function(e) {
e.preventDefault();
e = e || window.event;
console.log(e.target.innerHTML);
}
//向ul里新添加一个li,检验其是否也具备点击事件
const newLi = document.createElement("li");
newLi.innerHTML = "我是新添加的li";
ul.appendChild(newLi);
这样一来便可以解决上面的demo不能解决的问题
特点:
代码简洁
减少浏览器内存占用
27. 什么是浏览器的同源策略
ajax请求时,浏览器要求当前网页和server必须同源
同源策略:客户端脚本(尤其是JavaScript)的重要的安全度量标准。是为了防止某个文档或脚本从多个不同源装载
同源:即协议、域名、端口三者必须一致
同源策略是一种安全协议,指一段脚本只能读取来自同一来源的窗口和文档的属性
加载图片、css、js可无视同源策略
28. 实现跨域常见的方式--jsonp和CORS
jsonp的原理:
- <script>可以绕过跨域限制
- 服务器可以任意动态拼接数据返回
- 因此,<script>就可以获得跨域的数据,只要服务端愿意返回
注意:
只支持get请求
为什么jsonp不是真正的ajax:
- ajax的核心是通过xmlHttpRequest获取非本页内容
- jsonp的核心是动态添加script标签调用服务器提供的js脚本
- jsonp只支持get请求,ajax支持get和post请求
CORS:(服务器端)
服务器端可以设置http header
//第二个参数填写允许跨域的域名称,不建议直接写"*"
response.setHeader("Access-Control-Allow-Origin", "http://locahst:8011");
response.setHeader("Access-Control-Allow-Headers", "X-Requested-Width");
response.setHeader("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
//接收跨域的cookie
response.setHeader("Access-Control-Allow-Credentials", "true");
29. 手写简易的Ajax
//原始
function ajax(url, successFn) {
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.readyState === 200) {
successFn(xhr.responseText);
}
}
}
xhr.send(null);
}
//promise
function ajax(url) {
const p = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) { //返回完成
if (xhr.readyState === 200) { //成功
resolve(JSON.parse(xhr.responseText));
} else if (xhr.readyState === 404) {
reject(new Error('404 not found'));
}
}
}
xhr.send(null);
});
return p;
}
const url = "/data/test.json";
ajax(url)
.then(res => console.log(res))
.catch(err => console.log(err))
30. ajax
什么是Ajax,为什么使用ajax
一个可以快速创建动态网页的一种技术,通过XMLHttpRequest对象与后端进行通信,访问服务器并进行少量的数据交换,也可以实现网页的异步请求和局部刷新
为什么使用?
通过异步模式,提高用户体验
优化了浏览器和服务器之间的传输,减少了不必要的数据的往返,减少了宽带的占用,同时减轻了服务器的压力
Ajax应用和传统web应用有什么不同
传统的web应用是浏览器直接将请求发送给服务器,服务器把数据返回给浏览器;而对于ajax则是当你在输入内容的时候通过ajax去获取用户输入的内容,输入完成后便创建XMLHttpRequest对象并让其去请求服务器而且是异步请求,服务器接收到请求后响应回来,并渲染到页面
例如注册账户时:传统的web是当你所有信息都填写完了点击提交后才去进行验证,而对于ajax是当你填完其中一个信息时如果不符合那么就会立刻提示
Ajax的优缺点
优点:
整个页面无刷新,只是局部刷新,用户体验很好
使用异步的请求模式与服务器进行通信 ,因此可以更加迅速的响应数据
减轻了服务器的压力,利用客户端去渲染数据,同时节省了客户端的宽带/流量
缺点:
不支持浏览器的返回
不安全,ajax会暴露与服务器交互的细节(可通过浏览器查看)
对搜索引擎的支持比较弱
ajax请求get和post的区别:
get一般用于查询操作,post一般用于用户提交操作
get参数拼接在url上,post放在请求体内(数据体积可更大)
安全性:post易于防止CSRF
31. 描述 cookie localStorage sessionStorage 的区别
本身用于浏览器和server通讯
被“借用”到本地存储来的,因为在那个时候刚刚发布HTML5还没有localStorage sessionStorage,只能是用cookie来做本地存储
可通过 document.cookie="";来修改和赋值
缺点:
- 最大存储为4KB
- http请求时需要发送到服务端,增加了请求的数据量
- 只能用document.cookie="";来修改和赋值,太过简陋
localStorage sessionStorage:
HTML5专门为存储而设计,最大可存储5M
API简单易用 setItem getItem
不会随着thhp请求被发送出去
localStorage数据会永久存储,除非代码或手动删除
sessionStorage数据只存在于当前会话,浏览器关闭则清空
一般用localStorage会更多一些
cookie localStorage sessionStorage 的区别:
容量
API易用性
是否跟随http请求发送出去
32. 手写防抖和节流
防抖:
监听一个输入框的文字变化后触发change事件
直接用keyup事件,则会频繁触发change事件
防抖:用户输入结束或暂停时,才会触发change事件
<input type="text" id="input1">
<script>
const input1 = document.getElementById("input1");
// input1.addEventListener('keyup', function() {
// console.log(input1.value);
// });
// let timer = null;
// input1.addEventListener('keyup', function() {
// if (timer) {
// clearTimeout(timer);
// }
// timer = setTimeout(() => {
// console.log(input1.value); //模拟触发事件
// //清空定时器
// timer = null;
// }, 300);
// });
// 封装
function debounce(fn, delay = 300) {
// timer 是在闭包中的,不对外暴露
let timer = null;
return function() {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
//fn函数在执行的时候也需要把this和arguments传进去
//因为传入的函数里面可能会有参数
fn.apply(this, arguments);
//清空定时器
timer = null;
}, delay);
}
}
//将函数当做参数传入
input1.addEventListener('keyup', debounce(function() {
console.log(input1.value); //模拟触发事件
}), 500);
</script>
节流:
拖拽一个元素时,要随时拿到该元素被拖拽到的位置
直接用drag事件,则会频繁触发,很容易导致卡顿
节流:无论拖拽速度有多快,都会每隔100ms触发一次
#div1 {
border: 1px solid #ccc;
width: 200px;
height: 100px;
}
<div id="div1" draggable="true">可拖拽</div>
<script>
const div1 = document.getElementById("div1");
// let timer = null;
// div1.addEventListener('drag', function(e) {
// if (timer) {
// return;
// }
// timer = setTimeout(() => {
// console.log(e.offsetX, e.offsetY);
// //清空
// timer = null;
// }, 100);
// })
//封装
function throttle(fn, delay = 200) {
let timer = null;
return function() {
if (timer) {
return;
}
timer = setTimeout(() => {
fn.apply(this, arguments);
//清空
timer = null;
}, delay);
}
}
div1.addEventListener('drag', throttle(function(e) {
console.log(e.offsetX, e.offsetY);
}), 100);
</script>
33. 介绍一下 RAF requestAnimationFrame
- 想要动画流畅,更新频率要60帧/s, 即 16.67ms 更新一次视图
setTimeout
要手动控制频率,而 RAF 浏览器会自动控制- 后台标签或隐藏
iframe
中,RAF会暂停,而setTimeout
依然执行
示例,
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>JS 真题演示</title>
<style>
#div1 {
width: 100px;
height: 50px;
background-color: red;
}
</style>
</head>
<body>
<p>JS 真题演示</p>
<div id="div1"></div>
<script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.js"></script>
<script src="./RAF.js"></script>
</body>
</html>
//方式1 setTimeout
// 3s 把宽度从 100px 变为 640px ,即增加 540px
// 60帧/s ,3s 180 帧 ,每次变化 3px
const $div1 = $('#div1')
let curWidth = 100
const maxWidth = 640
// setTimeout
function animate() {
curWidth = curWidth + 3
$div1.css('width', curWidth)
if (curWidth < maxWidth) {
setTimeout(animate, 16.7) // 自己控制时间
}
}
animate()
//方式2 RAF
// 3s 把宽度从 100px 变为 640px ,即增加 540px
// 60帧/s ,3s 180 帧 ,每次变化 3px
const $div1 = $('#div1')
let curWidth = 100
const maxWidth = 640
// RAF
function animate() {
curWidth = curWidth + 3
$div1.css('width', curWidth)
if (curWidth < maxWidth) {
window.requestAnimationFrame(animate) // 时间不用自己控制
}
}
animate()
34. 前端性能如何优化?一般从哪几个方面考虑?
- 原则:多使用内存,缓存,减少计算,减少网络请求
- 方向:加载页面,页面渲染,页面操作流畅度
35. async/awit 是什么
ES7 标准中新增的 async
函数,从目前的内部实现来说其实就是 Generator
函数的语法糖。
它基于 Promise,并与所有现存的基于Promise 的 API 兼容。
async 关键字
-
async
关键字用于声明⼀个异步函数(如async function asyncTask1() {...}
) -
async
会⾃动将常规函数转换成 Promise,返回值也是⼀个 Promise 对象 -
async
函数内部可以使⽤await
await 关键字
-
await
用于等待异步的功能执⾏完毕var result = await someAsyncCall()
-
await
放置在 Promise 调⽤之前,会强制async函数中其他代码等待,直到 Promise 完成并返回结果 -
await
只能与 Promise ⼀起使⽤ -
await
只能在async
函数内部使⽤
与 promise 相比 async/await 的优势在哪里:
-
同步化代码的阅读体验(Promise 虽然摆脱了回调地狱,但 then 链式调⽤的阅读负担还是存在的)
-
和同步代码更一致的错误处理方式( async/await 可以⽤成熟的 try/catch 做处理,比 Promise 的错误捕获更简洁直观)
-
调试时的阅读性, 也相对更友好