前端面试题集锦——JavaScript
1.请你谈谈 Cookie 的优缺点
cookie是存储于访问者计算机中的变量 cookie是浏览器提供的一种机制 可以由JavaScript对其进行控制(设置、读取、删除)
优点:极高的扩展性和可用性
\1) 数据持久性。
\2) 不需要任何服务器资源。 Cookie 存储在客户端并在发送后由服务器读取。
\3) 可配置到期规则。 控制 cookie 的生命期,使之不会永远有效。偷盗者很可能拿到一个过期的 cookie 。
\4) 简单性。 基于文本的轻量结构。
\5) 通过良好的编程,控制保存在 cookie 中的 session 对象的大小。
\6) 通过加密和安全传输技术( SSL ),减少 cookie 被破解的可能性。
\7) 只在 cookie 中存放不敏感数据,即使被盗也不会有重大损失。
缺点:
\1) Cookie 数量和长度的限制 。 数量:每个域的 cookie 总数有限。
a) IE6 或更低版本最多 20 个 cookie
b) IE7 和之后的版本最后可以有 50 个 cookie
c) Firefox 最多 50 个 cookie
d) chrome 和 Safari 没有做硬性限制
长度:每个 cookie 长度不超过 4KB ( 4096B ),否则会被截掉。
\2) 潜在的安全风险 。 Cookie 可能被拦截、篡改。如果 cookie 被拦截,就有 可能取得所有的 session 信息。
\3) 用户配置为禁用 。有些用户禁用了浏览器或客户端设备接受 cookie 的能 力,因此限制了这一功能。
\4) 有些状态不可能保存在客户端 。例如,为了防止重复提交表单,我们需要在 服务器端保存一个计数器。如果我们把这个计数器保存在客户端,那么它起不到 任何作用。
2.Array.prototype.slice.call(arr,2)方法的作用是:
Array.prototype.slice.call(arguments,1);
这句话的意思就是说把调用方法的参数截取出来。
Array.prototype.slice.call(arguments)
能将具有length属性的对象转成数组,除了IE下的节点集合(因为ie下的dom对象是以com对象的形式实现的,js对象与com对象不能进行转换)
因为arguments并不是真正的数组对象,只是与数组类似而已,所以它并没有slice这个方法,而Array.prototype.slice.call(arguments, 1)可以理解成是让arguments转换成一个数组对象,让arguments具有slice()方法。要是直接写arguments.slice(1)会报错。
利用 Array 原型上的 slice 方法,使用 call 函数的第一个参数,让这个方法中的 this 指向 arr,并传递参数 2,实际上等于 arr.slice(2),即从下标为 2 开始截取到末尾。
3.以下代码执行后,控制台的输出是:
var a = 10;
function a(){}
console.log(typeof a)
A."number"
B."object"
C."function"
D."undefined"
答案:A
函数提升优先级高于变量提升,变量提升会提升到除函数声明的后面;变量提升,但是赋值不提升,所以代码等价于
function a(){}
var a;
a = 10;
console.log(typeof a)
4、简单说一下浏览器本地存储是怎样的
总的来说,浏览器存储分为以下几种:
1、Cookie 存储,明文,大小限制 4k 等
2、localStorage,持久化存储方式之一,不用在两端之间传输,且限制大小为 10M
- 声明周期永久生效,除非手动删除 否则关闭页面也会存在
- 可以多窗口(页面)共享(同一浏览器可以共享)
- 以键值对的形式存储使用
3、sessionStorage,会话级存储方式,浏览器关闭立即数据丢失
- 生命周期为关闭浏览器窗口
- 在同一个窗口(页面)下数据可以共享
- 以键值对的形式存储使用
4、indexDb,浏览器端的数据库
IndexedDB 是一种可以让你在用户的浏览器内持久化存储数据的方法。IndexedDB 为生成 Web Application 提供了丰富的查询能力,使我们的应用在在线和离线时都可以正常工作。
-
键值对储存:indexDB采用对象仓库存放数据,所有类型的数据都可以存入。
-
异步:indexDB操作不会锁死浏览器
-
支持事务:操作步骤中一步失败,整个事务取消,不存在只改写一部分数据的情况
-
同源限制:不能访问跨域数据库,只能访问自身域名下的数据库。
-
储存空间大:一般来说不少于250MB,没上限
-
支持二进制储存:可以存储字符串,而且可以存储二进制数据。
5.原型 / 构造函数 / 实例
原型(prototype): 一个简单的对象,用于实现对象的 属性继承。可以简单的理解成对象的爹。在 Firefox 和 Chrome 中,每个 JavaScript 对象中都包含一个__proto__ (非标准)的属性指向它爹(该对象的原型),可 obj.__proto__进行访问。
构造函数: 可以通过 new 来 新建一个对象 的函数。
实例: 通过构造函数和 new 创建出来的对象,便是实例。 实例通过__proto__ 指向原型,通过 constructor 指向构造函数。
说了一大堆,大家可能有点懵逼,这里来举个栗子,以 Object 为例,我们常用的 Object 便是一个构造函数,因此我们可以通过它构建实例。
// 实例
const instance = new Object()
则此时, 实例为 instance, 构造函数为 Object,我们知道,构造函数拥有一个
prototype 的属性指向原型,因此原型为:
// 原型
const prototype = Object.prototype
这里我们可以来看出三者的关系:
实例.__proto__ === 原型
原型.constructor === 构造函数
构造函数.prototype === 原型
// 这条线其实是是基于原型进行获取的,可以理解成一条基于原型的映射线
// 例如:
// const o = new Object()
// o.constructor === Object --> true
// o.__proto__ = null;
// o.constructor === Object --> false
// 注意: 其实实例上并不是真正有 constructor 这个指针,它其实是从原型链上获取的
// instance.hasOwnProperty('constructor') === false
实例.constructor === 构造函数
6.原型链:
原型:
- 所有引用类型都有一个__proto__(隐式原型)属性,属性值是一个普通的对象
- 所有函数都有一个prototype(原型)属性,属性值是一个普通的对象
- 所有引用类型的__proto__属性指向它构造函数的prototype
原型链: 是由原型对象组成,每个对象都有 proto 属性,指向了创建该对象 的构造函数的原型,proto 将对象连接起来组成了原型链。是一个用来实现 继承和共享属性的有限的对象链。
- a.__proto__和 a.prototype都称做为a的原型。
- Object.prototype是原型链的顶端。
- 如果 a是由b实例化出来的,则有关系a.proto === b.prototype,所以a.__proto__的地址和b.prototype指向的地址是一样的,即他们使用的是同一个对象。
function A(){}
const a=new A()
console.log(a.__proto__===A.prototype)
// true,这里是根据上面 '需要了解'处可以推断
console.log(A.prototype.__proto__===Object.prototype)
// true,这里可以通过打印 A ,然后展开看到 A 就能看到该关系
const arr=[]
console.log(arr.__proto__===Array.prototype)
//true,根据上面 '需要了解'处可以推断
console.log(Array.prototype.__proto__===Object.prototype) // true,打印 Array,然后展开Array就能看到该关系
//其它的原型链如:函数、对象、正则、日期的原型链和上面的基本一致。
Object.prototype.constructor.proto === Function.prototype // true
Function.prototype.proto === Object.prototype // true
Object.prototype.proto === null // true
原型链的作用: 首先你需要只知道的是,处在原型上的对象,是可以顺着自己所在原型链向上找,然后可以使用上面的方法或者属性。主要作用就是减少内存消耗,提高代码的复用率。
属性查找机制: 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着 原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查 找,直至最顶级的原型对象 Object.prototype,如还是没找到,则输出 undefined;
属性修改机制: 只会修改实例对象本身的属性,如果不存在,则进行添加该属性, 如果需要修改原型的属性时,则可以用: b.prototype.x = 2;但是这样会造成 所有继承于该对象的实例的属性发生改变。
7.执行上下文(EC)
执行上下文可以简单理解为一个对象:
它包含三个部分:
-
变量对象(VO)
-
作用域链(词法作用域)
-
this 指向
它的类型:
-
全局执行上下文
-
函数执行上下文
-
eval 执行上下文
如何执行
执行上下文是以栈的方式被存放起来的,称这个栈为执行上下文栈。
在JavaScript代码开始执行时,将全局上下文创建并入栈,当调用函数时,会进入对应函数的环境,创建函数上下文并入栈,当栈顶的执行上下文代码执行完毕后,则将其出栈。
代码执行过程:
-
创建 全局上下文 (global EC)
-
全局执行上下文 (caller) 逐行 自上而下 执行。遇到函数时,函数执行上下文 (callee) 被 push 到执行栈顶层
-
函数执行上下文被激活,成为 active EC, 开始执行函数中的代码,caller 被挂起
-
函数执行完后,callee 被 pop 移除出执行栈,控制权交还全局上下文 (caller),继续执行。
8.变量对象
变量对象,是执行上下文中的一部分,可以抽象为一种 数据作用域,其实也可 以理解为就是一个简单的对象,它存储着该执行上下文中的所有 变量和函数声明(不包含函数表达式)。
在执行上下文的创建阶段会生成变量对象,主要是以下三个过程
- 检索当前上下文中的参数,该过程生成的Arguments对象,并建立形参
- 检索当前上下文中的函数声明,该过程建立以函数名为属性名,函数所在内存地址引用为属性值的属性;
- 检索当前上下文中的变量声明,该过程建立以变量名为属性名,undefined 为属性值的属性(如果变量名跟已声明的形参变量名或函数名相同,则该变量声明不会干扰已经存在的这类属性)。
VO = {
Arguments: {},
ParamVariable: 具体值, //函数传递的形参变量
Function: <function reference>,
Variable:undefined//内部声明的变量
}
活动对象 (AO): 当变量对象所处的上下文为 active EC 时,称为活动对象。当执行上下文进入执行状态后,变量对象会变为活动对象(Active Object,AO),此时声明的变量会进行赋值
AO = {
Arguments: {},
ParamVariable: 具体值, //函数传递的形参变量
Function: <function reference>,
Variable:具体值//内部声明的变量
}
9.作用域链
我们知道,我们可以在执行上下文中访问到父级甚至全局的变量,这便是作用域链的功劳。作用域链可以理解为一组对象列表,包含 父级和自身的变量对象,因此我们便能通过作用域链访问到父级里声明的变量或者函数。
作用域链是指由当前上下文和上下层上下文的一系列变量对象形成的层级链。保证了当前执行环境对符合访问权限的变量和函的有序访问
在执行上下文分成两个阶段,一个是创建一个是执行。在执行上下文阶段,如果需要查找某个变量或者函数时,会从当前上下文中查找,如果没有找到,就会沿着上层上下文进行查找,直到找到全局上下文。
作用域访问就是里面可以访问外面的,外面的不能访问里面的。
执行上下文中的作用域链是如何建立的?
在JavaScript中主要包含了全局作用域和函数作用域,函数作用域是在函数被声明的时候确定的。
每一个函数都会包含一个[[scope]]内部属性,在函数被声明的时候,该函数的[[scope]]属性会保存其上下文的变量对象,形成层级链。[[scope]]属性的值是在函数被声明的时候确定的。
在函数调用的时候,其执行上下文会被创建并且入栈。在创建阶段生成的变量对象,会将该变量对象添加到作用域的顶端并将[[scope]]添加进该作用域链中,在执行阶段,变量对象会变成活动对象,相应属性被赋值。
所以,作用域链是由当前上下文变量对象及上层上下文变量对象组成的。
作用域链的原理和原型链很类似,如果这个变量在自己的作用域中没有,那么它会寻找父级的,直到最顶层。
注意:JS 没有块级作用域,若要形成块级作用域,可通过
(function(){})();
立即执行的形式实现。
10.this指向
this的指向,是在函数被调用的时候确定的。也就是在执行上下文被创建的时候确定的。
this 的指向,最主要的是三种场景,分别是全局上下文中 this、函数中 this 和构造函数中 this。
- 在全局上下文中,this 指代全局对象。
- 如果被调用的函数,被某一个对象所拥有,那么其内部的 this 指向该对象;如果该函数被独立调用,那么其内部的 this 指向 undefined(非严格模式下指向 window)。
- 对于构造函数来说,其内部 this 指向新创建的对象实例。
默认绑定:全局环境中,this 默认绑定到 window。
隐式绑定:一般地,被直接对象所包含的函数调用时,也称为方法调用,this
隐式绑定到该直接对象。
隐式丢失:隐式丢失是指被隐式绑定的函数丢失绑定对象,从而默认绑定到
window。
显式绑定:通过 call()、apply()、bind()方法把对象绑定到 this 上, 叫做显式绑定。
new 绑定:如果函数或者方法调用之前带有关键字 new,它就构成构造函数调用。 对于 this 绑定来说,称为 new 绑定。
11.闭包
要理解闭包就要去理解变量的作用域,在JS中存在两种变量的作用域,一种是全局变量,一种是局部变量。两种变量的区别就是函数内部可以直接读取全局变量,但是在函数外部无法读取函数内部的局部变量。
闭包属于一种特殊的作用域,称为静态作用域。它的定义可以理解为: 父函数 被销毁的情况下,返回出的子函数的[[scope]]中仍然保留着父级的单变量对象 和作用域链,因此可以继续访问到父级的变量对象,这样的函数称为闭包。
**闭包是指有权访问另外一个函数作用域中的局部变量的函数。**声明在一个函数中的函数,叫做闭包函数。而且内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后。
闭包的特点
1、让外部访问函数内部变量成为可能
2、局部变量会常驻在内存中
3、可以避免使用全局变量,防止全局变量污染
4、会造成内存泄漏(有一块内存空间被长期占用,而不被释放)
闭包就是可以创建一个独立的环境,每个闭包里面的环境都是独立的,并且互不干扰。闭包会发生内存泄漏,每次外部函数执行的时候,外部函数的引用地址不同,都会重新创建一个新的地址。但凡是当前活动对象中有被内部子集引用的数据,那么这个时候,这个数据不删除,保留一根指针给内部活动对象。大概意思就是说当外部函数运行结束甚至销毁时,局部的变量key=value,尽管key被垃圾回收机制给回收了,但是value仍不会被回收,会变成一个自由变量留下引用的指针。
为什么要用:
匿名自执行函数:我们知道所有的变量,如果不加上 var 关键字,则默认的会添加到全局对象的属性上去,这样的临时变量加入全局对象有很多坏处,比如:别的函数可能误用这些变量;造成全局对象过于庞大,影响访问速度(因为变量的取值是需要从原型链上遍历的)。除了每次使用变量都是用 var 关键字外,我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护,可以用闭包。
结果缓存:我们开发中会碰到很多情况,设想我们有一个处理过程很耗时的函数对象,每次调用都会花费很长时间,那么我们就需要将计算出来的值存储起来, 当调用这个函数的时候,首先在缓存中查找,如果找不到,则进行计算,然后更新缓存并返回值,如果找到了,直接返回查找到的值即可。闭包正是可以做到这 一点,因为它不会释放外部的引用,从而函数内部的值可以得以保留。
封装:实现类和继承等。
闭包会产生一个很经典的问题:
多个子函数的[[scope]]都是同时指向父级,是完全共享的。因此当父级的变量对象被修改时,所有子函数都受到影响。
解决:
变量可以通过 函数参数的形式 传入,避免使用默认的[[scope]]向上查找
使用 setTimeout 包裹,通过第三个参数传入
使用 块级作用域,让变量成为自己上下文的属性,避免共享
12.对象的拷贝
对象拷贝就是将一个对象的属性拷贝到另一个有着相同属性类类型的对象中去。
主要是为了在新的上下文环境中复用对象的部分或全部数据。 java中有三种类型的对象拷贝:浅拷贝、深拷贝、延迟拷贝。
浅拷贝: 以赋值的形式拷贝引用对象,仍指向同一个地址,修改时原对象也会受到影响;就是按位拷贝,他会创建一个新对象,这个对象有着原始对象属性值的一份精准拷贝。若属性是基本类型,拷贝的就是基本类型的值;若属性是内存地址(引用类型),拷贝的就是内存地址,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
浅拷贝的实现方法:
-
Object.assign
-
展开运算符(…)
深拷贝: 完全拷贝一个新对象,修改时原对象不再受到任何影响;深拷贝会拷贝所有的属性,并拷贝属性指向动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比浅拷贝速度较慢并且花销较大。
深拷贝的实现方法:
JSON.parse(JSON.stringify(obj)): 性能最快
- 具有循环引用的对象时,报错
- 当值为函数、undefined、或 symbol 时,无法拷贝
用for…in实现遍历和复制
利用数组的Array.prototype.forEach进copy
延迟拷贝:是浅拷贝和深拷贝的一个组合。当最开始拷贝一个对象时,会使用速度较快的浅拷贝,还会使用一个计数器来记录有多少对象共享这个数据,当程序想要修改原始的对象时,它会决定数据是否被共享并根据需要进行深拷贝。
延迟拷贝从外面看起来就是深拷贝,但是只要有可能他就会利用浅拷贝的速度。当原始对象中,引用不经常改变的时候可以使用延迟拷贝。由于存在计数器,效率很高,但只是常量级的开销。
13.new 运算符的执行过程
1. 创建了一个空的对象
2. 将空对象的原型,指向于构造函数的原型
3. 将空对象作为构造函数的上下文(改变this指向)
4. 对构造函数有返回值的处理判断
function Animal (name) {
this.name = name;
}
var cat = new Animal('cat');
/*var cat = new Animal('cat');是关键代码,js引擎在执行这句代码的时候,内部做了很多工作,用伪代码模拟一下其内部流程如下:
new Animal('cat') = {
var obj = {};
obj.__proto__ = Animal.prototype;
var result = Animal.call(obj, 'cat');
return typeof result === 'object' ? result : obj;
}*/
-
新生成一个对象:窗口一个空对象obj;
-
链接到原型:把obj的__proto__指向了构造函数的Animal的原型对象prototype,此时便建立obj的原型链;obj -> Animal.prototype -> Object.prototype -> null
-
绑定this.call,apply,bind:在obj对象的执行环境中调用Animal函数并传递参数cat,相当于var result = obj.Animal(‘cat’),当执行完这句话后obj就产生了属性name并赋值“cat”。
-
返回新对象(如果构造函数有自己的return,则返回该值):观察第三步的返回值,如果无返回值或者返回一个非对象值,则将obj作为新对象返回,否则会将result作为新对象返回。
在javascript中,万物皆对象,但是为什么还要new创建对象?
通过new创建的对象和构造函数直接建立一条原型链,原型链的建立,让原本孤独的对象有了依赖关系和继承能力,让javascript对象能以更合适的方式来映射真实世界里的对象,这是面向对象的本质。
14.instanceof 原理
作用:
- 用于判断某个实例是否属于某构造函数
- 在继承关系中用来判断一个实例是否属于它的父类型或者祖先类型的实例
查找构造函数的原型对象是否在实例对象的原型链上,如果在返回true,如果不在返回false。
说白了,只要右边变量的 prototype 在左边变量的原型链上即可。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false。
即:
// __proto__: 代表原型对象链
instance.[__proto__...] === instance.constructor.prototype
// return true
15.代码的复用
当你发现任何代码开始写第二遍时,就要开始考虑如何复用。一般有以下的方式:
-
函数封装
-
继承
-
复制 extend
-
混入 (mix-in)
-
借用 apply/call
16.继承
原型链继承
核心:创建父类实例对象作为子类原型
优点:可以访问父类原型上的方法或属性,实现了方法复用
缺点:创建子类实例时,不能传父类的参数(比如name),子类实例共享了父类构造函数的属性值,
function Parent(){
this.age = 20;
}
function Child(){
this.name = '张三'
}
Child.prototype = new Parent();
let o2 = new Child();
console.log( o2,o2.name,o2.age );
借用构造函数来继承
核心:在子构造函数中调用父构造函数
优点:解决了原型继承的缺点 (使用构造函数来继承可以传父类的参数,可以解决子类共享父类构造函数中属性的问题)
缺点:子类无法访问父类原型中的方法和属性。(这个原型链继承可以解决)
function Parent(){
this.age = 22;
}
function Child(){
this.name = '张三'
Parent.call(this);
}
let o3 = new Child();
console.log( o3,o3.name,o3.age );
构造函数+原型链 组合继承
核心:使用原型链实现对原型属性和方法的继承,而通过构造函数来实现对实例属性的继承。
缺点:调用了两次父类构造函数,会存在多一份的父类实例属性。
function Parent(){
this.age = 100;
}
function Child(){
Parent.call(this);
this.name = '张三'
}
Child.prototype = new Parent();
let o4 = new Child();
console.log( o4,o4.name,o4.age );
原型式继承
优点是:不需要单独创建构造函数。
缺点是:属性中包含的引用值始终会在相关对象间共享,子类实例不能向父类传参
方法一:
借用构造函数在一个函数A内部创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。
本质上,函数A是对传入的对象执行了一次浅复制。
function createObject(obj) {
function Fun() {}
Fun.prototype = obj
return new Fun()
}
let person = {
name: 'cxk',
age: 18,
hoby: ['唱', '跳'],
showName() {
console.log('my name is:', this.name)
}
}
let child1 = createObject(person)
child1.name = 'jntm'
child1.hoby.push('rap','篮球')
let child2 = createObject(person)
console.log(child1)
console.log(child2)
console.log(person.hoby) // ['唱', '跳', 'rap','篮球']
方法二:Object.create()
Object.create() 是把现有对象的属性,挂到新建对象的原型上,新建对象为空对象
ECMAScript 5通过增加Object.create()方法将原型式继承的概念规范化了。这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。在只有一个参数时,Object.create()与这里的函数A方法效果相同。
let person = {
name: 'cxk',
age: 19,
hoby: ['唱', '跳'],
showName() {
console.log('my name is: ', this.name)
}
}
let child1 = Object.create(person)
child1.name = 'jntm'
child1.hoby.push('rap','篮球')
let child2 = Object.create(person)
console.log(child1)
console.log(child2)
console.log(person.hoby) // ['唱', '跳', 'rap','篮球']
寄生式继承
寄生式继承的思路与(寄生) `原型式继承` 和 `工厂模式` 似, 即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。
优点:写法简单,不需要单独创建构造函数。
缺点:通过寄生式继承给对象添加函数会导致函数难以重用。使用寄生式继承来为对象添加函数, 会由于不能做到函数复用而降低效率;这一点与构造函数模式类似.
使用原型式继承实现对目标对象的浅复制,同时增强浅复制的能力
function objectCopy(obj) {
function Fun() { };
Fun.prototype = obj;
return new Fun();
}
function createAnother(original) {
let clone = objectCopy(original);
clone.getName = function () {
console.log(this.name);
};
return clone;
}
let person = {
name: "yhd",
friends: ["rose", "tom", "jack"]
}
let person1 = createAnother(person);
person1.friends.push("lily");
console.log(person1.friends); // ["rose", "tom", "jack", "lily"]
person1.getName(); // yhd
let person2 = createAnother(person);
console.log(person2.friends); // ["rose", "tom", "jack", "lily"]
寄生组合式继承
前面讲过,组合继承是常用的经典继承模式,不过,组合继承最大的问题就是无论什么情况下,都会调用两次父类构造函数;一次是在创建子类型的时候,一次是在子类型的构造函数内部。寄生组合继承就是为了降低父类构造函数的开销而实现的。
通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
优点是:高效率只调用一次父构造函数,并且因此避免了在子原型上面创建不必要,多余的属性。与此同时,原型链还能保持不变;
缺点是:代码复杂
function objectCopy(obj) {
function Fun() { };
Fun.prototype = obj;
return new Fun();
}
function inheritPrototype(child, parent) {
let prototype = objectCopy(parent.prototype);
// 创建对象
// 这两行代码形成闭环,prototype上的属性constructor指向child构造函数,child构造函数上的属性prototype 指向它的原型prototype对象
prototype.constructor = child; // 增强对象
Child.prototype = prototype; // 赋值对象
}
function Parent(name) {
this.name = name;
this.friends = ["rose", "lily", "tom"]
}
Parent.prototype.sayName = function () {
console.log(this.name);
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
//与组合继承的区别在于这一句代码
inheritPrototype(Child, Parent);
// 在组合式继承中,是继承父类方法
// Child.prototype = new Parent();
Child.prototype.sayAge = function () {
console.log(this.age);
}
let child1 = new Child("yhd", 23);
child1.sayAge(); // 23
child1.sayName(); // yhd
child1.friends.push("jack");
console.log(child1.friends); // ["rose", "lily", "tom", "jack"]
let child2 = new Child("yl", 22)
child2.sayAge(); // 22
child2.sayName(); // yl
console.log(child2.friends); // ["rose", "lily", "tom"]
class+extends继承
核心: ES6继承的结果和寄生组合继承相似,本质上,ES6继承是一种语法糖。但是,寄生组合继承是先创建子类实例this对象,然后再对其增强;而ES6先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
ES6继承与ES5继承的异同:
相同点:本质上ES6继承是ES5继承的语法糖。
不同点:
ES6继承中子类的构造函数的原型链指向父类的构造函数,ES5中使用的是构造函数复制,没有原型链指向。
ES6子类实例的构建,基于父类实例,ES5中不是。
class Parent{
constructor(){
this.age = 18;
}
}
class Child extends Parent{
constructor(){
super();
this.name = '张三';
}
}
let o1 = new Child();
console.log( o1,o1.name,o1.age );
17.类型转换
各种运算符对数据类型是有要求的,如果运算的类型与预期不符合,就会触发类型转换机制
通俗来说,就是把一种数据类型的变量转换成另外一种数据类型。
转换为字符串类型
转换为数字型
转换为布尔型
方式 | 说明 | 案例 |
---|---|---|
Boolean()函数 | 其他类型转成布尔值 | Boolean(“true”) |
- 代表空、否定的值会被转换为 false ,如 ’ '、0、NaN、null、undefined
- 其余值都会被转换为 true
18.类型判断
判断 Target 的类型,单单用 typeof 并无法完全满足,这其实并不是 bug,本质原因是 JS 的万物皆对象的理论。因此要真正完美判断时,我们需要区分对待:
基本类型(null): 使用 String(null) (string / number / boolean / undefined) + function: 直接使用 typeof 即可 ,
其余引用类型(Array / Date / RegExp Error): 调用 toString 后根据[object XXX]进行判断
Object.prototype.toString()
toString()是Object的原型方法,通过调用该方法会返回[[Class]],格式为[object xxx],其中xxx就是该对象的类型;判断Object对象类型可直接调用toString()方法;其他对象需要通过call、apply进行调用
Object.prototype.toString.call(999) // "[object Number]"
Object.prototype.toString.call('') // "[object String]"
Object.prototype.toString.call(Symbol()) // "[object Symbol]"
Object.prototype.toString.call(42n) // "[object BigInt]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(true) // "[object Boolean]"
instanceof: 运算符用来检测构造函数的prototype属性是否出现在某个实例对象的原型链上
constructor: 当定义一个函数时,函数会拥有prototype原型,原型中拥有一个constructor属性,指向prototype原型,用constructor判断是否属于某个数据类型,是返回true,不是返回false
//可以左边放你要判断的内容,右边放类型来进行JS类型判断,只能用来判断复杂数据类型,
[1,2] instanceof Array // true
(function(){}) instanceof Function // true
({a:1}) instanceof Object // true
(new Date) instanceof Date // true
//obj instanceof Object方法也可以判断内置对象。
缺点:在不同window或者iframe间,不能使用instanceof。
19.模块化
1.什么是模块?
- 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
- 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信
模块化开发在现代开发中已是必不可少的一部分,它大大提高了项目的可维护、 可拓展和可协作性。通常,我们 在浏览器中使用 ES6 的模块化支持,在 Node 中 使用 commonjs 的模块化支持。
模块化的好处
- 避免命名冲突(减少命名空间污染)
- 更好的分离, 按需加载
- 更高复用性
- 高可维护性
分类:
-
es6: import / export
-
commonjs: require / module.exports / exports
-
amd: require / defined
require 与 import 的区别
-
require 支持 动态导入,import 不支持,正在提案 (babel 下可支持)
-
require 是 同步 导入,import 属于 异步 导入
-
require 是 值拷贝,导出值变化不会影响导入值;import 指向 内存地址,导入值会随导出值而变化
20.防抖与节流
防抖与节流函数是一种最常用的 高频触发优化方式,能对性能有较大的帮助。本质上是优化高频率执行代码的一种手段
防抖 (debounce): 将多次高频操作优化为只在最后一次执行,通常使用的场景 是:用户输入,只需再输入完成后做一次输入校验即可。
function debounce(fn, wait, immediate) {
let timer = null
return function() {
let args = arguments
let context = this
if (immediate && !timer) {
fn.apply(context, args)
}
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(context, args)
}, wait)
}
}
节流(throttle): 每隔一段时间后执行一次,也就是降低频率,将高频操作优化 成低频操作,通常使用场景: 滚动条事件 或者 resize 事件,通常每隔 100~500 ms 执行一次即可。
function throttle(fn, wait, immediate) {
let timer = null
let callNow = immediate
return function() {
let context = this,
args = arguments
if (callNow) {
fn.apply(context, args)
callNow = false
}
if (!timer) {
timer = setTimeout(() => {
fn.apply(context, args)
timer = null
}, wait)
}
}
}
21.函数执行改变 this
由于 JS 的设计原理: 在函数中,可以引用运行环境中的变量。因此就需要一个 机制来让我们可以在函数体内部获取当前的运行环境,这便是 this。
因此要明白 this 指向,其实就是要搞清楚 函数的运行环境,说人话就是,谁 调用了函数。例如:
obj.fn(),便是 obj 调用了函数,既函数中的 this === obj
fn(),这里可以看成 window.fn(),因此 this === window
但这种机制并不完全能满足我们的业务需求,因此提供了三种方式可以手动修改 this 的指向:
call: fn.call(target, 1, 2)
apply: fn.apply(target, [1, 2])
bind: fn.bind(target)(1,2)
共同点:功能一致 可以改变this指向
区别:
1. call、apply可以立即执行。bind不会立即执行,因为bind返回的是一个函数需要加入()执行。
2. 参数不同:apply第二个参数是数组。call和bind有多个参数需要挨个写。
22.ES6/ES7
ES6, 全称 ECMAScript 6.0 ,是 JavaScript 的下一个版本标准,2015.06 发版
let 与 const
var:ES5中用于声明变量的关键字,存在各种问题(例如:红杏出墙~)
let:ES6新增,用于声明变量,有块级作用域
var存在的问题:
// 1.声明提升
// 此处会正常打印,但这是错误的(属于先上车后买票了!)
console.log(name);
var name = "大帅比";
// 2. 变量覆盖
var demo = "小明";
var demo = "小红";
// 此处会打印小红,这也是错误的(属于套牌车,违法的啊,兄弟)
// 同一个项目中,发生变量覆盖可能会导致数据丢失以及各种不可预知的bug,原则上来说:变量不能重名
console.log(demo)
// 3. 没有块级作用域
function fn2(){
for(var i = 0; i < 5; i++){
// do something
}
// 此处会正常打印出 i 的值,这是错误的
// i是定义在循环体之内的,只能在循环体内打印,当前现象叫做红杏出墙!!!
console.log(i);
}
fn2();
let不会存在上述问题:
// 1. 不会存在声明提前
// 此处会报错(这里必须报错,原则上来说不能先上车后买票)
console.log(name);
let name = "大帅比";
// 2. 不会有变量覆盖
let demo = "小明";
let demo = "小红";
// 此处会报错(不能使用套牌车!)告诉你已经定义了此变量。避免了项目中存在变量覆盖的问题
console.log(demo)
// 3. 有块级作用域
function fn2(){
for(let i = 0; i < 5; i++){
// do something
}
// 此处会报错,无法打印,防止红杏出墙!!!
// i是定义在循环体之内的,循环体外当然无法打印
console.log(i);
}
fn2();
const 声明一个只读的常量,一旦声明,常量的值就不能改变
- 一般用于全局变量
- 通常变量名全部大写(请按照规则来,不要乱搞,容易出事情)
const PI = "3.1415926";
const修饰的基本类型不能改变,而其修饰的对象、函数、数组等引用类型,可以改变内部的值,但不能改变其引用。
解构赋值
- 解构赋值是对赋值运算符的扩展
- 针对数组或者对象进行模式匹配,然后对其中的变量进行赋值
- 代码简洁且易读,语义更加清晰明了,方便了复杂对象中数据字段获取(简而言之:用起来很爽!)
//用在数组上
let [a, b, c] = [1, 2, 3];
// a = 1,b = 2,c = 3 相当于重新定义了变量a,b,c,取值也更加方便
// , = 占位符
let arr = ["小明", "小花", "小鱼", "小猪"];
let [,,one] = arr; // 这里会取到小鱼
// 解构整个数组
let strArr = [...arr];
// 得到整个数组
console.log(strArr);
//用在对象上
let obj = {
className : "卡西诺",
age: 18
}
let {className} = obj; // 得到卡西诺
let {age} = obj; // 得到18
// 剩余运算符
let {a, b, ...demo} = {a: 1, b: 2, c: 3, d: 4};
// a = 1
// b = 2
// demo = {c: 3, d: 4}
模板字面量
- 利用模板字面量,我们可以直接在字符串使用特殊字符而不用转义它们。用反引号 ``
- 模板字面量还支持插值,可以轻松完成连接字符串和值的任务:
- 除了作为普通字符串,还可以用来定义多行字符串,可以在字符串中加入变量和表达式
// 普通字符串
let string = "hello"+"小兄弟"; // hello小兄弟
// 如果想要换行
let string = "hello'\n'小兄弟"
// hello
// 小兄弟
//模板字面量
let str1 = "穿堂而过的";
let str2 = "风";
// 模板字面量
let newStr = `我是${str1}${str2}`;
// 我是穿堂而过的风
console.log(newStr)
// 字符串中调用方法
function fn3(){
return "帅的不行!";
}
let string2= `我真是${fn3 ()}`;
console.log(string2); // 我真是帅的不行!
//模板字面量可以保留多行字符串,我们无需显式的放置它们:
let text = ( `cat
dog
nickelodeon`
);
//模板字面量可以接受表达式, 比如:
ES6 函数
箭头函数
箭头函数是一种更加简洁的函数书写方式
箭头函数本身没有作用域(无this)
箭头函数的this指向上一层,上下文决定其this
基本语法:参数 => 函数体
a. 基本用法
let fn = v => v;
//等价于
let fn = function(num){
return num;
}
fn(100); // 输出100
b. 带参数的写法
let fn2 = (num1,num2) => {
let result = num1 + num2;
return result;
}
fn2(3,2); // 输出5
c. 箭头函数中的this指向问题
箭头函数体中的 this 对象,是定义函数时的对象,而不是使用函数时的对象。在函数定义的时候就已经决定了
function fn3(){
setTimeout(()=>{
// 定义时,this 绑定的是 fn3 中的 this 对象
console.log(this.a);
},0)
}
var a = 10;
// fn3 的 this 对象为 {a: 10},因为它指向全局: window.a
fn3.call({a: 18}); // 改变this指向,此时 a = 18
d. 箭头函数适用的场景
当我们代码里存在这样的代码:let self = this;
需要新建变量去保存this的时候
案例如下:
let Person1 = {
'age': 18,
'sayHello': function () {
setTimeout(()=>{
console.log(this.age);
});
}
};
var age = 20;
Person1.sayHello(); // 18
函数参数的扩展
1. 默认参数
// num为默认参数,如果不传,则默认为10
function fn(type, num=10){
console.log(type, num);
}
fn(1); // 打印 1,10
fn(1,2); // 打印 1,2 (此值会覆盖默认参数10)
需要注意的是:只有在未传递参数,或者参数为 undefined 时,才会使用默认参数,null 值被认为是有效的值传递。
2. 不定参数
// 此处的values是不定的,且无论你传多少个
function f(...values){
console.log(values.length);
}
f(1,2); // 2
f(1,2,3,4); // 4
Class类
- class (类)作为对象的模板被引入,可以通过 class 关键字定义类
- class 的本质是 function,同样可以看成一个块
- 可以看作一个语法糖,让对象原型的写法更加清晰
- 更加标准的面向对象编程语法
//类的定义
// 匿名类
let Demo = class {
constructor(a) {
this.a = a;
}
}
// 命名类
let Demo = class Demo {
constructor(a) {
this.a = a;
}
}
//类的声明
class Demo {
constructor(a) {
this.a = a;
}
}
// 请注意,类不能重复声明
// 类定义不会被提升,必须在访问前对类进行定义,否则就会报错。
// 类中方法不需要 function 关键字。
// 方法间不能加分号
//类的主体
//公共属性(依然可以定义在原型上)
class Demo{}
Demo.prototype.a = 2;
//实例属性
class Demo {
a = 2;
constructor () {
console.log(this.a);
}
}
//方法:constructor
class Demo{
constructor(){
console.log('我是构造器');
}
}
new Demo(); // 我是构造器
//如果不写constructor,也会默认添加
//实例化对象
class Demo {
constructor(a, b) {
this.a = a;
this.b = b;
console.log('Demo');
}
sum() {
return this.a + this.b;
}
}
let demo1 = new Demo(2, 1);
let demo2 = new Demo(3, 1);
// 两者原型链是相等的
console.log(demo1._proto_ == demo2._proto_); // true
demo1._proto_.sub = function() {
return this.a - this.b;
}
console.log(demo1.sub()); // 1
console.log(demo2.sub()); // 2
用关键字extends实现继承:
使用ES6的语法创建类模糊了后台的实现和原型如何工作,这个好特性可以使我们的代码更整洁。
Map()
Map是一组键值对的结构,具有极快的查找速度。
map()方法返回一个新数组,数组中的元素为原始数组元素调用函数处理的后值。
map()方法按照原始数组元素顺序依次处理元素。
注意: map不会对空数组进行检测
map不会改变原始数组
函数的作用是对数组中的每一个元素进行处理,返回新的元素。
//Maps 和 Objects 的区别
一个 Object 的键只能是字符串或者 Symbols,但一个 Map 的键可以是任意值
Map 中的键值是有序的(FIFO 原则),而添加到对象中的键则不是
Map 的键值对个数可以从 size 属性获取,而 Object 的键值对个数只能手动计算
//Map中的key
// 1. key是字符串
//Map不再限制我们只使用字符串作为键,我们现在可以使用任何类型作为键而不会发生类型转换。
let myMap = new Map();
let keyString = "string";
myMap.set(keyString, "和键'string'关联的值");
// 添加新的key-value
// keyString === 'string'
myMap.get(keyString); // "和键'string'关联的值"
myMap.get("string"); // "和键'string'关联的值"
// 2.key是对象
//Map允许我们使用set、get和search(等等)访问属性值。
let myMap = new Map();
let keyObj = {},
myMap.set(keyObj, "和键 keyObj 关联的值");
myMap.get(keyObj); // "和键 keyObj 关联的值"
myMap.get({}); // undefined, 因为 keyObj !== {}
// 3. key也可以是函数或者NaN
//Map 的迭代
// 1.使用 forEach
let myMap = new Map();
myMap.set(0, "zero");
myMap.set(1, "one");
// 0 = zero , 1 = one
myMap.forEach(function(value, key) {
console.log(key + " = " + value);
}, myMap)
// 2. 也可以使用 for...of
//Map 与 Array的转换
let kvArray = [["key1", "value1"], ["key2", "value2"]];
// Map 构造函数可以将一个 二维 键值对数组转换成一个 Map 对象
let myMap = new Map(kvArray);
// 使用 Array.from 函数可以将一个 Map 对象转换成一个二维键值对数组
let outArray = Array.from(myMap);
//关于map的重点面试题
请谈一下 Map和ForEach 的区别(问到map,必定问到此题)
详细解析:
forEach()方法不会返回执行结果,而是undefined
map()方法会得到一个新的数组并返回
同样的一组数组,map()的执行速度优于 forEach()(map() 底层做了深度优化)
性质决定了两者应用场景的不同
forEach() 适合于你并不打算改变数据的时候,而只是想用数据做一些事情(比如存入数据库)
let arr = ['a', 'b', 'c', 'd'];
arr.forEach((val) => {
console.log(val); // 依次打印出 a,b,c,d
});
map() 适用于你要改变数据值的时候,它更快,而且返回一个新的数组
let arr = [1, 2, 3, 4, 5];
let arr2 = arr.map(num => num * 2).filter(num => num > 5);
// arr2 = [6, 8, 10]
用块(Blocks)替换立即执行函数(IIFEs)
立即执行函数常被用作闭包,把变量控制在作用域内。在ES6中,我们可以创建一个块作用域而且不仅仅是基于函数的作用域。
(function () {
var food = 'Meow Mix';
}());
console.log(food); // Reference Error
Using ES6 Blocks:
{
let food = 'Meow Mix';
};
console.log(food); // Reference Error
字符串(Strings)
在ES6中, 标准库也在不断的扩充。在这些变化中就有很多方法可以用于字符串,
// .includes()
var string = 'food';
var substring = 'foo';
console.log(string.indexOf(substring) > -1);
检测返回值是否大于-1表示字符串是否存在,我们可以用.includes()替换,它返回一个boolean值。
const string = 'food';
const substring = 'foo';
console.log(string.includes(substring)); // true
// .repeat()
function repeat(string, count) {
var strings = [];
while(strings.length < count) {
strings.push(string);
}
return strings.join('');
}
在ES6中,我们一种更简洁的实现方法:
// String.repeat(numberOfRepetitions)
'meow'.repeat(3); // 'meowmeowmeow'
模块(Modules)
ES6之前,我们只用如Browserify的库在客户端创建模块,并且需要用到Node.js。利用ES6,我们现在可以直接使用任何类型的模块(AMD和CommonJS)
CommonJs 是服务器端模块的规范,Node.js采用了这个规范。
根据CommonJS规范,一个单独的文件就是一个模块。加载模块使用require方法,该方法读取一个文件并执行,最后返回文件内部的exports对象。
CommonJS 加载模块是同步的,所以只有加载完成才能执行后面的操作。像Node.js主要用于服务器的编程,加载的模块文件一般都已经存在本地硬盘,所以加载起来比较快,不用考虑异步加载的方式,所以CommonJS规范比较适用。但如果是浏览器环境,要从服务器加载模块,这是就必须采用异步模式。所以就有了 AMD CMD 解决方案。
AMD((Asynchromous Module Definition)
AMD 是 RequireJS 在推广过程中对模块定义的规范化产出 AMD异步加载模块。它的模块支持对象、函数、构造器、字符串、JSON等各种类型的模块。
适用AMD规范适用define方法定义模块。
AMD规范允许输出模块兼容CommonJS规范
CMD
CMD是SeaJS 在推广过程中对模块定义的规范化产出
CMD和AMD的区别有以下几点:
1.对于依赖的模块AMD是提前执行,CMD是延迟执行。不过RequireJS从2.0开始,也改成可以延迟执行(根据写法不同,处理方式不通过)。
2.CMD推崇依赖就近,AMD推崇依赖前置。
虽然 AMD也支持CMD写法,但依赖前置是官方文档的默认模块定义写法。
3.AMD的api默认是一个当多个用,CMD严格的区分推崇职责单一。
例如:AMD里require分全局的和局部的。CMD里面没有全局的 require,提供 seajs.use()来实现模块系统的加载启动。CMD里每个API都简单纯粹。
// CommonJS中的exports
module.exports = 1;
module.exports = { foo: 'bar' };
module.exports = ['foo', 'bar'];
module.exports = function bar () {};
ES6中的export
在ES6中,提供各种不同类型的exports,我们可以运行如下:
export let name = 'David';
export let age = 25;
输出对象列表:
function sumTwo(a, b) {
return a + b;
}
function sumThree(a, b, c) {
return a + b + c;
}
export { sumTwo, sumThree };
我们也可以简单地通过export关键字输出函数、对象和值(等等):
export function sumTwo(a, b) {
return a + b;
}
export function sumThree(a, b, c) {
return a + b + c;
}
And lastly, we can export default bindings:
function sumTwo(a, b) {
return a + b;
}
function sumThree(a, b, c) {
return a + b + c;
}
let api = {
sumTwo,
sumThree
};
输出默认api:
/* Which is the same as
* export { api as default };
*/
建议:在模块结束的地方,始终输出默认的方法。这样可以清晰地看到接口,并且通过弄清楚输出值的名称节省时间。所以在CommonJS模块中通常输出一个对象或值。坚持使用这种模式,会使我们的代码易读,并且可以轻松的在ES6和CommonJS中进行插补。
ES6
ES6 提供提供各种不同的imports,我们输入一整个文件:
import 'underscore';
这里值得注意的是,简单的输入一文件会在文件的最顶层执行代码。和Python类似,我们已经命名了imports:
import { sumTwo, sumThree } from 'math/addition';
我们还可以重命名这些已经有名的imports:
import {
sumTwo as addTwoNumbers,
sumThree as sumThreeNumbers
} from 'math/addition';
此外,我们可以输入各种东西(也叫做 namespace import)
import * as util from 'math/addition';
最后,我们可以从模块输入一列值:
import * as additionUtil from 'math/addition';
const { sumTwo, sumThree } = additionUtil;
如下这样从默认绑定进行输入
import api from 'math/addition';
// 例如: import { default as api } from 'math/addition';
虽然最好要保持输出简单,但是如果我们需要,我们有时可以混用默认输入,当我们想如下输出的时候:
// foos.js
export { foo as default, foo1, foo2 };
我们可以如下输入它们:
import foo, { foo1, foo2 } from 'foos';
当使用commonj语法(如React)输入一个模型的输出时,我们可以这样做:
import React from 'react';
const { Component, PropTypes } = React;
这个也可以进一步简化,使用:
import React, { Component, PropTypes } from 'react';
注意:输出的值是绑定,不是引用。因此,绑定的值发生变化会影响输出的模型中的值。避免修改这些输出值的公共接口。
参数(Parameters)
在ES5中,我们可以很多方法操作函数参数的默认值、未定义的参数和有定义的参数。在ES6中,我们可以用更简单的语法实现这一切。
默认参数(Default Parameters)
function addTwoNumbers(x, y) {
x = x || 0;
y = y || 0;
return x + y;
}
在ES6中,我们可以简单的把默认值赋给参数:
function addTwoNumbers(x=0, y=0) {
return x + y;
}
addTwoNumbers(2, 4); // 6
addTwoNumbers(2); // 2
addTwoNumbers(); // 0
剩余参数(Rest Parameters)
在ES5中,我们这样操作一个未定义的参数:
function logArguments() {
for (var i=0; i < arguments.length; i++) {
console.log(arguments[i]);
}
}
使用休止符(…)我们可以传入大量未定义的参数:
function logArguments(...args) {
for (let arg of args) {
console.log(arg);
}
}
已命名的参数(Named Parameters)
ES5中,一种处理已命名参数的方式是使用选项方式,这种方法来自jQuery。
function initializeCanvas(options) {
var height = options.height || 600;
var width = options.width || 400;
var lineStroke = options.lineStroke || 'black';
}
通过解构成正式参数的方式,我们可以实现同样的功能:
function initializeCanvas(
{ height=600, width=400, lineStroke='black'}) {
// Use variables height, width, lineStroke here
}
如果我们想使全部参数值可选,我们可以用一个空对象这样结构:
function initializeCanvas(
{ height=600, width=400, lineStroke='black'} = {}) {
// ...
}
展开运算符(Spread Operator)
在ES5中,查找一个array中的最大值需要使用Math.max的apply方法:
Math.max.apply(null, [-1, 100, 9001, -32]); // 9001
在es6中,我们使用展开运算符将array传递给函数作为参数:
Math.max(...[-1, 100, 9001, -32]); // 9001
我们可以用这样简洁的语法链接数组字面量:
let cities = ['San Francisco', 'Los Angeles'];
let places = ['Miami', ...cities, 'Chicago']; // ['Miami', 'San Francisco', 'Los Angeles', 'Chicago']
Symbols
Symbol在ES6之前就已经出现了, 但是现在我们有了一个公共的接口可以直接使用。Symbol是唯一且不可改变的值,被用作哈希中的键。
调用Symbol()或者Symbol(description)会创建一个不能在全局查找的独一无二的符号。一种使用symbol()的情况是,利用自己的逻辑修补第三方的对象或命名空间,但不确定会不会在库更新时产生冲突。
例如,如果你想添加一个方法refreshCompontent到React.Component,并且确信这个方法他们不会在以后的更新中添加。
const refreshComponent = Symbol();
React.Component.prototype[refreshComponent] = () => {
// do something
}
Symbol.for(key)
Symbol.for(key) 依然会创建一个唯一且不能修改的Symbol,但是它可以在全局被查找。两次调用相同的Symbol.for(key) 会创建一样的Symbol实例。注意,他和Symbol(description)不是相同的:
Symbol('foo') === Symbol('foo') // false
Symbol.for('foo') === Symbol('foo') // false
Symbol.for('foo') === Symbol.for('foo') // true
一个常见的symbol方法Symbol.for(key)是可互操作的。(使用这个方法)这个可以通过使用自己的代码在包括已知接口的第三方的对象参数中查找symbol成员实现,例如:
function reader(obj) {
const specialRead = Symbol.for('specialRead');
if (obj[specialRead]) {
const reader = obj[specialRead]();
// do something with reader
} else {
throw new TypeError('object cannot be read');
}
}
在另一个库中:
const specialRead = Symbol.for('specialRead');
class SomeReadableType {
[specialRead]() {
const reader = createSomeReaderFrom(this);
return reader;
}
}
ES6中,一个值得注意的是关于Symbol的互操作性的例子是Symbol.iterator,它存在于Arrays、Strings、Generators等等的所有可迭代类型中。当作为一个方法调用的时候,它会返回一个具有迭代器接口的对象。
WeakMaps
ES6之前,为了保存私有数据,我们采取了很多方式。其中一个方法就是命名转换:
class Person {
constructor(age) {
this._age = age;
}
_incrementAge() {
this._age += 1;
}
}
但是命名转换会引起代码库混乱,并且不能保证总是被支持。为此,我们使用WeakMaps存储数据:
let _age = new WeakMap();
class Person {
constructor(age) {
_age.set(this, age);
}
incrementAge() {
let age = _age.get(this) + 1;
_age.set(this, age);
if (age > 50) {
console.log('Midlife crisis');
}
}
}
使用WeakMap存储数据时的一个很有趣的事情是,这个key不会暴露出属性名,需要使用Reflect.ownKeys()实现:
> const person = new Person(50);
> person.incrementAge(); // 'Midlife crisis'
> Reflect.ownKeys(person); // []
使用WeakMap更实际的例子是在不污染DOM自身的情况下存储与DOM元素相关的数据:
let map = new WeakMap();
let el = document.getElementById('someElement');
// 给元素存一个弱引用
map.set(el, 'reference');
// 获得元素的值
let value = map.get(el); // 'reference'
// 移除引用
el.parentNode.removeChild(el);
el = null;
// 元素被回收后,map是空的
如上所示,当一个对象被GC回收后,WeakMap会自动移除以其为标识符的键值对。
注意:为了进一步说明这个例子的实用性。当一个与DOM对应的对象的具有引用时,考虑jQuery如何存储它。使用WeakMaps,jQuery可以在DOM元素被删除时自动释放与之关联的内存。总而言之,对任何库而言,WeakMaps对操作DOM元素是非常实用的。
Promises
Promise允许我们把水平的代码(回调函数的地狱):
func1(function (value1) {
func2(value1, function (value2) {
func3(value2, function (value3) {
func4(value3, function (value4) {
func5(value4, function (value5) {
// Do something with value 5
});
});
});
});
});
转换为竖直的代码:
func1(value1)
.then(func2)
.then(func3)
.then(func4)
.then(func5, value5 => {
// Do something with value 5
});
在ES6之前,我们使用bluebird或是Q,现在我们有了Promises:
new Promise((resolve, reject) =>
reject(new Error('Failed to fulfill Promise')))
.catch(reason => console.log(reason));
这里我们有2个handlers,resolve(Promise执行成功时调用的函数)和reject(Promise失败时调用的函数)。
使用Promise的好处:使用嵌套的回调函数处理错误会很混乱。使用Promise,我们可以很清晰的使错误冒泡,并且就近处理它们。更好的是,在它处理成功(或失败)之后Promise的值是不可修改的。
以下是个使用Promise的实例:
var request = require('request');
return new Promise((resolve, reject) => {
request.get(url, (error, response, body) => {
if (body) {
resolve(JSON.parse(body));
} else {
resolve({});
}
});
});
我们可以使用Promise.all()并行的处理一个异步操作数组:
let urls = [
'/api/commits',
'/api/issues/opened',
'/api/issues/assigned',
'/api/issues/completed',
'/api/issues/comments',
'/api/pullrequests'
];
let promises = urls.map((url) => {
return new Promise((resolve, reject) => {
$.ajax({ url: url })
.done((data) => {
resolve(data);
});
});
});
Promise.all(promises)
.then((results) => {
// Do something with results of all our promises
});
Generators
和Promise使我们避免回调函数的地狱相似,Generators可以扁平化我们的代码——给我们一种同步执行异步代码的感觉,Generators是个很重要的函数,它使我们可以暂停操作的执行,随后返回表达式的值。
下面是使用Generator的一个简单例子:
function* sillyGenerator() {
yield 1;
yield 2;
yield 3;
yield 4;
}
var generator = sillyGenerator();
> console.log(generator.next()); // { value: 1, done: false }
> console.log(generator.next()); // { value: 2, done: false }
> console.log(generator.next()); // { value: 3, done: false }
> console.log(generator.next()); // { value: 4, done: false }
这里,next()使我们generator继续推进,并且得到新的表达式的值(译注:每次推进到下一个yield值)。当然,上面的例子很牵强,我们可以利用Generator以同步的方式写异步代码:
// 利用Generator屏蔽异步过程
function request(url) {
getJSON(url, function(response) {
generator.next(response);
});
}
下面我们写一个generator函数用来返回我们自己的数据:
function* getData() {
var entry1 = yield request('http://some_api/item1');
var data1 = JSON.parse(entry1);
var entry2 = yield request('http://some_api/item2');
var data2 = JSON.parse(entry2);
}
利用yield的功能,我们确保entry1可以获得返回的数据,用于解析并存储到data1中。
当我们利用generator以同步的方式写异步的代码时,其中的错误不会简单清晰的传递。因此,我们利用Promise加强generator:
function request(url) {
return new Promise((resolve, reject) => {
getJSON(url, resolve);
});
}
我们写了一个函数,利用next用来按序地一步步遍历generator。该函数利用上述的请求方式并yeild一个Promise(对象)。
function iterateGenerator(gen) {
var generator = gen();
(function iterate(val) {
var ret = generator.next();
if(!ret.done) {
ret.value.then(iterate);
}
})();
}
通过Promise加强generator后,我们可以利用Promise.catch和Promise.reject这样清晰的方式传播错误。只用这个加强版的Generator和以前一样简单:
iterateGenerator(function* getData() {
var entry1 = yield request('http://some_api/item1');
var data1 = JSON.parse(entry1);
var entry2 = yield request('http://some_api/item2');
var data2 = JSON.parse(entry2);
});
我们可以重用写好的代码,像过去使用Generator一样,这一点很强大。当我们利用generator以同步的方式写异步的代码的同时,利用一个不错的方式保留了错误传播的能力,我们实际上可以利用一个更为简单的方式达到同样的效果:异步等待(Async-Await)。
Async Await
这是一个在ES2016(ES7)中即将有的特性,async await允许我们更简单地使用Generator和Promise执行和已完成工作相同的任务:
var request = require('request');
function getJSON(url) {
return new Promise(function(resolve, reject) {
request(url, function(error, response, body) {
resolve(body);
});
});
}
async function main() {
var data = await getJSON();
console.log(data); // NOT undefined!
}
main();
在后台,它的实现类似Generators。我(作者)强烈建议使用这个替代Generators + Promises。还会有很多的资源出现并使用ES7,同时,Babel也会用在这里。
Getter 和 Setter 函数
ES6 已经支持了Getter和Setter函数,例如:
class Employee {
constructor(name) {
this._name = name;
}
get name() {
if(this._name) {
return 'Mr. ' + this._name.toUpperCase();
} else {
return undefined;
}
}
set name(newName) {
if (newName == this._name) {
console.log('I already have this name.');
} else if (newName) {
this._name = newName;
} else {
return false;
}
}
}
var emp = new Employee("James Bond");
// 内部使用了get方法
if (emp.name) {
console.log(emp.name); // Mr. JAMES BOND
}
// 内部使用了setter(译注:原文中这一句和上一句注释的表述就这么不一样)
emp.name = "Bond 007";
console.log(emp.name); // Mr. BOND 007
最新的浏览器都支持对象中的getter/setter函数,我们可以使用他们计算属性、添加事件以及在setting和getting前的预处理
var person = {
firstName: 'James',
lastName: 'Bond',
get fullName() {
console.log('Getting FullName');
return this.firstName + ' ' + this.lastName;
},
set fullName (name) {
console.log('Setting FullName');
var words = name.toString().split(' ');
this.firstName = words[0] || '';
this.lastName = words[1] || '';
}
}
person.fullName; // Jam
ES7
Array.prototype.includes()
includes()作用,是查找一个值在不在数组里,若是存在则返回true,不存在返回false.
//1.基本用法:
['a', 'b', 'c'].includes('a') // true
['a', 'b', 'c'].includes('d') // false
//2.接收俩个参数:要搜索的值和搜索的开始索引
['a', 'b', 'c', 'd'].includes('b') // true
['a', 'b', 'c', 'd'].includes('b', 1) // true
['a', 'b', 'c', 'd'].includes('b', 2) // false
由于它对NaN的处理方式与indexOf不同,假如你只想知道某个值是否在数组中而并不关心它的索引位置,建议使用includes()。如果你想获取一个值在数组中的位置,那么你只能使用indexOf方法。
求幂运算符
//基本用法:
3 ** 2 //9
效果同
Math.pow(3, 2) //9
//由于是运算符,所以可以和 +=一样的用法
var b = 3;
b **= 2;
console.log(b); //9
ES8
async await
异步函数async function()
作用
避免有更多的请求操作,出现多重嵌套,也就是俗称的“回调地狱”
this.$http.jsonp('/login', (res) => {
this.$http.jsonp('/getInfo', (info) => {
// do something
})
})
因此提出了ES6的Promise,将回调函数的嵌套,改为了链式调用:
var promise = new Promise((resolve, reject) => {
this.login(resolve);
})
.then(() => {
this.getInfo()
})
.catch(() => {
console.log('Error')
})
声明方式
异步函数存在以下四种使用形式:
函数声明: async function foo() {}
函数表达式: const foo = async function() {}
对象的方式: let obj = { async foo() {} }
箭头函数: const foo = async () => {}
支持返回Promise和同步的值
async用于定义一个异步函数,该函数返回一个Promise。 如果async函数返回的是一个同步的值,这个值将被包装成一个理解resolve的Promise,等同于return Promise.resolve(value)。 await用于一个异步操作之前,表示要“等待”这个异步操作的返回值。await也可以用于一个同步的值。
//async await
//返回Promise
let timer = async function timer() {
return new Promise((reslove, reject) => {
setTimeout(() => {
reslove('a');
}, 1000);
})
}
timer().then(result => {
console.log(result);
}).catch(err => {
console.log(err.message);
})
//返回同步的值
let sayHello = async function sayHello() {
let hi = 'hello world'
//等同于return Promise.resolve(hi);
return hi
}
sayHello().then(res => {
console.log(res)
}).catch(err => {
console.log(err.message);
})
//对异常的处理
首先来看下Promise中对异常的处理
//使用reject
let promise = new Promise((reslove, reject) => {
setTimeout(() => {
reject('promise使用reject抛出异常')
}, 1000)
})
promise().then(res => {
console.log(res)
})
.catch(err => {
console.log(err) //'promise使用reject抛出异常'
})
//使用new Error()
let promise = new Promise((reslove, reject) => {
throw new Error('promise使用Error抛出异常') //使用throw异常不支持放在定时器中
})
promise().then(res => {
console.log(res)
})
.catch(err => {
console.log(err.message) //'promise使用Error抛出异常'
})
//reject一个new Error()
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('promise抛出异常'));
}, 1000);
})
promise.then(res => {
console.log(res);
})
.catch(err => {
console.log(err.message); //'promise抛出异常'
})
async对异常的处理也可以直接用.catch()捕捉到
//async抛出异常
let sayHi = async sayHi => {
throw new Error('async抛出异常');
}
sayHi().then(res => {
console.log(res);
})
.catch(err => {
console.log(err.message);
})
和Promise链的对比:
我们的async函数中可以包含多个异步操作,其异常和Promise链有相同之处,如果有一个Promise被reject()那么后面的将不会再进行。
let count = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('promise故意抛出异常')
}, 1000);
})
}
let list = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([1, 2, 3])
}, 1000);
})
}
let getList = async () => {
let c = await count()
console.log('async') //此段代码并没有执行
let l = await list()
return { count: c, list: l }
}
console.time('start');
getList().then(res => {
console.log(res)
})
.catch(err => {
console.timeEnd('start')
console.log(err)
})
//start: 1000.81494140625ms
//promise故意抛出异常
可以看到上面的案例,async捕获到了一个错误之后就会立马进入.catch()中,不执行之后的代码
//并行
上面的案例中,async采用的是串行处理
count()和list()是有先后顺序的
let c = await count()
let l = await list()
实际用法中,若是请求的两个异步操作没有关联和先后顺序性可以采用下面的做法
let res = await Promise.all([count(), list()])
return res
//res的结果为
//[ 100, [ 1, 2, 3 ] ]
案例详情为:
let count = ()=>{
return new Promise((resolve,reject) => {
setTimeout(()=>{
resolve(100);
},500);
});
}
let list = ()=>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve([1,2,3]);
},500);
});
}
let getList = async ()=>{
let result = await Promise.all([count(),list()]);
return result;
}
console.time('begin');
getList().then(result => {
console.timeEnd('begin'); //begin: 505.557ms
console.log(result); //[ 100, [ 1, 2, 3 ] ]
}).catch(err => {
console.timeEnd('begin');
console.log(err);
});
我们将count()和list()使用Promise.all()“同时”执行,这里count()和list()可以看作是“并行”执行的,所耗时间将是两个异步操作中耗时最长的耗时。 最后得到的结果是两个操作的结果组成的数组。我们只需要按照顺序取出数组中的值即可。
//与Generator的关系
先来回顾一下ES6中Generator函数的用法:
function* getList() {
const c = yield count()
const l = yield list()
return 'end'
}
var gl = getList()
console.log(gl.next()) // {value: Promise, done: false}
console.log(gl.next()) // {value: Promise, done: false}
console.log(gl.next()) // {value: 'end', done: true}
虽然Generator将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。此时,我们便希望能出现一种能自动执行Generator函数的方法。我们的主角来了:async/await。
ES8引入了async函数,使得异步操作变得更加方便。简单说来,它就是Generator函数的语法糖。
let getList = async () => {
const c = await count()
const l = await list()
}
Object.entries()
作用:将一个对象中可枚举属性的键名和键值按照二维数组的方式返回。
若对象是数组,则会将数组的下标作为键值返回。
Object.entries({ one: 1, two: 2 }) //[['one', 1], ['two', 2]]
Object.entries([1, 2]) //[['0', 1], ['1', 2]]
//要点
1.若是键名是Symbol,编译时会被自动忽略
Object.entries({[Symbol()]:1, two: 2}) //[['two', 2]]
2.entries()返回的数组顺序和for循环一样,即如果对象的key值是数字,则返回值会对key值进行排序,返回的是排序后的结果
Object.entries({ 3: 'a', 4: 'b', 1: 'c' }) //[['1', 'c'], ['3', 'a'], ['4', 'b']]
//利用Object.entries()创建一个真正的Map
var obj = { foo: 'bar', baz: 42 };
var map1 = new Map([['foo', 'bar'], ['baz', 42]]); //原本的创建方式
var map2 = new Map(Object.entries(obj));
//等同于map1
console.log(map1);// Map { foo: "bar", baz: 42 }
console.log(map2);// Map { foo: "bar", baz: 42 }
//自定义Object.entries()
Object.entries的原理其实就是将对象中的键名和值分别取出来然后推进同一个数组中
//自定义entries()
var obj = { foo: 'bar', baz: 42 };
function myEntries(obj) {
var arr = []
for (var key of Object.keys(obj)) {
arr.push([key, obj[key]])
}
return arr
}
console.log(myEntries(obj))
//Generator版本
function* genEntryies(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]]
}
}
var entryArr = genEntryies(obj);
console.log(entryArr.next().value) //["foo", "bar"]
console.log(entryArr.next().value) //["baz", 42]
Object.values()
作用:只返回自己的键值对中属性的值。它返回的数组顺序,也跟Object.entries()保持一致
Object.values({ one: 1, two: 2 }) //[1, 2]
Object.values({ 3: 'a', 4: 'b', 1: 'c' }) //['c', 'a', 'b']
//与Object.keys()比较
ES6中的Object.keys()返回的是键名
var obj = { foo: 'bar', baz: 42 };
console.log(Object.keys(obj)) //["foo", "baz"]
console.log(Object.values(obj)) //["bar", 42]
//Object.keys()的作用就类似于for...in
function myKeys() {
let keyArr = []
for (let key in obj1) {
keyArr.push(key)
console.log(key)
}
return keyArr
}
console.log(myKeys(obj1)) //["foo", "baz"]
//entries()、values()总结
var obj = { foo: 'bar', baz: 42 };
console.log(Object.keys(obj)) //["foo", "baz"]
console.log(Object.values(obj)) //["bar", 42]
console.log(Object.entries(obj)) //[["foo", "bar"], ["baz", 42]]
字符串填充
字符串填充padStart()和padEnd()
//用法
String.padStart(targetLength, padding)
参数:字符串目标长度和填充字段
'Vue'.padStart(10) //' Vue'
'React'.padStart(10) //' React'
'JavaScript'.padStart(10) //'JavaScript'
//要点
填充函数只有在字符长度小于目标长度时才有效,而且目标长度如果小于字符串本身长度时,字符串也不会做截断处理,只会原样输出
'Vue'.padEnd(10, '_*') //'Vue_*_*_*_'
'React'.padEnd(10, 'Hello') //'ReactHello'
'JavaScript'.padEnd(10, 'Hi') //'JavaScript'
'JavaScript'.padEnd(8, 'Hi') //'JavaScript'
Object.getOwnPropertyDescriptors()
作用 该方法会返回目标对象中所有属性的属性描述符,该属性必须是对象自己定义的,不能是从原型链继承来的。
var obj = {
id: 1,
name: '霖呆呆',
get gender() {
console.log('gender')
},
set grad(d) {
console.log(d)
}
}
console.log(Object.getOwnPropertyDescriptors(obj))
/* 输出
{
gender: {
configurable: true,
enumerable: true,
get: f gender(),
set: undefined
},
grade: {
configurable: true,
enumerable: true,
get: undefined,
set: f grade(g)
},
id: {
configurable: true,
enumerable: true,
value: 1,
writable: true
},
name: {
configurable: true,
enumerable: true,
value: '霖呆呆',
writable: true
}
}
*/
第二个参数,用于指定属性的属性描述符
Object.getOwnPropertyDescriptors(obj, 'id')
/* 输出结果应该为
{
id: {
configurable: true,
enumerable: true,
value: 1,
writable: true
}
}
*/
但是我在谷歌/火狐浏览器试了好像没有效果,有知道原因的小伙请留言
//与getOwnPropertyDescriptor()比较
ES6中也有一个返回目标对象可枚举属性的方法
var obj = {
id: 1,
name: '霖呆呆',
get gender() {
console.log('gender')
},
set grad(d) {
console.log(d)
}
}
console.log(Object.getOwnPropertyDescriptor(obj, 'id')) //输出结果
{
id: {
configurable: true,
enumerable: true,
value: 1,
writable: true
}
}
两者的区别:一个是只返回知道属性名的描述对象,一个返回目标对象所有自身属性的描述对象
//自定义该方法
function myDescriptors(obj) {
let descriptors = {}
for (let key in obj) {
descriptors[key] = Object.getOwnPropertyDescriptor(obj, key)
}
return descriptors
}
console.log(myDescriptors(obj))
//返回的结果和该方法一样
//其中上面自定义方法的for...in也可以换成,效果也是一样的
for (let key of Object.keys(obj)) {
descriptors[key] = Object.getOwnPropertyDescriptor(obj, key)
}
函数参数支持尾部逗号
该特性允许我们在定义或者调用函数时添加尾部逗号而不报错
let foo = function (
a,
b,
c,
) {
console.log('a:', a)
console.log('b:', b)
console.log('c:', c)
}
foo(1, 3, 4, )
//输出结果为:
a: 1
b: 3
c: 4
修饰器Decorator
ES8神器Decorator,修饰器,也称修饰器模式
7.1 伪Decorator
在介绍Decorator之前,我们先来实现这样一个功能:
定义一个函数,在调用这个函数时,能够执行一些其他额外操作
如下代码,定义doSometing(),在调用它时再执行其他代码
function doSometing(name) {
console.log('Hello' + name)
}
function myDecorator(fn) {
return function() {
console.log('start')
const res = fn.apply(this, arguments)
console.log('end')
return res
}
}
const wrapped = myDecorator(doSometing)
doSometing('lindaidai')
//Hellowlindaidai
wrapped('lindaidai')
//start
//Hellowlindaidai
//end
可以看到上面的操作:其实就是一个函数包装成另一个函数,这样的方式我们称之为“修饰器”
同理,我们是不是能用一个什么东西附着在我们的类或者类的属性上,让它们也有一些附加的属性或者功能呢,比如这样:
@addSkill
class Person { }
function addSkill(target) {
target.say = "hello world";
}
在Person这个类中,开始定义的时候是什么属性都没有的,在其上面使用@来附着上一个函数,这个函数的功能是给目标对象添加额外的属性say。
这样Person这个类就有了say这个属性了。
此时控制台输出:
console.log(Person['say']) //'hello world'
同样的,如果想使用Person这个类创建出来的对象也能附加上一些属性,可以在目标对象的原型对象中进行添加:
@addSkill
class Person { }
function addSkill(target) {
target.say = "hello world"; //直接添加到类中
target.prototype.eat = "apple"; //添加到类的原型对象中
}
var personOne = new Person()
console.log(Person['say']) // 'hello world'
console.log(personOne['eat']) // 'apple'
上面案例中的@addSkill其实就是一个最简单的修饰器。
当然,如果你将上面案例中的代码复制到你html文件中,会发现它并不能如愿的执行:
那是因为decorator是es7提供的方法,在浏览器中是无法直接运行的,如果你想要使用它,我们需要提前做一些准备,对它进行编译。
如果你不想深入其中,只是想单纯的了解并使用它可以参考下面的简易教程。
7.2 快速使用
网上使用Decorator的教材有很多,大多都是要需要使用插件来让浏览器支持Decorator。这里长话短说,贴上一个最精简的使用教程:
1.创建一个名为:Decorator的文件夹
2.在文件夹目录下执行命令行
npm i babel-plugin-transform-decorators-legacy babel-register --save-dev
此时文件夹下会出现俩个文件: node_modules 依赖文件夹和package.json-lock.json
3.创建文件 complie.js
require('babel-register')({
plugins: ['transform-decorators-legacy']
});
require("./app.js")
4.创建文件 app.js
@addSkill
class Person { }
function addSkill(target) {
target.say = "hello world";
}
console.log(Person.say) //'hello world'
5.在根目录下执行指令:
node complie.js
此时可以看到命令行中打印出了 hello world
简单介绍下上面步骤的原理:
第二步中使用了俩个基础插件:
transform-decorators-legacy:
//是第三方插件,用于支持decorators
babel-register:
//用于接入node api
第三步、第四步创建的俩个文件
complie.js //用来编译app
app.js //使用了装饰器的js文件
第五步:
原理:
1,node执行complie.js文件;
2,complie文件改写了node的require方法;
3,complie在引用app.js,使用了新的require方法;
4,app.js在加载过程中被编译,并执行。
当然你也可以将app.js替换为app.ts 不过别忘了把complie.js中的app.js修改为app.ts
// app.ts
@addSkill
class Person { }
function addSkill(target) {
target.say = "hello world";
}
console.log(Person['say'])
//这里如果直接使用Person.say会提示say属性不存在,如我使用的vscode编辑器就会报错,是因为ts的原因,只需要用[]的形式获取对象属性即可。
注:ts中有些语法是和js中不一样的,比如有些对象上提示没有属性的时候,只需要换一种获取对象属性的方式即可。
7.3 类修饰器
直接作用在类上面的修饰器,我们可以称之为类修饰器。
如上面案例中的@addSkill就是一个类修饰器,它修改了Person这个类的行为,为它加上了静态属性say。
addSkill函数的参数target是Person这个类本身。
1.修饰器的执行原理基本就是这样:
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;
换句话说,类修饰器是一个对类进行处理的函数。
它的第一个参数target就是函数要处理的目标类。
2.多参数
当然如果你想要有多个参数也是可以的,我们可以在修饰器外面再封装一层函数:
@addSkill("hello world")
class Person { }
function addSkill(text) {
return function(target) {
target.say = text;
}
}
console.log(Person.say) //'hello world'
上面代码中,修饰器addSkill可以接受参数,这就等于可以修改修饰器的行为。
3.修饰器在什么时候执行。
先来看一个案例:
@looks
class Person { }
function looks(target) {
console.log('I am handsome')
target.looks = 'handsome'
}
console.log(Person['looks'])
//I am handsome
//handsome
在修饰器@looks中添加一个console.log()语句,却发现它是最早执行的,其次才打印出handsome。
这是因为装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数。
装饰器是在编译时就执行的函数
7.4 方法修饰器
上面的案例中,修饰器作用的对象是类本身。
当然修饰器不仅仅这么简单,它也可以作用在类里的某个方法或者属性上,这样的修饰器我们称它为方法修饰器。
如下面的案例:
class Person {
constructor() {}
@myname //方法修饰器
name() {
console.log('霖呆呆')
}
}
function myname(target, key, descriptor) {
console.log(target);
console.log(key);
console.log(descriptor);
descriptor.value = function() {
console.log('霖呆呆')
}
}
var personOne = new Person() //实例化
personOne.name() //调用name()方法
//打印结果:
Person {}
name
{ value: [Function: name],
writable: true,
enumerable: false,
configurable: true
}
霖呆呆
上面案例中的修饰器@myname是放在name()方法上的,myname函数有三个参数:
target: 类的原型对象,上例是Person.prototype
key: 所要修饰的属性名 name
descriptor: 该属性的描述对象
我们改变了descriptor中的value,使之打印出霖呆呆。
7.5 多个修饰器的执行顺序
若是同一个方法上有多个修饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行。
class Person {
constructor() {}
@dec(1)
@dec(2)
name() {
console.log('霖呆呆')
}
}
function dec(id) {
console.log('out', id);
return function(target, key, descriptor) {
console.log(id);
}
}
var person = new Person()
person.name()
//结果
out 1
out 2
2
1
霖呆呆
如上所属,外层修饰器dec(1)先进入,但是内层修饰器dec(2)先执行。
7.6 不能作用于函数
修饰器不能作用于函数之上,这是因为函数和变量一样都会提升
var counter = 0;
var add = function () {
counter++;
};
@add
function foo() {
}
如上面的例子所示,给函数foo()定义了修饰器@add,作用是想将counter++
预计的结果counter为1,但实际上却还是为0
原因:
定义的函数foo()会被提升至最上层,定义的变量counter和add也会被提升,效果如下:
@add
function foo() {
}
var counter;
var add;
counter = 0;
add = function () {
counter++;
};
总之,由于存在函数提升,使得修饰器不能用于函数。类是不会提升的,所以就没有这方面的问题。
另一方面,如果一定要修饰函数,可以采用高阶函数的形式直接执行。
如在7.1中的例子所示:
function doSometing(name) {
console.log('Hello' + name)
}
function myDecorator(fn) {
return function() {
console.log('start')
const res = fn.apply(this, arguments)
console.log('end')
return res
}
}
const wrapped = myDecorator(doSometing)
doSometing('lindaidai')
//Hellowlindaidai
wrapped('lindaidai')
//start
//Hellowlindaidai
//end
23.AST
抽象语法树 (Abstract Syntax Tree),是将代码逐字母解析成 树状对象 的形式。这是语言之间的转换、代码语法检查,代码风格检查,代码格式化,代码高亮,代码错误提示,代码自动补全等等的基础。
抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,比如说,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现。抽象语法树并不依赖于源语言的语法,也就是说语法分析阶段所采用的上下文无文文法,因为在写文法时,经常会对文法进行等价的转换(消除左递归,回溯,二义性等),这样会给文法分析引入一些多余的成分,对后续阶段造成不利影响,甚至会使合个阶段变得混乱。因些,很多编译器经常要独立地构造语法分析树,为前端,后端建立一个清晰的接口。
抽象语法树在很多领域有广泛的应用,比如浏览器,智能编辑器,编译器。
24.babel 编译原理
babylon 将 ES6/ES7 代码解析成 AST
babel-traverse 对 AST 进行遍历转译,得到新的 AST
新 AST 通过 babel-generator 转换成 ES5
25.函数柯里化
柯里化(Currying)又称部分求值,一个柯里化的函数首先会接收一些参数,接收了这些参数后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)或者f(a, b)(c)或者f(a)(b, c)
通俗的来说:固定部分参数,返回一个接受剩余参数的函数,也称为部分计算函数,目的是为了缩小适用范围,创建一个针对性更强的函数。核心思想是把多参数传入的函数拆成一个个的单参数(或部分)函数,内部再返回调用下一个单参数(或部分)函数,依次处理剩余的参数。有点类似俄罗斯套娃,一层包一层
通常可用于在不侵入函数的前提下,为函数 预置通用参数,供多次 重复调用。
const add = function add(x) {
return function (y) {
return x + y
}
}
const add1 = add(1)
add1(2) === 3
add1(20) === 21
26.get 请求传参长度的误区
误区:我们经常说 get 请求参数的大小存在限制,而 post 请求的参数大小是无限制的。
实际上 HTTP 协议从未规定 GET/POST 的请求长度限制是多少。对 get 请求参数 的限制是来源与浏览器或 web 服务器,浏览器或 web 服务器限制了 url 的长度。
为了明确这个概念,我们必须再次强调下面几点:
HTTP 协议 未规定 GET 和 POST 的长度限制
GET 的最大长度显示是因为 浏览器和 web 服务器限制了 URI 的长度
不同的浏览器和 WEB 服务器,限制的最大长度不一样
要支持 IE,则最大长度为 2083byte,若只支持 Chrome,则最大长度 8182byte
27.补充 get 和 post 请求在缓存方面的区别
post/get 的请求区别,具体不再赘述。
补充一个 get 和 post 在缓存方面的区别:
get 请求类似于查找的过程,用户获取数据,可以不用每次都与数据库连接,所 以可以使用缓存。
post 不同,post 做的一般是修改和删除的工作,所以必须与数据库交互,所以 不能使用缓存。因此 get 请求适合于请求缓存。
28.如何解决异步回调地狱
当一个函数作为参数传入另一个函数中,并且它不会立即执行,只有当满足一定条件后该函数才可以执行,这种函数就称为回调函数。回调地狱就是在此基础上代码一层层的嵌套,使代码的可读性非常差。
//回调地狱
setTimeout(function () { //第一层
console.log('111');
//等3秒打印111在执行下一个回调函数
setTimeout(function () { //第二层
console.log('222');
//等2秒打印222在执行下一个回调函数
setTimeout(function () { //第三层
console.log('333');//等一秒打印333
}, 1000)
}, 2000)
}, 3000)
promise、generator、async/await
29.说说前端中的事件流
HTML 中与 javascript 交互是通过事件驱动来实现的,例如鼠标点击事件 onclick、页面的滚动事件 onscroll 等等,可以向文档或者文档中的元素添加事件侦听器来预订事件。想要知道这些事件是在什么时候进行调用的,就需要了解 一下“事件流”的概念。
什么是事件流:事件流描述的是从页面中接收事件的顺序,
DOM2 级事件流包括下面几个阶段。
事件捕获阶段 : 当一个事件触发后,从Window对象触发,不断经过下级节点,直到目标节点。在事件到达目标节点之前的过程就是捕获阶段。所有经过的节点,都会触发对应的事件。当为事件捕获(useCapture:true)时,先执行body的事件,再执行div的事件
处于目标阶段:
事件冒泡阶段 :当事件到达目标节点后,会沿着捕获阶段的路线原路返回。同样,所有经过的节点,都会触发对应的事件。当为事件冒泡(useCapture:false)时,先执行div的事件,再执行body的事件。默认为false
addEventListener:addEventListener 是 DOM2 级事件新增的指定事件处理程序的操作,
这个方法接收 3 个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是 true,表示在捕获阶段调用事件处理程序;如果是 false,表示在冒泡阶段调用事件处理程序。
IE 只支持事件冒泡。不支持冒泡的事件
- UI事件
- load
- unload
- scroll
- resize
- 焦点事件
- blur
- focus
- 鼠标事件
- mouseleave
- mouseenter
事件代理在捕获阶段的实际应用
可以在父元素层面阻止事件向子元素传播,也可代替子元素执行某些操作。
30.如何让事件先冒泡后捕获
在 DOM 标准事件模型中,是先捕获后冒泡。但是如果要实现先冒泡后捕获的效果, 对于同一个事件,监听捕获和冒泡,分别对应相应的处理函数,监听到捕获事件, 先暂缓执行,直到冒泡事件被捕获后再执行捕获之间。
根据w3c标准,应先捕获再冒泡。若要实现先冒泡后捕获,给一个元素绑定两个addEventListener,其中一个第三个参数设置为false(即冒泡),另一个第三个参数设置为true(即捕获),调整它们的代码顺序,将设置为false的监听事件放在设置为true的监听事件前面即可。
还有一种,在执行捕获时,设置setTimeOut(方法名,0),把它放到下一个宏任务
31.事件委托以及冒泡原理。
简介:事件委托指的是,不在事件的发生地(直接 dom)上设置监听函数,而是 在其父元素上设置监听函数,通过事件冒泡,父元素可以监听到子元素上事件的 触发,通过判断事件发生元素 DOM 的类型,来做出不同的响应。
利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
举例:最经典的就是 ul 和 li 标签的事件监听,比如我们在添加事件时候,采用 事件委托机制,不会在 li 标签上直接添加,而是在 ul 父元素上添加。
好处:比较合适动态元素的绑定,新添加的子元素也会有监听函数,也可以有事件触发机制
事件委托是利用冒泡阶段的运行机制来实现的,就是把一个元素响应事件的函数委托到另一个元素,一般是把一组元素的事件委托到他的父元素上,委托的优点是减少内存消耗,节约效率
动态绑定事件
事件冒泡,就是元素自身的事件被触发后,如果父元素有相同的事件,如 onclick 事件,那么元素本身的触发状态就会传递,也就是冒到父元素,父元素的相同事件也会一级一级根据嵌套关系向外触发,直到 document/window,冒泡过程结束。
32.说一下图片的懒加载和预加载
预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染资源,预加载是网页的另一个性能优化技术,我们可以使用该技术来预先告知浏览器某些资源可能在将来会被使用到.。
为什么要使用预加载:在网页全部加载之前,对一些主要内容进行加载,提供给用户更好的体验,减少等待的时间,如果一个长页面,过于庞大,没有使用预加载,页面就会长时间展现为一片空白,直到所有内容加载完毕 。
实现预加载思路
- 创建好要显示的图片节点1
- 创建用来加载图片的节点2
- 监听节点2的onload事件
- 返回一个对象,包含一个设置图片src的方法,节点1显示本地图片,节点2加载真正的资源
懒加载:又名延迟加载(简称lazyload),可以在长网页中延迟加载图像,是对网页性能优化的的一种方案,它的核心是按需加载。懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数;提升用户体验; 减少无效资源加载,按需要去加载数据;防止并发加载的资源过多,阻塞js的加载 影响网站的正常使用。
实现懒加载思路
-
利用Image的src有图片地址时才会加载图片
-
图片的初始状态不设置src属性,使用一个自定义属性保存图片路径 如data-src
-
图片进入窗口可视区时给src赋值
-
window.onscroll判断图片是否进入窗口:图片到body的offsetTop<(窗口高+scrollTop)
懒加载原理
将页面上的图片src属性设置为空,然后将图片的真实路径存放在当前图片标签的自定义属性data-src上,当页面滚动的时候,需要去监听scroll事件,在scroll事件的回调中,判断我们的懒加载的图片是否进入可视区域,如果图片在可视区域就将图片的src属性设置为data-src的值,然后这样就实现延迟加载
注意:如果是异步加载的数据,我们实际上只需要做一次请求即可,不需要多次请求
区别
两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。
懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。
意义
(1) 懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。
(2) 预加载可以说是牺牲服务器前端性能,换取更好的用户体验,这样可以使用户的操作得到最快的反映
33.mouseover 和 mouseenter 的区别
mouseover:当鼠标移入元素或其子元素都会触发事件,所以有一个重复触发, 冒泡的过程。对应的移除事件是 mouseout
当鼠标从元素的边界之外移入元素的边界之内时,事件被触发。如果移到父元素里面的子元素,事件也会被触发
mouseenter:当鼠标移除元素本身(不包含元素的子元素)会触发事件,也就是 不会冒泡,对应的移除事件是 mouseleave
当鼠标从元素的边界之外移入元素的边界之内时,事件被触发。而鼠标本身在元素边界内时,要触发该事件,必须先将鼠标移出元素边界外,再次移入才能触发。
mouseover和mouseenter的异同体现在两个方面:
- 是否支持冒泡
- 事件的触发时机
所以我们在使用鼠标经过事件一般会使用
mouseenter 和 mouseleave 没有冒泡效果(推荐)
而mouseover 和 mouseout 会有冒泡效果
34.js 的 各 种 位 置 , 比 如 clientHeight,scrollHeight,offsetHeight , 以 及 scrollTop, offsetTop,clientTop 的区别?
clientHeight:表示的是可视区域的高度,不包含 border 和滚动条
包括padding但不包括border、水平滚动条、margin的元素的高度。对于inline的元素这个属性一直是0,单位px,只读元素。
offsetHeight:表示可视区域的高度,包含了 border 和滚动条
包括padding、border、水平滚动条,但不包括margin的元素的高度。对于inline的元素这个属性一直是0,单位px,只读元素。
scrollHeight:表示了所有区域的高度,包含了因为滚动被隐藏的部分。
在有滚动条时讨论scrollHeight才有意义,在没有滚动条时scrollHeight==clientHeight恒成立。单位px,只读元素。
clientTop:表示边框 border 的厚度,在未指定的情况下一般为 0
offsetTop:当前元素顶部距离最近父元素顶部的距离,和有没有滚动条没有关系。单位px,只读元素。
scrollTop:滚动后被隐藏的高度,获取对象相对于由 offsetParent 属性指定的 父坐标(css 定位的元素或 body 元素)距离顶端的高度。
代表在有滚动条时,滚动条向下滚动的距离也就是元素顶部被遮住部分的高度。在没有滚动条时scrollTop==0恒成立。单位px,可读可设置。
35.js 拖拽功能的实现
首先是三个事件,分别是
-
onmousedown:鼠标按下事件
-
onmousemove:鼠标移动事件
-
onmouseup:鼠标抬起事件
当鼠标点击按下的时候,需要一个 tag 标识此时已经按下,可以执行 mousemove 里面的具体方法。
clientX,clientY 标识的是鼠标的坐标,分别标识横坐标和纵坐标,并且我们 用 offsetX 和 offsetY 来表示元素的元素的初始坐标,移动的举例应该是:
鼠标移动时候的坐标-鼠标按下去时候的坐标。
也就是说定位信息为:
鼠标移动时候的坐标-鼠标按下去时候的坐标+元素初始情况下的 offetLeft.
还有一点也是原理性的东西,也就是拖拽的同时是绝对定位,我们改变的是绝对 定位条件下的 left 以及 top 等等值。
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Lazyload</title>
<style>
.drag {
background-color: skyblue;
position: absolute;
line-height: 100px;
text-align: center;
width: 100px;
height: 100px;
}
</style>
</head>
<body>
<!-- left和top要写在行内样式里面 -->
<div class="drag" style="left: 0; top: 0">按住拖动</div>
<script src="./jquery-3.6.0.min.js"></script>
<script>
// 获取DOM元素
let dragDiv = document.getElementsByClassName('drag')[0]
// 鼠标按下事件 处理程序
let putDown = function (event) {
dragDiv.style.cursor = 'pointer'
let offsetX = parseInt(dragDiv.style.left) // 获取当前的x轴距离
let offsetY = parseInt(dragDiv.style.top) // 获取当前的y轴距离
let innerX = event.clientX - offsetX // 获取鼠标在方块内的x轴距
let innerY = event.clientY - offsetY // 获取鼠标在方块内的y轴距
// 按住鼠标时为div添加一个border
dragDiv.style.borderStyle = 'solid'
dragDiv.style.borderColor = 'red'
dragDiv.style.borderWidth = '3px'
// 鼠标移动的时候不停的修改div的left和top值
document.onmousemove = function (event) {
dragDiv.style.left = event.clientX - innerX + 'px'
dragDiv.style.top = event.clientY - innerY + 'px'
// 边界判断
if (parseInt(dragDiv.style.left) <= 0) {
dragDiv.style.left = '0px'
}
if (parseInt(dragDiv.style.top) <= 0) {
dragDiv.style.top = '0px'
}
if (
parseInt(dragDiv.style.left) >=
window.innerWidth - parseInt(dragDiv.style.width)
) {
dragDiv.style.left =
window.innerWidth - parseInt(dragDiv.style.width) + 'px'
}
if (
parseInt(dragDiv.style.top) >=
window.innerHeight - parseInt(dragDiv.style.height)
) {
dragDiv.style.top =
window.innerHeight - parseInt(dragDiv.style.height) + 'px'
}
}
// 鼠标抬起时,清除绑定在文档上的mousemove和mouseup事件
// 否则鼠标抬起后还可以继续拖拽方块
document.onmouseup = function () {
document.onmousemove = null
document.onmouseup = null
// 清除border
dragDiv.style.borderStyle = ''
dragDiv.style.borderColor = ''
dragDiv.style.borderWidth = ''
}
}
// 绑定鼠标按下事件
dragDiv.addEventListener('mousedown', putDown, false)
</script>
</body>
</html>
补充:也可以通过 html5 的拖放(Drag 和 drop)来实现
36.异步加载 js 的方法
异步加载又叫非阻塞,浏览器在加载执行 js 同时,还会继续进行后续页面的处理。
异步加载js,按需加载,用到的时候再加载,不用到不加载。
异步加载的三种方式:
- defer属性:在文档完成解析完成开始执行,并且在DOMContentLoaded事件之前执行完成。仅支持IE,最好是外部的script使用。如果有多个声明了defer的脚本,则会按顺序下载和执行
- async属性:加载完就执行,只能加载外部脚本,是W3C的标准。高版本的浏览器支持加载内部脚本,但是标准中不允许。
- 创建一个script标签,插入到DOM中。兼容性最好。
异步加载禁止使用document.write(),因为document.write()有可能会清空文档流
异步加载的兼容性写法:
function loadScript(url, callback){
var script = document.createElement('script');
script.type = "text/javascript";
if(script.readyState){
// 状态码 readyState-->complete loaded 表示ie中script加载完成了
script.onreadystatechange = function (){// ie
if(script.readyState == 'complete' || script.readyState == 'loaded'){
obj[callback]();
}
}
}else{
// 什么时候load.js加载完成了? 提示 onload
script.onload = function(){//加载完成的标志
// safari chrome firefox opera
obj[callback]();
}
}
script.src = url;//下载了指定地址的js文件
document.head.appendChild(script);//挂到DOM树上,此时才执行了js文件中的代码
}
37.Ajax 解决浏览器缓存问题
1.浏览器缓存的表现:
在项目中一般提交请求都会通过ajax来提交,但是发现,每次提交后得到的数据都是一样的,每次清除缓存后,就又可以得到一个新的数据。
2.浏览器缓存原因:
ajax能提高页面载入的速度主要的原因是ajax能实现局部刷新,通过局部刷新机制减少了重复数据的载入,也就是说在载入数据的同时将数据缓存到内存中,一旦数据被加载其中,只要没有刷新页面,这些数据就会一直被缓存在内存中,当我们提交 的URL与历史的URL一致时,就不需要提交给服务器,也就是不需要从服务器上面去获取数据。那么,我们得到还是最开始缓存在浏览器中的数据。虽然降低了服务器的负载提高了用户的体验,但是我们不能获取最新的数据。为了保证我们读取的信息都是最新的,我们就需要禁止他的缓存功能。
3.解决方法:
(1)在ajax发送请求前加上 anyAjaxObj.setRequestHeader("If-Modified-Since","0")。
原理:If-Modified-Since:0 故意让缓存过期
(2)在ajax发送请求前加上 anyAjaxObj.setRequestHeader("Cache-Control","no-cache")。
原理:直接禁用缓存机制
(3)在URL后面加上一个随机数: "fresh=" + Math.random();。
原理:强行让每次的请求地址不同
(4)在URL后面加上时间搓:"nowtime=" + new Date().getTime();。
原理:强行让每次的请求地址不同
(5)如果是使用jQuery,直接这样就可以了 $.ajaxSetup({cache:false})。这样页面的所有 ajax 都会执行这条语句就是不需要保存缓存记录。
原理:不设置ajax缓存
38.JS 中的垃圾回收机制
什么是垃圾?
(1)没有被引用的对象或变量
(2)无法访问到的对象(几个对象引用形成一个环,互相引用)
可达性
是指那些以某种方式可以访问到或可以用到的值,它们被保证存储在内存中。
有一组基本的固有可达值,由于显而易见而无法删除:
(1)本地函数的局部变量和参数
(2)嵌套调用链上的其他函数的变量与参数
(3)全局变量
(4)还有一些其他的,内部的
这些值成为根。
如果引用或引用链可以从根访问任何其他值,则认为该值是可访问的。
垃圾回收机制
垃圾回收机制(GC:Garbage Collection):执行环境负责管理代码执行过程中使用的内存。JS的垃圾回收机制是为了以防内存泄漏,内存泄漏的含义就是当已经不需要某块内存时这块内存还存在着,没有被释放,导致该内存无法被使用,垃圾回收机制就是间歇的不定期的寻找到不再使用的变量,并释放掉它们所指向的内存。
垃圾回收的必要性
字符串、对象和数组没有固定的大小,所以只有当它们大小已知时才能对它们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都要分配内存才存储这个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便它们能够被再次利用;否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。
JavaScript有自己的垃圾回收机制(Garbage Collection),JavaScript的解释器可用检测到何时程序不再使用一个对象,当确定了一个对象无用的时候,就说明不再需要这个对象了,就可用把这个对象所占用的内存释放掉。
例如:
var a=“hello world”;
var b=“world”;
var a=b;
//这时,会释放掉"hello world",释放内存以便再引用
垃圾回收的方法:标记清除、计数引用。
标记清除
这是JavaScript中最常用的垃圾回收方式。
- 当变量进入执行环境时(函数中声明变量),就标记这个变量为“进入环境”,当变量离开环境时(函数执行结束),则将其标记为“离开环境”,离开环境之后还有的变量则是需要被删除的变量。
- 垃圾回收器在运行的时候会给存储在内存中的变量都加上标记(所有都加)。
- 然后去掉环境变量中的变量,以及被环境变量中的变量所引用的变量的标记(条件性去除标记);
- 删除所有被标记的变量,删除的变量无法在环境变量中被访问,所以会被删除;
- 最后垃圾回收器,完成了内存的清除工作,并回收他们所占用的内存。
引用计数法
另一种不太常见的方法就是引用计数法,引用计数法的意思就是每个值没引用的 次数,当声明了一个变量,并用一个引用类型的值赋值给改变量,则这个值的引 用次数为 1,;相反的,如果包含了对这个值引用的变量又取得了另外一个值, 则原先的引用值引用次数就减 1,当这个值的引用次数为 0 的时候,说明没有办 法再访问这个值了,因此就把所占的内存给回收进来,这样垃圾收集器再次运行 的时候,就会释放引用次数为 0 的这些值。
实现流程:
(1)先声明一个变量,并将一个引用类型的值赋给该变量,那么这个引用类型的引用次数为1,计数为1
(2)如果同一个引用类型的值又赋给其他变量,那么这个引用类型的值被引用的次数就会加一,引用次数为2,计数2,并以此类推;
(3)如果被该引用类型的值赋值的变量,被赋值了其他的引用类型的值,那么该类型的值的引用次数就需要减一。
(4)当该引用类型的值的引用次数为0,就说明没有变量被该引用类型的值赋值,所以就没有办法访问到这个引用类型的值。
(5)周期一到,垃圾收集器就会释放掉引用次数计数为0的引用类型的值所占的内存。
用引用计数法会存在内存泄露,下面来看原因:
function problem() {
var objA = new Object();
var objB = new Object();
objA.someOtherObject = objB;
objB.anotherObject = objA;
}
在这个例子里面,objA 和 objB 通过各自的属性相互引用,这样的话,两个对象 的引用次数都为 2,在采用引用计数的策略中,由于函数执行之后,这两个对象 都离开了作用域,函数执行完成之后,因为计数不为 0,这样的相互引用如果大量存在就会导致内存泄露。
特别是在 DOM 对象中,也容易存在这种问题:
var element=document.getElementById(’‘);
var myObj=new Object();
myObj.element=element;
element.someObject=myObj;
这样就不会有垃圾回收的过程。
常见内存泄漏的原因:
(1)全局变量引起的内存泄露
(2)闭包引起的内存泄露:慎用闭包
(3)dom清空或删除时,事件未清除导致的内存泄漏
(4)循环引用带来的内存泄露
39.eval 是做什么的
它的功能是把对应的字符串解析成js代码并运行,
例如:
1、eval("2+3");//执行加运算,并返回运算值。
2、eval("varage=10");//声明一个age变量
eval的作用域在它所有的范围内容有效
其它作用
由JSON字符串转换为JSON对象的时候可以用eval,例如:
1、varjson="{name:'Mr.CAO',age:30}";
2、varjsonObj=eval("("+json+")");
3、console.log(jsonObj);
应该避免使用eval,因为不安全,非常耗性能(2次,一次解析成js语句,一次执行)
注意:在项目里写js代码的时候,禁止使用的,因为有安全因素。
40.对象深度克隆的简单实现
众所周知,对象是一种引用类型
对象的地址指针存放于栈中,而对象实际的数据存放于堆中。
因此当我们简单地执行复制操作时,实际是把地址指针进行了复制操作,因此在对象的实际数据改变之后,新旧对象都会受到影响。
那么如何不受到影响呢?
此谓深度克隆
JS中的深度克隆,指的是原对象改变了,克隆出来的新对象也不会改变,原对象与新对象是完全独立的关系。
有深度克隆就是
浅度克隆
原始类型为值传递,对象类型仍为引用传递。
(1)原始类型包括:数值、字符串、布尔值、null、undefined
(2)对象类型包括:对象即是属性的集合,当然这里又两个特殊的对象----函数(js中的一等对象)、数组(键值的有序集合)。
原始类型复制:
//数值克隆的表现
var a="1";
var b=a;
b="2";
console.log(a);// "1"
console.log(b);// "2"
//字符串克隆的表现
var c="1";
var d=c;
d="2";
console.log(c);// "1"
console.log(d);// "2"
//布尔值克隆的表现
var x=true;
var y=x;
y=false;
console.log(x);// true
console.log(y);// false
原始类型即使我们采用普通的克隆方式仍能得到正确的结果,原因就是原始类型存储的是对象的实际数据。
对象类型:
函数式一等对象,当然也是对象类型,但是函数的克隆通过浅克隆即可实现
var m=function(){alert(1);};
var n=m;
n=function(){alert(2);};
console.log(m());//1
console.log(n());//2
深度简单实现:
function deepClone(obj){
//判断是数组还是对象
var newObj= obj instanceof Array ? []:{};
for(var item in obj){
//如果key是自身的属性,非原型上的属性。
var temple= typeof obj[item] == 'object' ? deepClone(obj[item]):obj[item];
//是原始值,就赋值,否则递归
newObj[item] = temple;
}
return newObj;
}
判定要克隆的对象是不是引用类型,如果是引用类型,则继续迭代,如果该项是基本类型,则直接复制。
ES5 的常用的对象克隆的一种方式。注意数组是对象,但是跟对象又有一定区别,
所以我们一开始判断了一些类型,决定 newObj 是对象还是数组~
41.实现一个 once 函数,传入函数参数只执行一次
function ones(func){
let tag=true;
return function(){
if(tag==true){
func.apply(null,arguments);
tag=false;
}
return undefined
}
}
42.将原生的 ajax 封装成 promise
var myNewAjax=function(url){
return new Promise(function(resolve,reject){
var xhr = new XMLHttpRequest();
xhr.open('get',url);
xhr.send(data);
xhr.onreadystatechange=function(){
if(xhr.status==200&&readyState==4){
var json=JSON.parse(xhr.responseText);
resolve(json)
}
else if(xhr.readyState==4&&xhr.status!=200){
reject('error');
}
}
})
}
43.js 监听对象属性的改变
我们假设这里有一个 user 对象,
(1)在 ES5 中可以通过
Object.defineProperty(obj, prop, descriptor) 来实现已有属性的监听
要想监听属性的变化,首先需要通过Object.defineProperty()为需要监听的属性设置一个代理。通过改变代理的值,触发set和get的方法,在这两个方法中我们编写我们想要的操作。
参数
obj 要在其上定义属性的对象。
prop 要定义或修改的属性的名称。
descriptor 将被定义或修改的属性描述符。
var obj = {
name: 'itclanCoder',
phone: 13711767328,
};
Object.defineProperty(obj, 'phone', {
configurable: true, // 属性可配置
set: function (v) {
console.log('phone发生了变化');
this.phone = v;
},
get: function () {
return this.phone;
},
});
obj.phone = 15213467443;
缺点:如果 id 不在 user 对象中,则不能监听 id 的变化
(2)在 ES6 中可以通过 Proxy 来实现
var obj = {
name: 'itclanCoder',
phone: 13711767328,
};
var handler = {
set: function (target, name, value) {
console.log('phone发生了变化');
// 改变被代理对象的值,使之保持一致
target[name] = value;
},
};
var proxy = new Proxy(obj, handler);
proxy.phone = 1371123765;
这样即使有属性在 user 中不存在,通过 user.id 来定义也同样可以这样监听这个属性的变化哦~
44.如何实现一个私有变量,用 getName 方法可以访问,不能直接访问
(1)通过 defineProperty 来实现
obj={
name:yuxiaoliang,
getName:function(){
return this.name
}
}
object.defineProperty(obj,"name",{
//不可枚举不可配置
});
(2)通过函数的创建形式
function product(){
let name='yuxiaoliang';
this.getName = function(){
return name;
}
}
let obj=new product();
console.log(p.getName());
45、和=、以及 Object.is 的区别
== :等同,比较运算符,两边值类型不同的时候,先进行类型转换,再比较; 主要存在:强制转换成 number,null==undefined
" "==0 //true
"0"==0 //true
" " !="0" //true
123=="123" //true
null==undefined //true
===:恒等,严格比较运算符,不做类型转换,类型不同就是不等;
这么理解: 当进行双等号比较时候: 先检查两个操作数数据类型,如果相同, 则进行===比较, 如果不同, 则愿意为你进行一次类型转换, 转换成相同类型后再进行比较, 而===比较时, 如果类型不同,直接就是false.
Object.js :是ES6新增的用来比较两个值是否严格相等的方法,与===的行为基本一致。 其行为与===基本一致,不过有两处不同:
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
主要的区别就是+0!=-0 而 NaN==NaN
(相对比===和==的改进)
46.setTimeout、setInterval 和 requestAnimationFrame之间的区别
与 setTimeout 和 setInterval 不同,requestAnimationFrame 不需要设置时间间隔,这有什么好处呢?
计时器一直是 JavaScript 动画的核心技术,而编写动画循环的核心是要知道延迟时间多长合适。一方面循环间隔必须足够短,这样才能让不同的动画效果显得平滑顺畅,另一方面循环间隔要足够的长,这样才能保证浏览器有能力渲染产生的变化。
大多数电脑显示器的刷新频率是 60Hz,大概相当于每秒钟重绘 60 次。大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过那个频率用户体验也不会有提升。因此,最平滑动画的最佳循环间隔是 1000ms/60,约 等于 16.6ms。
而 setTimeout 和 setInterval 的缺点是他们都不够精确。它们内在的运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器 UI 线程队列中等待执行的时间,如果队列中已经加入了其他任务,那么动画的执行要等前面的任务结束之后才会执行。
requestAnimationFrame采用的是系统时间间隔,保证了最佳绘制效率。不会因间隔时间过短,造成过度绘制,增加开销;也不会因时间间隔太长,造成动画卡顿。它能够让各种网页动画有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。
特点:
(1)requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次 重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。
(2)在隐藏或不可见的元素中,requestAnimationFrame 将不会进行重绘或回流,这当然就意味着更少的 CPU、GPU 和内存使用量
(3)requestAnimationFrame 是由浏览器专门为动画提供的 API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停, 有效节省了 CPU 开销。
requestAnimationFrame 使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。他返回一个整数,标识定时器的编号,这个值可以传递给 cancelAnimationFrame用于取消这个函数的执行。
const requestId = requestAnimationFrame(callback);
//控制台输出1和0
var timer = requestAnimationFrame(function(){
console.log(0);
});
console.log(timer);//1
cancelAnimationFrame方法用于取消定时器
//控制台什么都不输出
var timer = requestAnimationFrame(function(){
console.log(0);
});
cancelAnimationFrame(timer);
也可以直接使用返回值进行取消
var timer = requestAnimationFrame(function(){
console.log(0);
});
cancelAnimationFrame(1);
兼容
IE9-浏览器不兼容该方法,可以使用 setTimeout 来兼容
if(!window.requestAnimationFrame){
let lastTime = 0;
window.requestAnimationFrame = function(callback){
const currTime = new Date().getTime();
const timeToCall = Math.max(0, 16.7-(currTime - lastTime));
const id = window.setTimeout(function(){
callback(currTime + timeToCall);
},timeToCall);
lastTime = currTime + timeToCall;
return id;
}
}
if(!window.cancelAnimationFrame){
window.cancelAnimationFrame = function(id){
clearTimeout(id);
}
}
47.实现一个两列等高布局,讲讲思路
实现两栏布局,要求无论哪一栏的内容多,两栏高度均以最高一栏为准,两栏的背景色需和容器高度相等,即竖直方向铺满容器。
**方法一:**使用 display:table-cell ,将两栏作为单元格处理,而单元格本身就具有天然等高作用,直接就可达到效果需求啦~
**方法二:**使用margin负值实现
众所周知,给一个元素padding值可以扩大此元素背景颜色的显示范围,故为了两栏背景色在视觉上高度一致,给两栏元素加上较大的padding-bottom以保证最大范围内两者同高度。但这样一来,两栏元素的高度太高,影响了后面元素的显示位置,所以此时加上一个margin-bottom,取值和padding-bottom相等但取其负值,以正负抵消额外产生的元素外部高度,就不会把后文挤到很后面去,且视觉上多了很大的高度的可使用的背景色。
从图中可以看出,后文内容被正确提到了前面应在的位置上来,但由于左右两栏采用了浮动,所以排列上遵循了浮动元素的规则。此时父元素的高度为0(图中顶部红色线为父元素的border)
此时给父元素设置overflow: hidden :
- 触发父元素的BFC,使其高度自适应浮动元素高度
- 将超出部分隐藏,主要是隐藏多余的背景色
<style>
.con {
border: 1px solid red;
font-size: 0;
overflow: hidden;
}
.son {
margin-bottom: -9999px;
padding-bottom: 9999px;
/*border-bottom: 9999px solid transparent;*/
width: 50%;
font-size: 16px;
float: left;
}
.left {
background-color: pink;
}
.right {
background-color: lightblue;
}
</style>
<body>
<div class="con">
<div class="son left">
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid est blanditiis eius, officiis inventore, culpa error, dignissimos reiciendis, adipisci omnis cumque quaerat aspernatur vitae molestiae. Eveniet dolores nesciunt voluptate reprehenderit!</p>
</div>
<div class="son right">
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro nam praesentium unde impedit, odio accusamus soluta in quis repudiandae ad quam quia id voluptate nesciunt nulla odit neque accusantium? Culpa.</p>
</div>
</div>
</body>
//代码中给.con父元素设置 font-size: 0 后,又给.son子元素设置 font-size: 16 是为了消除HTML换行、空格等造成的“空白折叠现象”。
**方法三:**将上述padding-bottom: 9999px改为border-bottom: 9999px solid transparent;也可以达到一样的效果,原理类似。
三种方法之优劣
方法一:table-cell具有“天然等高”的优势,不足之处是仅在IE8/8+才支持
方法二:优点是兼容性好,IE6/6+支持,且支持人一个分栏等高布局,缺点是父元素overflow:hidden之后,如果有子元素需要定位到容器之外,会被隐藏掉;触发锚点定位或者使用DOM.srcollIntoview()方法的时候可能会出现奇怪的定位问题(这个问题是啥,我还没研究过0.0)。
方法三:同方法二一样,优点是兼容性较好好,IE7/7+(主要是对transparent值的支持),但比二好在没有锚点定位的隐患,不足之处是最多定义3栏(旭神书上写的,但我实验了下,并不存在这个问题,至少可以分四栏…),且border不支持百分比宽度,因此只能实现至少一侧定宽的布局(这个我也没想通为啥…除非是使用border-right或者border-left来完成背景色,但明显不对啊 T_T)
总结:如果仅需兼容IE8/8+就推荐使用table-cell,如要兼容更低版本的浏览器就采用另外两种,具体视情况而定。
48.自己实现一个bind 函数
bind是用来绑定上下文的,强制将函数的执行环境绑定到目标作用域中去。与call和apply类似,但不同点在于,他不会立即执行,而是返回一个函数。因此想要自己实现一个bind函数,就必须要返回一个函数,让这个函数接受绑定的参数的上下文。
原理:通过 apply 或者 call 方法来实现。
(1)初始版本
Function.prototype.bind=function(obj,arg){
var arg=Array.prototype.slice.call(arguments,1);
var context=this;
return function(newArg){
arg=arg.concat(Array.prototype.slice.call(newArg));
return context.apply(obj,arg);
}
}
(2) 考虑到原型链
为什么要考虑?因为在 new 一个 bind 过生成的新函数的时候,必须的条件是要继承原函数的原型
Function.prototype.bind=function(obj,arg){
var arg=Array.prototype.slice.call(arguments,1);
var context=this;
var bound=function(newArg){
arg=arg.concat(Array.prototype.slice.call(newArg));
return context.apply(obj,arg);
}
var F=function(){}
//这里需要一个寄生组合继承
F.prototype=context.prototype;
bound.prototype=new F();
return bound;
}
49.用 setTimeout()方法来模拟 setInterval()与 setInterval() 之间的什么区别?
setTimeout(fn,time)是超时调用,它在大于等于time之后调用fn;而setIntervl(fn,time)是间歇调用;每隔time调用一次。
首先来看 setInterval 的缺陷,使用 setInterval()创建的定时器确保了定时器 代码规则地插入队列中。这个问题在于:如果定时器代码在代码再次添加到队列之前还没完成执行,结果就会导致定时器代码连续运行好几次。而之间没有间隔。
不过幸运的是:javascript 引擎足够聪明,能够避免这个问题。当且仅当没有 该定时器的如何代码实例时,才会将定时器代码添加到队列中。这确保了定时器 代码加入队列中最小的时间间隔为指定时间。
这种重复定时器的规则有两个问题:
1.某些间隔会被跳过
2.多个定时器的代码执行时间可能会比预期小。
为了避免setInterval()的重复定时器的这两个缺点,可以使用模拟setInterval一般使用setTimeout 模拟 setInterval 来规避它所带来的问题。
/*
@param {*} dom1 启动定时器的元素
@param {*} dom2 关闭定时器的元素
@param {*} fn 需要执行的定时器函数
@param {*} interval 定时器的时间间隔
*/
function setTimeout_setInterval(dom1, dom2, fn, interval) {
var timer = time;
var flag = true;
function time() {
fn()
setTimeout(time, interval);
};
dom1.onclick = function() {
if (typeof time === "function") {
time();
} else {
time = timer;
time();
}
}
dom2.onclick = function() {
time = null;
}
}
模拟 setTimeout() :用 setInterval() 模拟 setTimeout() 很简单,在 setInterval() 执行一次后,立刻关闭窗口(当然这是耍无赖)或者执行 clearInterval() 方法(这个靠谱点)。clearInterval() 需要在 setInterval()执行code方法内或其他地方执行,不能紧接着 setInterval() 后面执行,那样setInterval() 还没等到执行,就已经被干掉了
var intervalNum = 0, clearId = 0;
function testsetInterval(){
var date = new Date();
console.log(date.getSeconds());
console.log("setInterval", intervalNum++);
clearInterval(clearId); //也可以在此执行
}
function testFuntion() {
clearId = setInterval(function () {
testsetInterval(); //每隔4秒调用testsetInterval()
// clearInterval(clearId); //可以在此执行
},4000);
}
上面代码实现了递归调用,这样做的好处是:在前一个定时器代码执行完成之前,不会向队列插入新的定时代码,确保不会有任何的缺失间隔。而且,它保证在下一次定时器代码执行之前,至少要等待指定的时间间隔。
50. js 怎么控制一次加载一张图片,加载完后再加载下一张
// 只要能监控到图片是否加载完成 就能实现了
// 要把图片当成是图片对象才行;
(1)方法1
<script type="text/javascript">
var obj=new Image();
obj.src="https://dwz.cn/jbVvWYJr";
obj.onload=function(){
alert('图片的宽度为:'+obj.width+';图片的高度为:'+obj.height);
document.getElementById("mypic").innnerHTML="<img src='"+this.src+"' />";
}
</script>
<div id="mypic">onloading……</div>
(2)方法2
<script type="text/javascript">
var obj=new Image();
obj.src="https://dwz.cn/jbVvWYJr";
obj.onreadystatechange=function(){
if(this.readyState=="complete"){
alert('图片的宽度为:'+obj.width+';图片的高度为:'+obj.height);
document.getElementById("mypic").innnerHTML="<img src='"+this.src+"' />";
}
}
</script>
<div id="mypic">onloading……</div>
51.如何实现 sleep 的效果(es5 或者 es6)
在多线程编程中,sleep的作用是起到挂起的作用,使线程休眠,而js是单线程的,我们如何在js中模拟sleep的效果呢~ 也就是如何用同步的方式来处理异步。
(1)while 循环的方式
function sleep(ms){
var start=Date.now(),expire=start+ms;
while(Date.now()<expire);
console.log('1111');
return;
}
执行 sleep(1000)之后,休眠了 1000ms 之后输出了 1111。上述循环的方式缺点很明显,容易造成死循环。
(2)通过 promise 来实现
function sleep(ms){
var temple=new Promise(
(resolve)=>{
console.log(111);setTimeout(resolve,ms)
});
return temple
}
sleep(500).then(function(){
//console.log(222)
})
//先输出了 111,延迟 500ms 后输出 222
(3)通过 async 封装
function sleep(ms){
return new Promise((resolve)=>setTimeout(resolve,ms));
}
async function test(){
var temple=await sleep(1000);
console.log(1111)
return temple
}test();
//延迟 1000ms 输出了 1111
(4).通过 generate 来实现
function* sleep(ms){
yield new Promise(function(resolve,reject){
console.log(111);
setTimeout(resolve,ms);
})
}
sleep(500).next().value.then(function(){
console.log(2222)
})
52.Function.proto(getPrototypeOf)是什么?
获取一个对象的原型,在 chrome 中可以通过_proto_的形式,或者在 ES6 中可以通过 Object.getPrototypeOf 的形式。
那么 Function.proto 是什么么?也就是说 Function 由什么对象继承而来,我们来做如下判别。
Function.__proto__==Object.prototype //false
Function.__proto__==Function.prototype//true
我们发现 Function 的原型也是 Function。
53.实现 js 中所有对象的深度克隆(包装对象,Date 对象, 正则对象)
通过递归可以简单实现对象的深度克隆,但是这种方法不管是ES6还是ES5实现, 都有同样的缺陷,就是只能实现特定的 object 的深度复制(比如数组和函数), 不能实现包装对象 Number,String , Boolean,以及 Date 对象,RegExp 对象 的复制。
(1)前文的方法
function deepClone(obj){
var newObj= obj instanceof Array?[]:{};
for(var i in obj){
newObj[i]=typeof obj[i]=='object'?
deepClone(obj[i]):obj[i];
}
return newObj;
}
这种方法可以实现一般对象和数组对象的克隆,比如:
var arr=[1,2,3];
var newArr=deepClone(arr);
// newArr->[1,2,3]
var obj={
x:1,
y:2
}
var newObj=deepClone(obj);
// newObj={x:1,y:2}
但是不能实现例如包装对象 Number,String,Boolean,以及正则对象 RegExp 和 Date 对象的克隆,比如:
//Number 包装对象
var num=new Number(1);
typeof num // "object"
var newNum=deepClone(num);
//newNum -> {} 空对象
//String 包装对象
var str=new String("hello");
typeof str //"object"
var newStr=deepClone(str);
//newStr-> {0:'h',1:'e',2:'l',3:'l',4:'o'};
//Boolean 包装对象
var bol=new Boolean(true);
typeof bol //"object"
var newBol=deepClone(bol);
// newBol ->{} 空对象
....
(2)valueof()函数
所有对象都有 valueOf 方法,valueOf 方法对于:如果存在任意原始值,它就默认将对象转换为表示它的原始值。对象是复合值,而且大多数对象无法真正表示为一个原始值,因此默认的 valueOf()方法简单地返回对象本身,而不是返回一个原始值。数组、函数和正则表达式简单地继承了这个默认方法,调用这些类型的实例的 valueOf()方法只是简单返回这个对象本身。
对于原始值或者包装类:
function baseClone(base){
return base.valueOf();
}
//Number
var num=new Number(1);
var newNum=baseClone(num);
//newNum->1
//String
var str=new String('hello');
var newStr=baseClone(str);
// newStr->"hello"
//Boolean
var bol=new Boolean(true);
var newBol=baseClone(bol);
//newBol-> true
其实对于包装类,完全可以用=号来进行克隆,其实没有深度克隆一说, 这里用 valueOf 实现,语法上比较符合规范。
对于 Date 类型:
因为 valueOf 方法,日期类定义的 valueOf()方法会返回它的一个内部表示:1970 年 1 月 1 日以来的毫秒数.因此我们可以在 Date 的原型上定义克隆的方法:
Date.prototype.clone=function(){
eturn new Date(this.valueOf());
}
var date=new Date('2010');
var newDate=date.clone();
// newDate-> Fri Jan 01 2010 08:00:00 GMT+0800
对于正则对象 RegExp:
RegExp.prototype.clone = function() {
var pattern = this.valueOf();
var flags = '';
flags += pattern.global ? 'g' : '';
flags += pattern.ignoreCase ? 'i' : '';
flags += pattern.multiline ? 'm' : '';
return new RegExp(pattern.source, flags);
};
var reg=new RegExp('/111/');
var newReg=reg.clone();
//newReg-> /\/111\//
54.简单实现 Node 的 Events 模块
简介:观察者模式或者说订阅模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。
node 中的 Events 模块就是通过观察者模式来实现的:
var events=require('events');
var eventEmitter=new events.EventEmitter();
eventEmitter.on('say',function(name){
console.log('Hello',name);
})
eventEmitter.emit('say','Jony yu');
这样,eventEmitter 发出 say 事件,通过 On 接收,并且输出结果,这就是一个订阅模式的实现,下面我们来简单的实现一个 Events 模块的 EventEmitter。
(1)实现简单的 Event 模块的 emit 和 on 方法
function Events(){
this.on=function(eventName,callBack){
if(!this.handles){
this.handles={};
}
if(!this.handles[eventName]){
this.handles[eventName]=[];
}
this.handles[eventName].push(callBack);
}
this.emit=function(eventName,obj){
if(this.handles[eventName]){
for(var i=0;o<this.handles[eventName].length;i++){
this.handles[eventName][i](obj);
}
}
}
return this;
}
这样我们就定义了 Events,现在我们可以开始来调用:
var events=new Events();
events.on('say',function(name){
console.log('Hello',nama)
});
events.emit('say','Jony yu');
//结果就是通过 emit 调用之后,输出了 Jony yu
(2)每个对象是独立的
因为是通过 new 的方式,每次生成的对象都是不相同的,因此:
var event1=new Events();
var event2=new Events();
event1.on('say',function(){
console.log('Jony event1');
});
event2.on('say',function(){
console.log('Jony event2');
})
event1.emit('say');
event2.emit('say');
//event1、event2 之间的事件监听互相不影响
//输出结果为'Jony event1' 'Jony event2'
55.箭头函数中 this 指向举例
var a=11;
function test2(){
this.a=22;
let b=()=>{console.log(this.a)
}
b();
}
var x=new test2();
//输出 22 定义时绑定。
56.数组常用方法
数组基本操作可以归纳为 增、删、改、查,需要留意的是哪些方法会对原数组产生影响,哪些方法不会下面对数组常用的操作方法做一个归纳
增
下面前三种是对原数组产生影响的增添方法,第四种则不会对原数组产生影响
- push()
- unshift()
- splice()
- concat()
push()
push()方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度
let colors = []; // 创建一个数组
let count = colors.push("red", "green"); // 推入两项
console.log(count) // 2
unshift()
unshift()在数组开头添加任意多个值,然后返回新的数组长度
let colors = new Array(); // 创建一个数组
let count = colors.unshift("red", "green"); // 从数组开头推入两项
alert(count); // 2
splice()
传入三个参数,分别是开始位置、0(要删除的元素数量)、插入的元素,返回空数组
let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 0, "yellow", "orange")
console.log(colors) // red,yellow,orange,green,blue
console.log(removed) // []
concat()
首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组
let colors = ["red", "green", "blue"];
let colors2 = colors.concat("yellow", ["black", "brown"]);
console.log(colors); // ["red", "green","blue"]
console.log(colors2); // ["red", "green", "blue", "yellow", "black", "brown"]
删
下面三种都会影响原数组,最后一项不影响原数组:
- pop()
- shift()
- splice()
- slice()
pop()
pop() 方法用于删除数组的最后一项,同时减少数组的length 值,返回被删除的项
let colors = ["red", "green"]
let item = colors.pop(); // 取得最后一项
console.log(item) // green
console.log(colors.length) // 1
shift()
shift()方法用于删除数组的第一项,同时减少数组的length 值,返回被删除的项
let colors = ["red", "green"]
let item = colors.shift(); // 取得第一项
console.log(item) // red
console.log(colors.length) // 1
slice()
slice() 用于创建一个包含原有数组中一个或多个元素的新数组,不会影响原始数组
let colors = ["red", "green", "blue", "yellow", "purple"];
let colors2 = colors.slice(1);
let colors3 = colors.slice(1, 4);
console.log(colors) // red,green,blue,yellow,purple
concole.log(colors2); // green,blue,yellow,purple
concole.log(colors3); // green,blue,yellow
改
即修改原来数组的内容,常用splice
splice()
传入三个参数,分别是开始位置,要删除元素的数量,要插入的任意多个元素,返回删除元素的数组,对原数组产生影响
let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 1, "red", "purple"); // 插入两个值,删除一个元素
console.log(colors); // red,red,purple,blue
console.log(removed); // green,只有一个元素的数组
查
即查找元素,返回元素坐标或者元素值
- indexOf()
- includes()
- find()
indexOf()
返回要查找的元素在数组中的位置,如果没找到则返回-1
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.indexOf(4) // 3
includes()
返回要查找的元素在数组中的位置,找到返回true,否则false
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.includes(4) // true
find()
返回第一个匹配的元素
const people = [
{
name: "Matt",
age: 27
},
{
name: "Nicholas",
age: 29
}
];
people.find((element, index, array) => element.age < 28)
// {name: "Matt", age: 27}
排序方法
数组有两个方法可以用来对元素重新排序:
- reverse()
- sort()
reverse()
顾名思义,将数组元素方向排列
let values = [1, 2, 3, 4, 5];
values.reverse();
alert(values); // 5,4,3,2,1
sort()
sort()方法接受一个比较函数,用于判断哪个值应该排在前面
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
let values = [0, 1, 5, 10, 15];
values.sort(compare);
alert(values); // 0,1,5,10,15
转换方法
常见的转换方法有:
join()
join() 方法接收一个参数,即字符串分隔符,返回包含所有项的字符串
let colors = ["red", "green", "blue"];
alert(colors.join(",")); // red,green,blue
alert(colors.join("||")); // red||green||blue
迭代方法
常用来迭代数组的方法(都不改变原数组)有如下:
- some()
- every()
- forEach()
- filter()
- map()
some()
对数组每一项都运行传入的函数,如果有一项函数返回 true ,则这个方法返回 true
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let someResult = numbers.every((item, index, array) => item > 2);
console.log(someResult) // true
every()
对数组每一项都运行传入的函数,如果对每一项函数都返回 true ,则这个方法返回 true
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let everyResult = numbers.every((item, index, array) => item > 2);
console.log(everyResult) // false
forEach()
对数组每一项都运行传入的函数,没有返回值
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.forEach((item, index, array) => {
// 执行某些操作
});
filter()
对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let filterResult = numbers.filter((item, index, array) => item > 2);
console.log(filterResult); // 3,4,5,4,3
map()
对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let mapResult = numbers.map((item, index, array) => item * 2);
console.log(mapResult) // 2,4,6,8,10,8,6,4,2
数组去重
数组去重,一般都是在面试的时候才会碰到,一般是要求手写数组去重方法的代码。
在真实的项目中碰到的数组去重,一般都是后台去处理,很少让前端处理数组去重。虽然日常项目用到的概率比较低,但还是需要了解一下,以防面试的时候可能回被问到。
1)es6 Set方法去重
//es6方法数组去重,方法1
let arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
let s = [...new Set(arr)];
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}] //{}空对象没有去除
//es6方法数组去重,方法2
function dedupe(array) {
return Array.from(new Set(array)); //直接new Set()出来是个对象,Array.from()能把set结构转换为数组
}
2)利用indexOf去重
新建一个空数组,for循环原数组,判断结果数组是否存在当前元素,有相同的值跳过,不相同则push进新数组
//利用indexOf去重
fliterArray (array) {
const tempArr = []
for (var i = 0; i < array.length; i++) {
if (tempArr.indexOf(array[i]) === -1) {
tempArr.push(array[i])
console.log(tempArr, 'tempArr')
}
}
return tempArr
},
const array4 = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}]
this.fliterArray(array4)
// [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}] //NaN、{}没有去重
3)利用includes
fliterArray (array) {
const tempArr = []
for (var i = 0; i < array.length; i++) {
if (!tempArr.includes(array[i])) {
tempArr.push(array[i])
console.log(tempArr, 'tempArr')
}
}
return tempArr
},
const array4 = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}]
this.fliterArray(array4)
// [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}] //{}没有去重
4) Object 键值对去重;
把数组的值存成 Object 的 key 值,比如
Object[value1] = true,在判断另一个值的时候,如果 Object[value2]存在的 话,就说明该值是重复的。
57.去除字符串首尾空格
function trimStr(str){
return str.replace(/(^\s*)|(\s*$)/g,"");
}
a = ' runoob '
console.log(trimStr(a));
58.性能优化
减少 HTTP 请求
使用内容发布网络(CDN)
添加本地缓存
压缩资源文件
将 CSS 样式表放在顶部,把 javascript 放在底部(浏览器的运行机制决定)
避免使用 CSS 表达式
减少 DNS 查询
使用外部 javascript 和 CSS
避免重定向
图片 lazyLoad
59.能来讲讲 JS 的语言特性吗
运行在客户端浏览器上;
不用预编译,直接解析执行代码;
是弱类型语言,较为灵活;
与操作系统无关,跨平台的语言;
脚本语言、解释性语言
60.你说到 typeof,能不能加一个限制条件达到判断条件
typeof 只能判断是 object,可以判断一下是否拥有数组的方法
61.JS 实现跨域
这里说的js跨域是指通过js在不同的域之间进行数据传输或通信,比如用ajax向一个不同的域请求数据,或者通过js获取页面中不同域的框架中(iframe)的数据。只要协议、域名、端口有任何一个不同,都被当作是不同的域。
要解决跨域的问题,我们可以使用以下几种方法:
- 如果是协议和端口造成的跨域问题“前台”是无能为力的;
- 在跨域问题上,域仅仅是通过“URL的首部”来识别而不会去尝试判断相同的ip地址对应着两个域或两个域是否在同一个ip上。
JSONP : 通 过 动 态 创 建 script , 再 请 求 一 个 带 参 网 址 实 现 跨 域 通 信 。
JSONP包含两部分:回调函数和数据。
回调函数:当响应到来时要放在当前页面被调用的函数。
数据:就是传入回调函数中的json数据,也就是回调函数的参数了。
在js中,我们直接用XMLHttpRequest请求不同域上的数据时,是不可以的。但是,在页面上引入不同域上的js脚本文件却是可以的,jsonp正是利用这个特性来实现的。
jsonp虽然很简单,但是有如下缺点:
1)安全问题(请求代码中可能存在安全隐患)
2)要确定jsonp请求是否失败并不容易
document.domain + iframe 跨域:
两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。
浏览器同源策略限制:
(1)不能通过ajax的方法去请求不同源中的文档。
(2)浏览器中不同域的框架之间是不能进行js的交互操作的。
所以,在不同的框架之间(父子或同辈),是能够获取到彼此的window对象的,但不能使用获取到的window对象的属性和方法(html5中的postMessage方法是一个例外),总之,你可以当做是只能获取到一个几乎无用的window对象。
注意,document.domain的设置是有限制的:
我们只能把document.domain设置成自身或更高一级的父域,且主域必须相同。
例如:a.b.c.com 中某个文档的document.domain 可以设成a.b.c.com、b.c.com 、c.com中的任意一个
location.hash + iframe 跨域:a 欲与 b 跨域相互通信,通过中间页 c 来实现。 三个页面,不同域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通信。
window.name + iframe 跨域:通过 iframe 的 src 属性由外域转向本地域,跨域 数据即由 iframe 的 window.name 从外域传递到本地域。
postMessage 跨域:可以跨域操作的 window 属性之一。
CORS:服务端设置 Access-Control-Allow-Origin 即可,前端无须设置,若要带 cookie 请求,前后端都需要设置。
代理跨域:启一个代理服务器,实现数据的转发
62.js 深度拷贝一个元素的具体实现
var deepCopy = function(obj) {
if (typeof obj !== 'object') return;
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
}
}
return newObj;
}
63.重排和重绘
重绘(repaint 或 redraw):当盒子的位置、大小以及其他属性,例如颜色、字 体大小等都确定下来之后,浏览器便把这些原色都按照各自的特性绘制一遍,将内容呈现在页面上。重绘是指一个元素外观的改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。
触发重绘的条件:改变元素外观属性。如:color,background-color 等。
注意:table 及其内部元素可能需要多次计算才能确定好其在渲染树中节点的属性值,比同等元素要多花两倍时间,这就是我们尽量避免使用 table 布局页面的原因之一。
重排(重构/回流/reflow):当渲染树中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建, 这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。
重绘和重排的关系:在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。所以,重排必定会引发重绘,但重绘不一定会引发重排。
64.JS 的全排列
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
function Permutation(str){
if(str.length == 0){
return [];
}
var result = [];
if(str.length == 1){
return [str];
}else{
//把str分为两部分,第一部分为第一个字母str[0],第二部分为剩余的字符串str.slice(1),把Permutation(str.slice(1))作为一个已知量。
var rest = Permutation(str.slice(1));
//递归,Permutation(str.slice(1))表示剩余部分字符串的全排列。
for (var j = 0; j < rest.length; j++) {//插空
for (var k = 0; k < rest[j].length+1; k++) {
var temp = rest[j].slice(0,k)+str[0]+rest[j].slice(k);
result.push(temp);
}
}
//去掉result中重复的元素
var res = [];
for(var k = 0;k<result.length;k++){
if(res.indexOf(result[k]) === -1){
res.push(result[k]);
}
}
return res.sort();
}
}
65.不同数据类型的值的比较
数据类型之间的比较规则:
{} == {} :两个对象进行比较,比较的是堆内存的地址,如果空间地址相同就是 true,不同就是 false
null 和 undefined 永远不等于任何一种数据类型,但是
null == undefined => true;
null===undefined => false
NaN 永远不等于任何一种数据类型,包括它自己
可以使用 Object.is(NaN, NaN)->true 检测
NaN == NaN => false : NaN和谁都不相等
对象和字符串进行比较,是把对象toString()转换为字符串后再比较
剩余的所有数据类型不一样的情况:都是先转换为数字
(1)对象转数字:先转换为字符串,再转换为数字
(2)字符串转换为数字:只要出现非数字字符,结果就是NaN
(3)布尔转数字:true=>1 false=>0
(4)null转数字:0
(5)undefined转数字:NaN
66、null == undefined 为什么
要比较相等性之前,不能将 null 和 undefined 转换成其他任何值,
但 null == undefined 会返回 true 。ECMAScript 规范中是这样定义的。
虽然 undefined 和 null 的语义和场景不同,但总而言之,它们都表示的是一个无效的值。 因此,在JS中对这类值访问属性时,都会得到异常的结果。
ECMAScript 规范认为,既然 null 和 undefined 的行为很相似,并且都表示 一个无效的值,那么它们所表示的内容也具有相似性,
undefined 表示一个变量自然的、最原始的状态值,而 null 则表示一个变量被人为的设置为空对象,而不是原始状态。所以,在实际使用过程中,为了保证变量所代表的语义,不要对一个变量显式的赋值 undefined,当需要释放一个对象时,直接赋值为 null 即可。
67、return
【1】构造函数通常不使用 return 关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会显式返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值。
【2】如果构造函数使用 return 语句但没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果。
【3】如果构造函数显式地使用 return 语句返回一个对象,那么调用表达式的值就是这个对象。
68、暂停死区
在代码块内,使用 let、const 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”
只要块级作用域存在let命令,它所声明的变量就“绑定这个区域”,不受外部影响,
ES6规定暂时性死区和let const语句不出现变量提升,主要是为了减少运行时错误,防止变量在声明前就使用这个变量,从而导致意外行为。
69、AngularJS 双向绑定原理
什么是AngularJS
AngularJS是一个JavaScript框架,它诞生于2009年,由Misko Hevery 等人创建,后为Google所收购。是一款优秀的前端JS框架,已经被用于Google的多款产品当中。
AngularJS有着诸多特性,最为核心的是:MVVM、模块化、自动化双向数据绑定、语义化标签、依赖注入等等。
什么是数据绑定
首先我们要理解什么是数据绑定。目前我们所看到的网站页面中,基本是由数据和设计两部分组成。将设计转换成浏览器能理解的语言,便是html和css主要做的工作。而将数据显示在页面上,并且有一定的交互效果(比如点击等用户操作及对应的页面反应)则是js主要完成的工作。出于用户体验、数据安全等方面考虑,很多时候我们不会一提交数据就刷新页面(get请求),而是通过向后端请求相关数据,然后通过无刷新加载(ajax)的方式进行页面更新(post请求)。当数据进行更新后,页面上相应的位置也能自动做出对应的修改,这就是数据绑定。可以看出,数据绑定是M(model,数据)通过VM(model-view,数据与页面之间的变换规则)向V(view)的一个修改。
什么是双向绑定
所谓的双向绑定,就是从界面的操作能实时反映到数据,也就是数据的变更能够实时展现到界面。
在新的框架中(angualr,react,vue等),通过对数据的监视,一旦数据发生变化,就根据已经写好的规则对页面进行修改,这样便实现了V——M——VM——V的一个双向绑定。
它是相当于增加了一条反向的路,在用户操作页面(比如在Input中输入值)的时候,数据能及时发生变化,并且根据数据的变化,页面的另一处也做出对应的修改。有一个常见的例子就是淘宝中的购物车,在商品数量发生变化的时候,商品价格也能及时变化。
双向绑定的实现
AngularJs主要通过scopes模型实现数据双向绑定。AngularJS的scopes包括以下四个主要部分:
- digest循环以及dirty-checking,包括 w a t c h , watch, watch,digest,和$apply。
- Scope继承 - 这项机制使得我们可以创建scope继承来分享数据和事件。
- 对集合 – 数组和对象 – 的有效dirty-checking。
- 事件系统 - o n , on, on,emit,以及$broadcast。
AngularJs 为 scope 模型上设置了一个 监听队列$watch,用来监听数据变化并更新 view 。每次绑定一个东西到 view(html) 上时 AngularJs 就会往 $watch 队列里插入一条 w a t c h ,用来检测它监视的 m o d e l 里是否有变化的东西。当浏览器接收到可以被 a n g u l a r c o n t e x t 处理的事件时, watch,用来检测它监视的 model 里是否有变化的东西。当浏览器接收到可以被 angular context 处理的事件时, watch,用来检测它监视的model里是否有变化的东西。当浏览器接收到可以被angularcontext处理的事件时,digest 循环就会触发。$digest 会遍历所有的 $watch。从而更新DOM。
Angular 将双向绑定转换为一堆 watch 表达式,然后递归这些表达式检查是否发生过变化,如果变了则执行相应的 watcher 函数(指 view 上的指令,如 ng-bind, ng-show 等或是{{}})。等到 model 中的值不再发生变化,也就不会再有 watcher 被触发,一个完整的 digest 循环就完成了。
Angular 中在 view 上声明的事件指令,如:ng-click、ng-change 等,会将浏览器的事件转发给$scope 上相应的 model 的响应函数。等待相应函数改变 model, 紧接着触发脏检查机制刷新 view。
watch 表达式:可以是一个函数、可以是 s c o p e 上的一个属性名,也可以是一个字符串形式的表达式。 scope 上的一个属性名,也可以是一个字符串形式的表达式。 scope上的一个属性名,也可以是一个字符串形式的表达式。watch 函数所监听的对象叫做 watch 表达式。watcher 函数:指在 view 上的指令(ngBind,ngShow、ngHide 等)以及{{}}表达式,他们所注册的函数。每一个 watcher 对象都包括:监听函数,上次变化的值,获取监听表达式的方法以及监听表达式,最后还包括是否需要使用深度对比 (angular.equals())
70、写一个深度拷贝
function clone( obj ) {
var copy;
switch( typeof obj ) {
case "undefined":
break;
case "number":
copy = obj - 0;
break;
case "string":
copy = obj + "";
break;
case "boolean":
copy = obj;
break;
case "object":
//object 分为两种情况 对象(Object)和数组(Array)
if(obj === null) {
copy = null;
} else {
if( Object.prototype.toString.call(obj).slice(8, -1) === "Array") {
copy = [];
for( var i = 0 ; i < obj.length ; i++ ) {
copy.push(clone(obj[i]));
}
} else {
copy = {};
for( var j in obj) { copy[j] = clone(obj[j]);
}
}
}
break;
default:
copy = obj;
break;
}
return copy;
}
71、 requestAnimationFrame**,**请问是怎么使用的
在Web应用中,实现动画效果的方法比较多,Javascript 中可以通过定时器 setTimeout/ setInterval 来实现,css3 可以使用 transition和 animation 来实现,html5 中的 canvas 也可以实现。除此之外,html5 还提供一个专门用于请求动画的API,那就是 requestAnimationFrame。
setTimeout/ setInterval 的显著缺陷就是设定的时间并不精确,它们只是在设定的时间后将相应任务添加到任务队列中,而任务队列中如果还有前面的任务尚未执行完毕,那么后添加的任务就必须等待,这个等待的时间造成了原本设定的动画时间间隔不准。requestAnimationFrame的到来就是解决这个问题的 ,它采用的是系统时间间隔(约16.7ms),保持最佳绘制效果与效率,使各种网页动画有一个统一的刷新机制,从而节省系统资源,提高系统性能。
requestAnimationFrame() 方法告诉浏览器您希望执行动画并请求浏览器在下 一次重绘之前调用指定的回调函数来更新动画。该方法使用一个回调函数作为参数, 这个回调函数会在浏览器重绘之前调用。
window.requestAnimationFrame(callback);
callback:下一次重绘之前更新动画帧所调用的函数(即上面所说的回调函数)。回调函数会被传入DOMHighResTimeStamp参数(时间戳,十进制数,单位是毫秒,最小精度为1ms),DOMHighResTimeStamp指示当前被 requestAnimationFrame()排序的回调函数被触发的时间。
返回值:是个非零值( long 整数),请求 ID ,是回调列表中唯一的标识。可以传这个值给 window.cancelAnimationFrame() 以取消回调函数。
注意:若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用window.requestAnimationFrame()
72、有一个游戏叫做 Flappy Bird,就是一只小鸟在飞,前面是无尽的沙漠,上下不断有钢管生成,你要躲避钢管。然后小明在玩这个游戏时候老是卡顿甚至崩溃,说出原因(3-5 个)以及解决办法(3-5 个)
原因可能是:
1.内存溢出问题。
2.资源过大问题。
3.资源加载问题。
4.canvas 绘制频率问题
解决办法:
1.针对内存溢出问题,我们应该在钢管离开可视区域后,销毁钢管,让垃圾收集器回收钢管,因为不断生成的钢管不及时清理容易导致内存溢出游戏崩溃。
2.针对资源过大问题,我们应该选择图片文件大小更小的图片格式,比如使用 webp、png 格式的图片,因为绘制图片需要较大计算量。
3.针对资源加载问题,我们应该在可视区域之前就预加载好资源,如果在可视区域生成钢管的话,用户的体验就认为钢管是卡顿后才生成的,不流畅。
4.针对 canvas 绘制频率问题,我们应该需要知道大部分显示器刷新频率为 60次/s,因此游戏的每一帧绘制间隔时间需要小于 1000/60=16.7ms,才能让用户觉得不卡顿。
(注意因为这是单机游戏,所以回答与网络无关)
73、 什么是按需加载
当用户触发了动作时才加载对应的功能。触发的动作,是要看具体的业务场景而言,包括但不限于以下几个情况:鼠标点击、输入文字、拉动滚动条,鼠标移动、 窗口大小更改等。加载的文件,可以是 JS、图片、CSS、HTML 等。
74、virtual dom
虚拟dom最先是由facebook团队提出的,最先运用在react中,之后在vue2.0版本中引入了虚拟DOM的概念。
虚拟 dom 是相对于浏览器所渲染出来的真实 dom 的,以往,我们改变更新页面,只能通过首先查找dom对象,再进行修改dom的方式来达到目的。 但这种方式相当消耗计算资源, 因为每次查询 dom ,都需要遍历整颗 dom 树。
现在,我们用对象的方式来描述真实的 dom,并且通过对象与真实dom建立了一一对应的关系,那么每次 dom 的更改,我通过找到相应对象,也就找到了相应的dom节点,再对其进行更新。这样的话,就能节省性能,因为js 对象的查询,比对整个dom 树的查询,所消耗的性能要少。
简单解释:
本质来说:Virtual DOM是一个JavaScript对象,通过对象的方式来表示DOM结构.将页面的状态抽象为JS对象的形式,配合不同的渲染工具,使跨平台成为了可能.通过事务处理的机制,将多次DOM的修改结果一次性的更新到页面上,从而有效的减少页面的渲染次数,减少修改DOM的重绘重排次数,提高渲染的性能.
更本质解释:
虚拟DOM是对DOM的抽象,这个对象是更加的轻量级的DOM的描述.它设计的最初的目的,就是为了更好的跨平台.举个例子,Node就没有DOM,想要实现SSR,那个一个方式就是借助虚拟DOM,因为虚拟的DOM本身就是JS对象.在代码渲染到页面之前,vue或者React会把代码转换成一个对象(虚拟DOM).以对象的形式来描述真实DOM结构,最终渲染到页面.在每次数据发生变化前,虚拟DOM就会缓存一份,变化之前,现在的虚拟DOM会与缓存的虚拟DOM进行比较.在Vue或React中内部封装了diff算法,通过这个算法来进行比较,渲染时修改
改变的变化,原先没有发生变化的通过原先数据进行渲染.最重要的一点,前端的框架的一个基本要求就是无需手动操纵DOM,一方面时因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动操作DOM。可以大大的提高开发的效率.
75、webpack 用来干什么的
webpack 是一个现代 JavaScript 应用程序的静态模块打包器 (module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图 (dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
76、ant-design 优点和缺点
优点:组件非常全面,样式效果也都比较不错。
缺点:框架自定义程度低,默认 UI 风格修改困难
77、写一个函数,第一秒打印 1,第二秒打印 2
两个方法,第一个是用 let 块级作用域
for(let i=0;i<5;i++){
setTimeout(function(){
console.log(i)
},1000*i)
}
第二个方法闭包
for (var i=0; i<10; i++) {
function fn(j) {
return function() {
setTimeout(_ => {
console.log(j)
}, j*1000)
}
}
fn(i)();
}
使用立即执行函数 (IIFE)
for(var i=0;i<5;i++){
(function(i){
setTimeout(function(){
console.log(i)
},1000*i)
})(i)
}
78、vue 的生命周期
Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模板、 挂载 Dom、渲染→更新→渲染、销毁等一系列过程,我们称这是 Vue 的生命周期。
通俗说就是 Vue 实例从创建到销毁的过程,就是生命周期。
每一个组件或者实例都会经历一个完整的生命周期,总共分为三个阶段:初始化、 运行中、销毁。
实例、组件通过 new Vue() 创建出来之后会初始化事件和生命周期,然后就会执行 beforeCreate 钩子函数,这个时候,数据还没有挂载呢,只是一个空壳, 无法访问到数据和真实的 dom,一般不做操作
挂载数据,绑定事件等等,然后执行 created 函数,这个时候已经可以使用到数据,也可以更改数据,在这里更改数据不会触发 updated 函数,在这里可以在渲染前倒数第二次更改数据的机会,不会触发其他的钩子函数,一般可以在这里做初始数据的获取
接下来开始找实例或者组件对应的模板,编译模板为虚拟 dom 放入到 render 函数中准备渲染,然后执行 beforeMount 钩子函数,在这个函数中虚拟 dom 已经创建完成,马上就要渲染,在这里也可以更改数据,不会触发 updated,在这里可以在渲染前最后一次更改数据的机会,不会触发其他的钩子函数,一般可以在这里做初始数据的获取
接下来开始 render,渲染出真实 dom,然后执行 mounted 钩子函数,此时,组件已经出现在页面中,数据、真实 dom 都已经处理好了,事件都已经挂载好了,可以在这里操作真实 dom 等事情…
当组件或实例的数据更改之后,会立即执行 beforeUpdate,然后 vue 的虚拟 dom 机制会重新构建虚拟 dom 与上一次的虚拟 dom 树利用 diff 算法进行对比之后重新渲染,一般不做什么事儿
当更新完成后,执行 updated,数据已经更改完成,dom 也重新 render 完成,可以操作更新后的虚拟 dom
当经过某种途径调用$destroy 方法后,立即执行 beforeDestroy,一般在这里做一些善后工作,例如清除计时器、清除非指令绑定的事件等等
组件的数据绑定、监听…去掉后只剩下 dom 空壳,这个时候,执行 destroyed,在这里做善后工作也可以
79、简单介绍一下 symbol
Symbol 是 ES6 的新增属性,代表用给定名称作为唯一标识,这种类型的值可以这样创建,let id=symbol(“id”)
Symbl 确保唯一,即使采用相同的名称,也会产生不同的值,我们创建一个字段,仅为知道对应 symbol 的人能访问,使用 symbol 很有用,symbol 并不是 100%隐藏,有内置方法 Object.getOwnPropertySymbols(obj)可以获得所有的 symbol。
也有一个方法 Reflect.ownKeys(obj)返回对象所有的键,包括 symbol。
所以并不是真正隐藏。但大多数库内置方法和语法结构遵循通用约定他们是隐藏的。
80、什么是事件监听
addEventListener()方法,用于向指定元素添加事件句柄,它可以更简单的控制事件,语法为
element.addEventListener(event, function, useCapture);
第一个参数是事件的类型(如 “click” 或 “mousedown”).
第二个参数是事件触发后调用的函数。
第三个参数是个布尔值用于描述事件是冒泡还是捕获。该参数是可选的。
事件传递有两种方式,冒泡和捕获
事件传递定义了元素事件触发的顺序,如果你将 P 元素插入到 div 元素中,用户点击 P 元素, 在冒泡中,内部元素先被触发,然后再触发外部元素, 捕获中,外部元素先被触发,在触发内部元素。
80、介绍一下 promise,及其底层如何实现
Promise 是一个对象,保存着未来将要结束的事件,她有两个特征:
1、对象的状态不受外部影响,Promise 对象代表一个异步操作,有三种状态, pending 进行中,fulfilled 已成功,rejected 已失败,只有异步操作的结果, 才可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也就是 promise 名字的由来
2、一旦状态改变,就不会再变,promise 对象状态改变只有两种可能,从 pending 改到 fulfilled 或者从 pending 改到 rejected,只要这两种情况发生,状态就凝固了,不会再改变,这个时候就称为定型 resolved,
Promise 的基本用法,
let promise1 = new Promise(function(resolve,reject){
setTimeout(function(){
resolve('ok')
},1000)
})
promise1.then(function success(val){
console.log(val)
})
最简单代码实现 promise
class PromiseM {
constructor (process) {
this.status = 'pending'
this.msg = ''
process(this.resolve.bind(this), this.reject.bind(this))
return this
}
resolve (val) {
this.status = 'fulfilled'
this.msg = val
}
reject (err) {
this.status = 'rejected'
this.msg = err
}
then (fufilled, reject) {
if(this.status === 'fulfilled') {
fufilled(this.msg)
}
if(this.status === 'rejected') {
reject(this.msg)
}
}
}
//测试代码
var mm=new PromiseM(function(resolve,reject){
resolve('123');
});
mm.then(function(success){
console.log(success);
},function(){
console.log('fail!');
});
81、说说 C++,Java,JavaScript 这三种语言的区别
从静态类型还是动态类型来看
静态类型,编译的时候就能够知道每个变量的类型,编程的时候也需要给定类型,
如 Java 中的整型 int,浮点型 float 等。C、C++、Java 都属于静态类型语言。
动态类型,运行的时候才知道每个变量的类型,编程的时候无需显示指定类型, 如 JavaScript 中的 var、PHP 中的$。JavaScript、Ruby、Python 都属于动态类型语言。
静态类型还是动态类型对语言的性能有很大影响。
对于静态类型,在编译后会大量利用已知类型的优势,如 int 类型,占用 4 个字节,编译后的代码就可以用内存地址加偏移量的方法存取变量,而地址加偏移量的算法汇编很容易实现。
对于动态类型,会当做字符串通通存下来,之后存取就用字符串匹配。
从编译型还是解释型来看
编译型语言,像 C、C++,需要编译器编译成本地可执行程序后才能运行,由开发人员在编写完成后手动实施。用户只使用这些编译好的本地代码,这些本地代码由系统加载器执行,由操作系统的 CPU 直接执行,无需其他额外的虚拟机等。
源代码=》抽象语法树=》中间表示=》本地代码
解释性语言,像 JavaScript、Python,开发语言写好后直接将代码交给用户, 用户使用脚本解释器将脚本文件解释执行。对于脚本语言,没有开发人员的编译过程,当然,也不绝对。
源代码=》抽象语法树=》解释器解释执行。
对于 JavaScript,随着 Java 虚拟机 JIT 技术的引入,工作方式也发生了改变。 可 以 将 抽 象 语 法 树 转 成 中 间 表 示 ( 字 节 码 ), 再 转 成 本 地 代 码 , 如 JavaScriptCore,这样可以大大提高执行效率。也可以从抽象语法树直接转成本地代码,如 V8 Java 语言,分为两个阶段。首先像 C++语言一样,经过编译器编译。和 C++的不同,C++编译生成本地代码,Java 编译后,生成字节码,字节码与平台无关。第二阶段,由 Java 的运行环境也就是 Java 虚拟机运行字节码,使用解释器执行这些代码。一般情况下,Java 虚拟机都引入了 JIT 技术,将字节码转换成本地代码来提高执行效率。
注意,在上述情况中,编译器的编译过程没有时间要求,所以编译器可以做大量的代码优化措施。
对于 JavaScript 与 Java 它们还有的不同:
对于 Java,Java 语言将源代码编译成字节码,这个同执行阶段是分开的。也就是从源代码到抽象语法树到字节码这段时间的长短是无所谓的。
对于 JavaScript,这些都是在网页和 JavaScript 文件下载后同执行阶段一起在 网页的加载和渲染过程中实施的,所以对于它们的处理时间有严格要求。
82、在数组原型链上实现删除数组重复数据的方法
83、什么是 JavaScript
JavaScript 一种动态类型、弱类型、基于原型的客户端脚本语言,用来给 HTML 网页增加动态功能。
动态:
在运行时确定数据类型。变量使用之前不需要类型声明,通常变量的类型是被赋值的那个值的类型。
弱类:
计算时可以不同类型之间对使用者透明地隐式转换,即使类型不正确,也能通过隐式转换来得到正确的类型。
原型:
新对象继承对象(作为模版),将自身的属性共享给新对象,模版对象称为原型。
这样新对象实例化后不但可以享有自己创建时和运行时定义的属性,而且可以享有原型对象的属性。
84、JavaScript 组成部分:
ECMAScript(核心)
作为核心,它规定了语言的组成部分:语法、类型、语句、关键字、保留字、操作符、对象。
DOM(文档对象模型)
DOM 把整个页面映射为一个多层节点结果,开发人员可借助 DOM 提供的 API,轻松地删除、添加、替换或修改任何节点。
BOM (浏览器对象模型)
支持可以访问和操作浏览器窗口的浏览器对象模型,开发人员可以控制浏览器显示的页面以外的部分。
85、写个函数,可以转化下划线命名到驼峰命名
public static String UnderlineToHump(String para){
StringBuilder result=new StringBuilder();
String a[]=para.split("_");
for(String s:a){
if(result.length()==0){
result.append(s.toLowerCase());
}else{
result.append(s.substring(0, 1).toUpperCase());
result.append(s.substring(1).toLowerCase());
}
}
return result.toString();
}
}
驼峰命名转下划线命名
// 驼峰命名转下划线命名
function toUnderLine(str){
let nstr = str.replace(/[A-Z]/g,function($0){//函数里只有一个参数时表示与 regexp 中的表达式相匹配的文本。有多个参数时表示与 regexp 中的子表达式相匹配的文本
console.log($0);//Y C 个人的理解就是第一个参数会返回正则表达式//里匹配到的所有文本
return "_"+$0.toLocaleLowerCase();
});
//防止有开头大驼峰
if(nstr.slice(0,1) == "_"){
nstr = nstr.slice(1)
}
return nstr;
}
console.log(toUnderLine("YxjComCn"));//y
下划线命名转驼峰命名
// 下划线命名转驼峰命名 name_yxj_com
function toUpperCase(str){
// 匹配一个或多个下划线+ 后一个不是以下划线开头的字符
let nstr = str.replace(/(?:_)+([^_])/g,function($0,$1){
// console.log($0);//_n _y _c
console.log($1);//n y c
return $1.toUpperCase();
});
nstr = nstr.replace(nstr[0],nstr[0].toLowerCase());
return nstr;
} console.log(toUpperCase("__name_yxj_com"));//nameYxjCom
86、深浅拷贝的区别和实现
数组的浅拷贝:
如果是数组,我们可以利用数组的一些方法,比如 slice,concat 方法返回一个新数组的特性来实现拷贝,但假如数组嵌套了对象或者数组的话,使用 concat 方法克隆并不完整,如果数组元素是基本类型,就会拷贝一份,互不影响,而如果是对象或数组,就会只拷贝对象和数组的引用,这样我们无论在新旧数组进行了修改,两者都会发生变化,我们把这种复制引用的拷贝方法称为浅拷贝,
深拷贝就是指完全的拷贝一个对象,即使嵌套了对象,两者也互相分离,修改一 个对象的属性,不会影响另一个
如何深拷贝一个数组
1、这里介绍一个技巧,不仅适用于数组还适用于对象!那就是:
var arr = ['old', 1, true, ['old1', 'old2'], {old: 1}]
var new_arr = JSON.parse( JSON.stringify(arr) );
console.log(new_arr);
原理是 JOSN 对象中的 stringify 可以把一个 js 对象序列化为一个 JSON 字符串,parse 可以把 JSON 字符串反序列化为一个 js 对象,通过这两个方法,也可以实现对象的深复制。 但是这个方法不能够拷贝函数
浅拷贝的实现:
以上三个方法 concat,slice ,JSON.stringify 都是技巧类,根据实际项目情况选择使用,我们可以思考下如何实现一个对象或数组的浅拷贝,遍历对象,然后 把属性和属性值都放在一个新的对象里即可
var shallowCopy = function(obj) {
// 只拷贝对象
if (typeof obj !== 'object') return;
// 根据 obj 的类型判断是新建一个数组还是对象
var newObj = obj instanceof Array ? [] : {};
// 遍历 obj,并且判断是 obj 的属性才拷贝
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
return newObj;
}
深拷贝的实现
那如何实现一个深拷贝呢?说起来也好简单,我们在拷贝的时候判断一下属性值
的类型,如果是对象,我们递归调用深拷贝函数不就好了~
var deepCopy = function(obj) {
if (typeof obj !== 'object') return;
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
}
}
return newObj;
}
87、JS 中 string 的 startwith 和 indexof 两种方法的区别
JS 中 startwith 函数,其参数有 3 个,stringObj,要搜索的字符串对象,str, 搜索的字符串,position,可选,从哪个位置开始搜索,如果以 position 开始的字符串以搜索字符串开头,则返回 true,否则返回 false
Indexof 函数,indexof 函数可返回某个指定字符串在字符串中首次出现的位置。
88、js 字符串转数字的方法
通过函数 parseInt(),可解析一个字符串,并返回一个整数,语法为 parseInt (string ,radix)
string:被解析的字符串
radix:表示要解析的数字的基数,默认是十进制,如果 radix<2 或>36,则返回 NaN
89、let const var 的区别 ,什么是块级作用域,如何用 ES5 的方法实现块级作用域(立即执行函数),ES6 呢
提起这三个最明显的区别是 var 声明的变量是全局或者整个函数块的,而 let,const 声明的变量是块级的变量,var 声明的变量存在变量提升,let,const 不存在,let 声明的变量允许重新赋值,const 不允许。
90、ES6 箭头函数的特性
ES6 增加了箭头函数,基本语法为
let func = value => value;
相当于
let func = function (value) {
return value;
};
箭头函数与普通函数的区别在于:
1、箭头函数没有 this,所以需要通过查找作用域链来确定 this 的值,这就意 味着如果箭头函数被非箭头函数包含,this 绑定的就是最近一层非箭头函数的 this,
2、箭头函数没有自己的 arguments 对象,但是可以访问外围函数的 arguments 对象
3、不能通过 new 关键字调用,同样也没有 new.target 值和原型
91、setTimeout 和 Promise 的执行顺序
首先我们来看这样一道题:
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function(resolve, reject) {
console.log(2)
for (var i = 0; i < 10000; i++) {
if(i === 10) {
console.log(10)
}
i == 9999 && resolve();
}
console.log(3)
}).then(function() {
console.log(4)
})
console.log(5);
//输出答案为 2 10 3 5 4 1
//可以看出Promise会比setTimeout先执行。
//首先,Promise定以后会立即执行,所以会先打印2;
//然后,resolve和reject调用不会终止Promise内的参数函数继续执行,所以会打印3;
//之后,Promise的then方法和setTimeout都是异步任务,会先执行完本轮“事件循环”,所以会打印5;
//最后,由于then方法是异步里面的微任务,而setTimeout是异步的宏任务,会先打印4.
要先弄清楚 settimeout(fun,0)何时执行,promise 何时执行,then 何时执行 settimeout 这种异步操作的回调,只有主线程中没有执行任何同步代码的前提下,才会执行异步回调,而 settimeout(fun,0)表示立刻执行,也就是用来改变任务的执行顺序,要求浏览器尽可能快的进行回调 promise 何时执行,由上图可知 promise 新建后立即执行,所以 promise 构造函数里代码同步执行的,then 方法指向的回调将在当前脚本所有同步任务执行完成后执行, 那么 then 为什么比 settimeout 执行的早呢,因为 settimeout(
fun,0)不是真的立即执行,经过测试得出结论:
执行顺序为:同步执行的代码-》promise.then->settimeout
92、有了解过事件模型吗,DOM0 级和 DOM2 级有什么区 别,DOM 的分级是什么
JS DOM 事件流存在如下三个阶段:
-
事件捕获阶段
-
处于目标阶段
-
事件冒泡阶段
JSDOM 标准事件流的触发的先后顺序为:先捕获再冒泡,
点击 DOM 节点时,事件传播顺序:事件捕获阶段,从上往下传播,然后到达事件目标节点,最后是冒泡阶段,从下往上传播DOM 节点
添加事件监听方法 addEventListener,中参数 capture 可以指定该监听是添加在事件捕获阶段还是事件冒泡阶段,为 false 是事件冒泡,为 true 是事件捕获,并非所有的事件都支持冒泡,比如 focus,blur 等等,我们可以通过 event.bubbles 来判断事件模型有三个常用方法:
event.stopPropagation:阻止捕获和冒泡阶段中,当前事件的进一步传播,
event.stopImmediatePropagetion,阻止调用相同事件的其他侦听器,
event.preventDefault,取消该事件(假如事件是可取消的)而不停止事件的进一步传播,
event.target:指向触发事件的元素,在事件冒泡过程中这个值不变
event.currentTarget = this,时间帮顶的当前元素,只有被点击时目标元素的target 才会等于 currentTarget,
最后,对于执行顺序的问题,如果 DOM 节点同时绑定了两个事件监听函数,一个用于捕获,一个用于冒泡,那么两个事件的执行顺序真的是先捕获在冒泡吗,答案是否定的,绑定在被点击元素的事件是按照代码添加顺序执行的,其他函数是先捕获再冒泡。
DOM0级和DOM2级的共同优点:
能添加多个事件处理程序,按顺序执行,HTML事件处理程序无法做到~
关于dom0级和dom2级的区别
DOM0级事件处理:同时绑定几个不同的事件,例如在绑定onclick的基础上再绑定一个onmouseover为按钮2设置背景颜色(这里注意不能onclick、onmouseover事件都设为alert弹出哦,可能有冲突,dom0和dom2都不能成功);但是不能同时绑定多个相同的事件,比如onclick;会覆盖,只会执行最后一个的函数;
DOM2级事件处理:优点:同时绑定几个事件(相同或不同),然后顺序执行,不会覆盖。缺点:不具有跨浏览器优势.
另外,IE9能兼容dom2
DOM是W3C标准;它是一个使程序和脚本有能力动态地访问和更新文档的内容,结构以及样式的平台中立的接口
DOM分类
- DOM核心:针对任何结构化文档的标准模型
- DOM XML :只针对XML文档的标准模型
- DOM HTML:只针对HTML文档的标准模型;
DOM级别:
DOM级别一共可以分为四个级别:DOM0级、DOM1级、DOM2级和DOM3级。而DOM事件分为3个级别:DOM0级事件处理,DOM2级事件处理和DOM3级事件处理。
有人可能会疑惑,为什么没有DOM1级事件处理呢?因为1级DOM标准并没有定义事件相关的内容,所以没有所谓的1级DOM事件模型。
DOM0级处理事件就是将一个函数赋值给一个事件处理属性(DOM0级事件处理程序的缺点在于一个处理程序无法同时绑定多个处理函数,比如我还想再点击按钮事件上加上另外一个函数)
DOM2级事件在DOM0级时间段额基础上弥补了一个处理处理程序
无法同时绑定多个处理函数的缺点。允许给一个程序添加多个处理函数。
DOM3级事件是在DOM2级事件的基础上添加很多事件类型。
UI事件,当用户与页面上的元素交互时触发,如:load、scroll
焦点事件,当元素获得或失去焦点时触发,如:blur、focus
鼠标事件,当用户通过鼠标在页面执行操作时触发如:dbclick、mouseup
滚轮事件,当使用鼠标滚轮或类似设备时触发,如:mousewheel
文本事件,当在文档中输入文本时触发,如:textInput
键盘事件,当用户通过键盘在页面上执行操作时触发,如:keydown、keypress
合成事件,当为IME(输入法编辑器)输入字符时触发,如:compositionstart
变动事件,当底层DOM结构发生变化时触发,如:DOMsubtreeModified
同时DOM3级事件也允许使用者自定义一些事件。
93、平时是怎么调试 JS 的
一般用 Chrome 自带的控制台
94、setTimeout(fn,100);100 毫秒是如何权衡的
setTimeout()函数只是将事件插入了任务列表,必须等到当前代码执行完,主线程才会去执行它指定的回调函数,有可能要等很久,所以没有办法保证回调函数一定会在 setTimeout 指定的时间内执行,100 毫秒是插入队列的时间+等待的时间。
95、写一个 newBind 函数,完成 bind 的功能。
bind()方法,创建一个新函数,当这个新函数被调用时,bind()的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数
Function.prototype.bind2 = function (context) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fbound = function () {
self.apply(this instanceof self ? this : context,
args.concat(Array.prototype.slice.call(arguments)));
}
fNOP.prototype = this.prototype;
fbound.prototype = new fNOP();
return fbound;
}
96、怎么获得对象上的属性:比如说通过 Object.key()
Object.keys() & Object.values() & Object.entries
这三个方法都是为了来获得对象的属性与值的,最终返回值是一个数组,不过只获取对象本身的可每枚举字符串属性。
console.log(Object.keys(obj)); //["str"]
console.log(Object.values(obj)); // ["String property"]
console.log(Object.entries(obj)); //[["str", "String property"]]
Object.getOwnPropertyNames()
Object.getOwnPropertyNames()是获取对象自身上的字符串属性,包括可枚举的与不可枚举的属性,最后返回一个数组。
console.log(Object.getOwnPropertyNames(obj)); //["str", "unenum"]
Object.getOwnPropertySymbols()
Object.getOwnPropertySymbols()是获取对象自身上的Symbol属性,包括可枚举的与不可枚举的,最后返回一个数组。
console.log(Object.getOwnPropertySymbols(obj)); //[Symbol(), Symbol(unenum)]
Reflect.ownKeys()
Reflect.ownKeys()是获取对象自身的所有属性,包括可枚举的与不可枚举的,最后返回一个数组。
console.log(Reflect.ownKeys(obj)); //["str", "unenum", Symbol(), Symbol(unenum)]
for...in..
for...in...是用来遍历对象上可枚举的字符串属性的,包括原型链上的可枚举的字符串属性。使用for...in...加上obj.hasOwnProperty(prop)就可以实现Object.keys/values/entries的功能。
for(let key in obj){
console.log(key); //"str" "foo"
}
97、给出以下代码,输出的结果是什么?原因?
for(var i=0;i<5;i++) {
setTimeout(function(){ console.log(i); },1000);
}
console.log(i)
在一秒后输出 5 个 5
每次 for 循环的时候 setTimeout 都会执行,但是里面的 function 则不会执行被放入任务队列,因此放了 5 次;for 循环的 5 次执行完之后不到 1000 毫秒;1000毫秒后全部执行任务队列中的函数,所以就是输出 5 个 5。
98、给两个构造函数 A 和 B,如何实现 A 继承 B?
function A(...) {} A.prototype...
function B(...) {} B.prototype...
A. prototype = Object.create(B.prototype);
// 再在 A 的构造函数里 new B(props);
for(var i = 0; i < lis.length; i++) {
lis[i].addEventListener('click', function(e) {
alert(i);
}, false)
}
99、能不能正常打印索引
在 click 的时候,已经变成 length 了
100、如果已经有三个 promise,A、B 和 C,想串行执行, 该怎么写?
// promise
A.then(B).then(C).catch(...)
// async/await
(async ()=>{
await a();
await b();
await c();
})()
101、知道 private 和 public 吗
public:public 表明该数据成员、成员函数是对所有用户开放的,所有用户都
可以直接进行调用
private:private 表示私有,私有的意思就是除了 class 自己之外,任何人都
不可以直接使用
102、promise 和 await/async 的关系
都是异步编程的解决方案
103、js 加载过程阻塞,解决方法。
指定 script 标签的 async 属性。
如果 async=“async”,脚本相对于页面的其余部分异步地执行(当页面继续进行解析时,脚本将被执行)
如果不使用 async 且 defer=“defer”:脚本将在页面完成解析时执行
<script src="" async>
<script src="" defer>
104、js 对象类型,基本对象类型以及引用对象类型的区别
分为基本对象类型和引用对象类型
基本数据类型:按值访问,可操作保存在变量中的实际的值。基本类型值指的是
简单的数据段。基本数据类型有这六种:undefined、null、string、number、 boolean、symbol。
引用类型:当复制保存着对象的某个变量时,操作的是对象的引用,但在为对象添加属性时,操作的是实际的对象。引用类型值指那些可能为多个值构成的对象。
引用类型有这几种:Object、Array、RegExp、Date、Function、特殊的基本包装类型(String、Number、Boolean)以及单体内置对象(Global、Math)。
105、JavaScript 中的轮播实现原理?假如一个页面上有两个轮播,你会怎么实现?
图片轮播的原理就是图片排成一行,然后准备一个只有一张图片大小的容器,对这个容器设置超出部分隐藏,在控制定时器来让这些图片整体左移或右移,这样呈现出来的效果就是图片在轮播了。
如果有两个轮播,可封装一个轮播组件,供两处调用
106、怎么实现一个计算一年中有多少周?
首先你得知道是不是闰年,也就是一年是 365 还是 366.
其次你得知道当年 1 月 1 号是周几。假如是周五,一年 365 天把 1 号 2 号 3 号减去,也就是把第一个不到一周的天数减去等于 362 还得知道最后一天是周几,加入是周五,需要把周一到周五减去,也就是 362-5=357.正常情况 357 这个数计算出来是 7 的倍数。357/7=51 。即为周数。
<script>
//判断是否是闰年
function isLeapYear(year) {
if (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0) {
console.log(year + 'is leap year')
return true
} else {
console.log(year + 'is not leap year')
return false
}
}
//获取某年某月某日是星球几
function getDate(date) {
let oDate = new Date(date)
let day = oDate.getDay()
console.log(typeof day)
switch (day) {
case 0:
console.log('星期日')
return 0
case 1:
console.log('星期一')
return 1
case 2:
console.log('星期二')
return 2
case 3:
console.log('星期三')
return 3
case 4:
console.log('星期四')
return 4
case 5:
console.log('星期五')
return 5
case 6:
console.log('星期六')
return 6
}
}
function main() {
let currentYearDays = isLeapYear(2019) ? 366 : 365
let beforeDays = 7 - getDate('2019-1-1')+1
let afterDays = getDate('2019-12-31')
let vaildDays = currentYearDays - beforeDays - afterDays
let weeks = vaildDays / 7
console.log(weeks)
}
main()
</script>
107、箭头函数和 function 有什么区别
箭头函数根本就没有绑定自己的 this,在箭头函数中调用 this 时,仅仅是简
单的沿着作用域链向上寻找,找到最近的一个 this 拿来使用
108、没有 promise 怎么办
没有 promise,可以用回调函数代替
109、arguments 是什么?
arguments 是类数组对象,有 length 属性,不能调用数组方法
可用 Array.from()转换
110、箭头函数获取 arguments
可用…rest 参数获取
let func = (...rest) => {
console.log(rest)
//[1,2,3]
}
func(1,2,3)
111、Eventloop
Event Loop即事件循环,是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。
而事件循环就是指在javascript是单线程的模式下,如果要有异步任务那么需要如何进行任务调度。
事件循环中把任务分成了两大类,同步任务和异步任务,其中异步任务又分宏任务和微任务。
其中宏任务包括:
script(整体代码)、settimeout/setinterval、setImmediate(node环境)、I/O操作、UI render、xhr(发送网络请求),
callback 等等
微任务有:
Promise的一系列方法(请注意promise本身是同步的哦!)、MutationObserver 、process.nextTick(node环境)等等
同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。异步任务不在主线程中时,要在任务队列中等待调用。
执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去检查微任务(microTask)队列是否为空,如果不为空的话,会按照先入先出的规则全部执行完微任务(microTask)后,设置微任务(microTask)队列为null,然后再执行宏任务,如此循环。
eventloop的执行顺序是:同步任务>微任务>宏任务
下面就是这个循环的步骤:
1.把同步任务队列 或者 微任务队列 或者 宏任务队列中的任务放入主线程。
2.同步任务 或者 微任务 或者 宏任务在执行完毕后会全部退出主线程。
在实际场景下大概是这么一个顺序:
1.把同步任务相继加入同步任务队列。
2.把同步任务队列的任务相继加入主线程。
3.待主线程的任务相继执行完毕后,把主线程队列清空。
4.把微任务相继加入微任务队列。
5.把微任务队列的任务相继加入主线程。
6.待主线程的任务相继执行完毕后,把主线程队列清空。
7.把宏任务相继加入宏任务队列。无time的先加入,像网络请求。有time的后加入,像setTimeout(()=>{},time),在他们中time短的先加入。
8.把宏任务队列的任务相继加入主线程。
9.待主线程的任务相继执行完毕后,把主线程队列清空。
112、说说写 JavaScript 的基本规范?
\1) 不要在同一行声明多个变量
\2) 使用 ===或!==来比较 true/false 或者数值
\3) switch 必须带有 default 分支
\4) 函数应该有返回值
\5) for if else 必须使用大括号
\6) 语句结束加分号
\7) 命名要有意义,使用驼峰命名法
113、 jQuery 使用建议
\1) 尽量减少对 dom 元素的访问和操作
\2) 尽量避免给 dom 元素绑定多个相同类型的事件处理函数,可以将多个相同类型事件处理函数合并到一个处理函数,通过数据状态来处理分支
\3) 尽量避免使用 toggle 事件
114、 Ajax 使用
全称 :Asynchronous Javascript And XML
所谓异步,就是向服务器发送请求的时候,我们不必等待结果,而是可以同时做其他的事情,等到有了结果它自己会根据设定进行后续操作,与此同时,页面是不会发生整页刷新的,提高了用户体验。
创建 Ajax 的过程:
\1) 创建 XMLHttpRequest 对象(异步调用对象)
var xhr = new XMLHttpRequest();
\2) 创建新的 Http 请求(方法、URL、是否异步)
xhr.open(‘get’,’example.php’,false);
\3) 设置响应 HTTP 请求状态变化的函数。
onreadystatechange事件中readyState属性等于4。响应的HTTP状态为200(OK) 或者 304(Not Modified)。
\4) 发送 http 请求
xhr.send(data);
\5) 获取异步调用返回的数据
注意:
\1) 页面初次加载时,尽量在 web 服务器一次性输出所有相关的数据,只在页面
加载完成之后,用户进行操作时采用 ajax 进行交互。
\2) 同步 ajax 在 IE 上会产生页面假死的问题。所以建议采用异步 ajax。
\3) 尽量减少 ajax 请求次数
\4) ajax 安全问题,对于敏感数据在服务器端处理,避免在客户端处理过滤。对于关键业务逻辑代码也必须放在服务器端处理。
115、 JavaScript 有几种类型的值?你能画一下他们的内存图吗?
基本数据类型存储在栈中,引用数据类型(对象)存储在堆中,指针放在栈中。
两种类型的区别是:存储位置不同;原始数据类型直接存储在栈中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;引用数据类型存储在堆中的对象,占据空间大、大小不固定,如果存储在栈中,将会影响程序运行的性能
引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
116、栈和堆的区别?
栈(stack):由编译器自动分配释放,存放函数的参数值,局部变量等;
堆(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由操作系统释放。
117、JavaScript 定义类的 4 种方法
1)、工厂方法
function creatPerson(name, age) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.sayName = function() {
window.alert(this.name);
};
return obj;
}
2)、构造函数方法
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function() {
window.alert(this.name);
};
}
3)、原型方法
function Person() {
}
Person.prototype = {
constructor : Person,
name : "Ning",
age : "23",
sayName : function() {
window.alert(this.name);
}
};
大家可以看到这种方法有缺陷,类里属性的值都是在原型里给定的。
4)、组合使用构造函数和原型方法(使用最广)
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
constructor : Person,
sayName : function() {
window.alert(this.name);
}
};
将构造函数方法和原型方法结合使用是目前最常用的定义类的方法。这种方法的
好处是实现了属性定义和方法定义的分离。比如我可以创建两个对象 person1
和 person2,它们分别传入各自的 name 值和 age 值,但 sayName()方法可以同时使用原型里定义的。
118、什么是 window 对象**?** 什么是 document 对象?
window 对象代表浏览器中打开的一个窗口。document 对象代表整个 html 文档。
实际上,document 对象是 window 对象的一个属性。
119、null,undefined 的区别?
null 表示一个对象被定义了,但存放了空指针,转换为数值时为 0。
undefined 表示声明的变量未初始化,转换为数值时为 NAN。
typeof(null) – object;
typeof(undefined) – undefined
120、 [“1”, “2”, “3”].map(parseInt) 答案是多少?
写法1
const arr=[1,2,3]
const result = arr.map(function(item,index){
return parseInt(item,index)
})
console.log(result)
写法2
console.log([1,2,3].map(parseInt))
//[1,NaN,NaN]
解析:
map() 方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。
map() 方法按照原始数组元素顺序依次处理元素。
Array.prototype.map()
array.map(callback[, thisArg])
callback 函数的执行规则
参数:自动传入三个参数
currentValue(当前被传递的元素);
index(当前被传递的元素的索引);
array(调用 map 方法的数组)
parseInt 方法接收两个参数
第三个参数["1", "2", "3"]将被忽略。parseInt 方法将会通过以下方式被调用
parseInt("1", 0)
parseInt("2", 1)
parseInt("3", 2)
parseInt 的第二个参数 radix 为 0 时,ECMAScript5 将 string 作为十进制数字的字符串解析;
parseInt 的第二个参数 radix 为 1 时,解析结果为 NaN;
parseInt 的第二个参数 radix 在 2—36 之间时,如果 string 参数的第一个字符(除空白以外),不属于 radix 指定进制下的字符,解析结果为 NaN。
parseInt("3", 2)执行时,由于"3"不属于二进制字符,解析结果为 NaN。
121、关于事件,IE 与火狐的事件机制有什么区别?如何阻止冒泡?
IE 为事件冒泡,Firefox 同时支持事件捕获和事件冒泡。但并非所有浏览器都支持事件捕获。jQuery 中使用 event.stopPropagation()方法可阻止冒泡;(旧 IE 的方法 ev.cancelBubble = true;)
122、javascript 代码中的"use strict";是什么意思 ? 使用它区别是什么?
除了正常模式运行外,ECMAscript 添加了第二种运行模式:“严格模式”。
“use strict”:表示使用严格模式
优点
● 消除Javascript语法的一些不严谨之处,减少一些怪异行为;
● 消除代码运行的一些不安全之处,保证代码运行的安全;
● 提高编译器效率,增加运行速度;
● 为未来新版本的Javascript做好铺垫
缺点
严格模式改变了语义。依赖这些改变可能会导致没有实现严格模式的浏览器中出现问题或者错误
严格模式的限制
1)变量必须声明后再使用
2)在使用默认参数的函数中的参数不能有同名属性,否则报错,在同一作用域中不能有同名参数
3)增加了保留字(比如protected、static和interface)
4)禁止this指向全局对象
5)不能对只读属性赋值,否则报错
6)不能使用前缀0表示八进制数,否则报错(用0O表示)
7)不能删除不可删除的属性,否则报错
8)不能使用with语句(with语句接收的对象会添加到作用域链的前端并在代码执行完之后移除)( with(obj) )
9)eval不会在它的外层作用域引入变量 ( eval(str) )
10 )eval和arguments不能被重新赋值
11)arguments不会自动反映函数参数的变化
12)不能使用arguments.callee
13)不能使用arguments.caller
14)不能使用fn.caller和fn.arguments获取函数调用的堆栈
15)不能删除变量delete prop,会报错,只能删除属性delete global[prop]
123、如何判断一个对象是否属于某个类?
if(a instanceof Person){
alert('yes');
}
// 判断对象类型最好的方式
// 对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。
Object.prototype.toString.call('') ; // [object String]
Object.prototype.toString.call(1) ; // [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 是全局对象 global 的引用
124、Javascript 中,执行时对象查找时,永远不会去查找原型的函数?
Object.hasOwnProperty(proName):是用来判断一个对象是否有你给出名称的属性。不过需要注意的是,此方法无法检查该对象的原型链中是否具有该属性,该属性必须是对象本身的一个成员。
125、 对 JSON 的了解?
全称:JavaScript Object Notation
JSON 中对象通过“{}”来标识,一个“{}”代表一个对象,
如{“AreaId”:” 123”},对象的值是键值对的形式(key:value)。
JSON 是 JS 的一个严格的子集,一种轻量级的数据交换格式,类似于 xml。数据格式简单,易于读写,占用带宽小。
JSON 是一种数据交换格式,基于文本,优于轻量,用于交换数据
JSON 可以表示数字、布尔值、字符串、null、数组(值的有序序列),以及由这些值(或数组、对象)所组成的对象(字符串与 值的映射);
JSON 使用 JavaScript 语法,但是 JSON 格式仅仅是一个文本。文本可以被任何编程语言读取及作为数据格式传递;
在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理
JSON.stringify 函数,将 js 数据结构转换为一个 JSON 字符串;传入的数据结构不符合 JSON 格式,那么会特殊处理,使其符合规范;
JSON.parse() 函数,将 JSON 格式的字符串转换为一个 js 数据结构,如果传入的字符串不是标准的 JSON 格式的字符串的话,将会抛出错误;当我们从后端接收到 JSON 格式的字符串时,我们可以通过这个方法来将其解析为一个 js 数据结构,以此来进行数据的访问 ;
应用
在项目开发中,我们使用 JSON 作为前后端数据交换的方式。在前端我们通过将一个符合 JSON 格式的数据结构序列化为 JSON 字符串,然后将它传递到后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递;
因为 JSON 的语法是基于 js 的,因此很容易将 JSON 和 js 中的对象弄混,但是我们应该注意的是 JSON 和 js 中的对象不是一回事,JSON 中对象格式更加严格,比如说在 JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等,因此大多数的 js 对象是不符合 JSON 对象的格式的;
两个函数:
JSON.parse(str)
解析 JSON 字符串 把 JSON 字符串变成 JavaScript 值或对象
JSON.stringify(obj)
将一个 JavaScript 值(对象或者数组)转换为一个 JSON 字符串
eval(‘(‘+json+’)’)
用 eval 方法注意加括号 而且这种方式更容易被攻击
126、JS 延迟加载的方式有哪些?
js延迟加载就是当页面全部加载完毕,然后再加载js文件,这样做有助于提高页面加载的速度。
defer:延迟脚本。立即下载,但延迟执行(延迟到整个页面都解析完毕后再运 行),按照脚本出现的先后顺序执行。
async:异步脚本。下载完立即执行,但不保证按照脚本出现的先后顺序执行。
1、defer属性
在script标签上,设置defer属性,可以达到异步加载js文件,延迟执行js脚本文件的目的。
1、defer属性只对外部文件有效,对本地js文件没有效果。
2、defer属性是在遇到scirpt标签时,浏览器开始异步下载,当遇到</html>标签时,表名页面加载完毕,开始执行js文件。
3、并且js文件是按顺序执行的。
2、async属性
在script标签上,设置async属性,可以达到异步加载js文件的目的。
1、async属性只对外部文件有效,对本地js文件没有效果。
2、async属性是遇到scirpt标签开始通知浏览器异步下载,下载完毕之后,就可以立即执行。
3、async设置的js文件不是按照顺序的。
3、动态创建DOM方式
动态创建script标签,当页面的全部内容加载完毕后,在执行创建挂载。
<script>
function loadJS() {
let element = document.createElement("script")
element.src = "download.js"
document.body.appendChild(element)
}
if(window.addEventListener) {
window.addEventListener("load", loadJS, false)
}else if(window.attachEvent) {
window.attachEvent("onload", loadJS)
}else {
window.onload = loadJS
}
</script>
4、使用setTimeout
在每一个脚本文件最外层设置一个定时器。
5、把js文件放在最后
当外部加载js文件时,应该将js脚本放在最后,当全部的文件都加载完成后,再开始加载执行js脚本。
127、同步和异步的区别?
同步的概念在操作系统中:不同进程协同完成某项工作而先后次序调整(通过阻塞、唤醒等方式),同步强调的是顺序性,谁先谁后。异步不存在顺序性。
同步:浏览器访问服务器,用户看到页面刷新,重新发请求,等请求完,页面刷新,新内容出现,用户看到新内容之后进行下一步操作。
异步:浏览器访问服务器请求,用户正常操作,浏览器在后端进行请求。等请求完,页面不刷新,新内容也会出现,用户看到新内容。
128、页面编码和被请求的资源编码如果不一致如何处理?
若请求的资源编码,如外引 js 文件编码与页面编码不同。可根据外引资源编码方式定义为 charset=“utf-8"或"gbk”。
比如: http://www.yyy.com/a.html 中嵌入了一个http://www.xxx.com/test.js a.html 的编码是 gbk 或 gb2312 的。而引入的 js 编码为 utf-8 的 ,那就需要在引入的时候
<script src="http://www.xxx.com/test.js" charset="utf-8"></script>
129、requireJS 的核心原理是什么?(如何动态加载的? 如何避免多次加载的?如何缓存的?)
1,概念
requireJS是基于AMD模块加载规范,使用回调函数来解决模块加载的问题。
2,原理
requireJS是使用创建script元素,通过指定script元素的src属性来实现加载模块的。
3,特点
- 实现js文件的异步加载,避免网页失去响应
- 管理模块之间的依赖,便于代码的编写和维护
4,项目优化
r.js 是基于requirejs模块化的基础上进一步的压缩和打包成一个js,请求数大大减少,便于优化
核心是 js 的加载模块,通过正则匹配模块以及模块的依赖关系,保证文件加载的先后顺序,根据文件的路径对加载过的文件做了缓存。
130、DOM 操作
(1)创建新节点
createDocumentFragment() //创建一个 DOM 片段
createElement() //创建一个具体的元素
createTextNode() //创建一个文本节点
(2)添加、移除、替换、插入
appendChild()
removeChild()
replaceChild()
insertBefore() //在已有的子节点前插入一个新的子节点
(3)查找
getElementsByTagName() //通过标签名称
getElementsByName() //通过元素的 Name 属性的值(IE 容错能力较强,会得到一个数组,其中包括 id 等于 name 值的)
getElementById() //通过元素 Id,唯一性
131、数组对象有哪些原生方法,列举一下
pop、push、shift、unshift、splice、reverse、sort、concat、join、slice、toString、indexOf、lastIndexOf、reduce、reduceRight 、forEach、map、filter、every、some
132、那些操作会造成内存泄漏
全局变量、闭包、DOM 清空或删除时,事件未清除、子元素存在引用
133、什么是 Cookie 隔离?(或者:请求资源的时候不要带 cookie 怎么做)
通过使用多个非主要域名来请求静态文件,如果静态文件都放在主域名下,那静态文件请求的时候带有的 cookie 的数据提交给 server 是非常浪费流量的,还不如隔离开。
因为 cookie 有域的限制,因此不能跨域提交请求,故使用非主要域名的时候,请求头中就不会带有 cookie 数据,这样可以降低请求头的大小,降低请求时间,从而达到降低整体请求延时的目的。
同时这种方式不会将 cookie 传入Web server,也减少了 server 对 cookie 的处理分析环节,提高了 server 的 http 请求的解析速度。
134、响应事件
onclick 鼠标点击某个对象;onfocus 获取焦点;onblur 失去焦点;onmousedown 鼠标被按下
135、flash 和 js 通过什么类如何交互?
Flash 提供了 ExternalInterface 接口与 JavaScript 通信,
ExternalInterface 有两个方法,call 和 addCallback,
call 的作用是让 Flash 调用 js 里的方法,
addCallback 是用来注册 flash 函数让 js 调用。
136、Flash 与 Ajax 各自的优缺点?
Flash:适合处理多媒体、矢量图形、访问机器。但对 css、处理文本不足,不容易被搜索。
Ajax:对 css、文本支持很好,但对多媒体、矢量图形、访问机器不足。
137、有效的 javascript 变量定义规则
第一个字符必须是一个字母、下划线(_)或一个美元符号($);其他字符可以是字母、下划线、美元符号或数字。
138、XML 与 JSON 的区别?
\1) 数据体积方面。JSON 相对于 XML 来讲,数据的体积小,传递的速度更快些。
\2) 数据交互方面。JSON 与 JavaScript 的交互更加方便,更容易解析处理,更好的数据交互。
\3) 数据描述方面。JSON 对数据的描述性比 XML 较差。
\4) 传输速度方面。JSON 的速度要远远快于 XML。
139、HTML 与 XML 的区别?
(1)XML 用来传输和存储数据,HTML 用来显示数据;
(2)XML 使用的标签不用预先定义
(3)XML 标签必须成对出现
(4)XML 对大小写敏感
(5)XML 中空格不会被删减
(6)XML 中所有特殊符号必须用编码表示
(7)XML 中的图片必须有文字说明
140、渐进增强与优雅降级
渐进增强:针对低版本浏览器进行构建页面,保证最基本的功能,然后再针对高级浏览器进行效果、交互等改进,达到更好的用户体验。
优雅降级:一开始就构建完整的功能,然后再针对低版本浏览器进行兼容。
141、Web Worker 和 Web Socket?
作用:为构建高效能的web应用提供了新的参考方案。
web socket提供更高效的传输协议,web worker提供多线程提高web应用计算效率。
web socket:在一个单独的持久连接上提供全双工、双向的通信。使用自定义的协议(ws://、wss://),同源策略对 web socket 不适用。
web worker:运行在后台的 JavaScript,不影响页面的性能。
创建 worker:var worker = new Worker(url);
向 worker 发送数据:worker.postMessage(data);
接收 worker 返回的数据:worker.onmessage
终止一个 worker 的执行:worker.terminate()
142、web 应用从服务器主动推送 data 到客户端的方式?
- AJAX 轮询(long-polling)方式,即定期发送请求,获取数据。
- Commet,即基于HTTP长连接的服务器推送技术。comet是一种用于web的推送技术,能够让服务器实时的将更新的信息传送到客户端,而不需要客户端发出请求。
- XHR长轮询,即服务器端定期返回数据,客户端接收数据,并再次发送请求。
- WebSocket,即基于Socket协议实现数据的推送。
- SSE(Server-Send Event),即允许网页获取来自服务器端的更新。
143、如何删除一个 cookie?
1) 将 cookie 的失效时间设置为过去的时间(expires)
document.cookie = ‘user=’+ encodeURIComponent(‘name’) + ';
expires=’+ new Date(0);
2) 将系统时间设置为当前时间往前一点时间
var data = new Date();
date.setDate(date.getDate()-1)
144、 Ajax 请求的页面历史记录状态问题?
(1)通过 location.hash 记录状态,让浏览器记录 Ajax 请求时页面状态的变化。
ull、数组(值的有序序列),以及由这些值(或数组、对象)所组成的对象(字符串与 值的映射);
JSON 使用 JavaScript 语法,但是 JSON 格式仅仅是一个文本。文本可以被任何编程语言读取及作为数据格式传递;
在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理
JSON.stringify 函数,将 js 数据结构转换为一个 JSON 字符串;传入的数据结构不符合 JSON 格式,那么会特殊处理,使其符合规范;
JSON.parse() 函数,将 JSON 格式的字符串转换为一个 js 数据结构,如果传入的字符串不是标准的 JSON 格式的字符串的话,将会抛出错误;当我们从后端接收到 JSON 格式的字符串时,我们可以通过这个方法来将其解析为一个 js 数据结构,以此来进行数据的访问 ;
应用
在项目开发中,我们使用 JSON 作为前后端数据交换的方式。在前端我们通过将一个符合 JSON 格式的数据结构序列化为 JSON 字符串,然后将它传递到后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递;
因为 JSON 的语法是基于 js 的,因此很容易将 JSON 和 js 中的对象弄混,但是我们应该注意的是 JSON 和 js 中的对象不是一回事,JSON 中对象格式更加严格,比如说在 JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等,因此大多数的 js 对象是不符合 JSON 对象的格式的;
两个函数:
JSON.parse(str)
解析 JSON 字符串 把 JSON 字符串变成 JavaScript 值或对象
JSON.stringify(obj)
将一个 JavaScript 值(对象或者数组)转换为一个 JSON 字符串
eval(‘(‘+json+’)’)
用 eval 方法注意加括号 而且这种方式更容易被攻击