JavaScript基础知识汇总
一、原型/原型链/构造函数/实例/继承
每个函数都有 prototype 属性,除了 Function.prototype.bind() ,该属性指向原型。
每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]] ,但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 __proto__ 来访问。
对象可以通过 __proto__ 来寻找不属于该对象的属性, __proto__ 将对象连接起来组成了原型链。
实例指的就是实例对象,而实例对象可以通过构造函数创建。实例对象本身就有着__proto__属性,实例对象的__proto__属性指向原型对象。
构造函数与一般函数的区别在于构造函数是用于创建实例对象来使用的,所以构造函数一般都是带有new运算符的函数。构造函数有着所有函数都有的属性:prototype。构造函数的prototype属性指向原型对象。
原型对象是由构造函数的prototype属性和这个构造函数创建的实例对象的__proto__属性共同指向的一个原型链上的对象。如果要判断一个构造函数与实例对象是否有着共同指向的原型对象,可以使用instanceof 来判断,具体用法是 实例对象 instanceof 构造函数。比如引用上面构造函数创建实例对象的例子:obj2 instanceof people,结果返回true。
原型对象顾名思义它也是一个对象,所以它也有对象的__proto__属性,那原型对象的__proto__属性也同样地会指向它上一层的原型对象,顺着下去,原型对象的原型对象可能还有它的上一层原型对象,这样一直到Object.prototype这个原型对象为止,这就是整个原型链。
instanceof不仅仅判断实例对象与构造函数是否有着同样指向,实际上,但凡在这个实例对象的原型链上的构造函数与对象之间,使用instanceof来判断都会返回true,所以如果要找到实例对象是直接由哪个构造函数创建的,使用instanceof不可行,这可以使用constructor来替代。比如 obj2 constructor people 就是返回true。
创建对象的几种方法:
1. 工厂模式
function createPerson(name, age, job) { let o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function () { console.log(this.name); } return o; } let person1 = createPerson("Nicholas", 29, "Software Engineer"); let person2 = createPerson("Greg", 27, "Doctor");
2. 构造函数模式
function Person(name,age,job) { this.name = name; this.age = age; this.job = job; this.sayName = function () { console.log(this.name); } } let person1 = createPerson("Nicholas", 29, "Software Engineer"); let person2 = createPerson("Greg", 27, "Doctor");
使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍 (person1.sayName !== person2.sayName)
3. 原型模式
function Person(){ } Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person(); person1.sayName(); //"Nicholas" var person2 = new Person(); person2.sayName(); //"Nicholas" alert(person1.sayName == person2.sayName); //true
我们创建的每个函数都有一个prototype
(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法,但是原型中所有属性是被实例共享的, 引用类型的属性会出问题。
3. 组合使用构造函数模式和原型模式
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.friends = ["Shelby", "Court"]; } Person.prototype = { constructor : Person, sayName : function(){ console.log(this.name); } } var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor"); person1.friends.push("Van"); console.log(person1.friends); //"Shelby,Count,Van" console.log(person2.friends); //"Shelby,Count" console.log(person1.friends === person2.friends); //false console.log(person1.sayName === person2.sayName); //true
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性constructor和方法sayName()则是在原型中定义的。而修改了person1.friends(向其中添加一个新字符串),并不会影响到person2.friends,因为它们分别引用了不同的数组。 这种构造函数与原型混成的模式,是目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。
4. 动态原型模式
function Person(name, age, job){ //属性 this.name = name; this.age = age; this.job = job; if (typeof this.sayName != "function"){ console.log(1); Person.prototype.sayName = function(){ console.log(this.name); }; } } var person1 = new Person("Nicholas", 29, "Software Engineer"); //1 var person2 = new Person("Greg", 27, "Doctor"); person1.sayName(); person2.sayName();
把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。这里只在sayName()方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了。不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映。因此,这种方法确实可以说非常完美其中,if语句检查的可以是初始化之后应该存在的任何属性或方法——不必用一大堆if语句检查每个属性和每个方法;只要检查其中一个即可。对于采用这种模式创建的对象,还可以使用instanceof
操作符确定它的类型。
本部分参考博主链接:https://juejin.im/post/5cde77c151882526015c3d11#heading-9
二、有几种方式可以实现继承
1. 原型链继承
// 定义一个动物类
function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
--原型链继承
function Cat(){ }
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
// Test Code
var cat = new Cat();
console.log(cat.name);
- 特点:基于原型链,既是父类的实例,也是子类的实例
- 缺点:无法实现多继承
2. 构造函数继承
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
// Test Code
var cat = new Cat();
console.log(cat.name);
- 特点:可以实现多继承
- 缺点:只能继承父类实例的属性和方法,不能继承原型上的属性和方法。
3. 组合继承(1和2的组合)
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
// Test Code
var cat = new Cat();
console.log(cat.name);
- 特点:可以继承实例属性/方法,也可以继承原型属性/方法
- 缺点:调用了两次父类构造函数,生成了两份实例
4. 寄生组合继承
组合继承最大的问题就是无论什么情况下,都会调用两次父类构造函数: 一次是在创建子类型原型的时候, 另一次是在子类型构造函数内部. 寄生组合式继承就是为了降低调用父类构造函数的开销而出现的.
其背后的基本思路是: 不必为了指定子类型的原型而调用超类型的构造函数
function extend(subClass, superClass) {
subClass.prototype = superClass.prototype;
subClass.superclass = superClass.prototype;
if(superClass.prototype.constructor == Object.prototype.constructor) {
superClass.prototype.constructor = superClass;
}
}
function Father(name){
this.name = name;
this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function(){
alert(this.name);
};
function Son(name,age){
Father.call(this,name);//继承实例属性,第一次调用Father()
this.age = age;
}
extend(Son,Father)//继承父类方法,此处并不会第二次调用Father()
Son.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new Son("louis",5);
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
instance1.sayName();//louis
instance1.sayAge();//5
var instance1 = new Son("zhai",10);
console.log(instance1.colors);//"red,blue,green"
instance1.sayName();//zhai
instance1.sayAge();//10
补充:new 操作符
为了追本溯源, 我顺便研究了new运算符具体干了什么?发现其实很简单,就干了三件事情.
var obj = {};
obj.__proto__ = F.prototype;
F.call(obj);
第一行,我们创建了一个空对象obj;
第二行,我们将这个空对象的__proto__成员指向了F函数对象prototype成员对象;
第三行,我们将F函数对象的this指针替换成obj,然后再调用F函数.
我们可以这么理解: 以 new 操作符调用构造函数的时候,函数内部实际上发生以下变化:
1、创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
2、属性和方法被加入到 this 引用的对象中。
3、新创建的对象由 this 所引用,并且最后隐式的返回 this.
自己实现 new 操作符
- new 操作符会返回一个对象,所以我们需要在内部创建一个对象
- 这个对象,也就是构造函数中的 this,可以访问到挂载在 this 上的任意属性
- 这个对象可以访问到构造函数原型上的属性,所以需要将对象与构造函数链接起来
- 返回原始值需要忽略,返回对象需要正常处理
function create(Con, ...args) { let obj = {} Object.setPrototypeOf(obj, Con.prototype) let result = Con.apply(obj, args) return result instanceof Object ? result : obj }
三、DOM
文档对象模型(Document Object Model,简称DOM),是W3C组织推荐的处理可扩展标志语言的标准编程接口。在网页上,组织页面(或文档)的对象被组织在一个树形结构中,用来表示文档中对象的标准模型就称为DOM
1. DOM操作
createDocumentFragment() //创建一个DOM片段 createElement() //创建一个具体的元素 createTextNode() //创建一个文本节点
-
添加:
appendChild()
-
移出:
removeChild()
-
替换:
replaceChild()
-
插入:
insertBefore()
-
复制:
cloneNode(true)
节点变化触发的事件
- DOMSubtreeModified:在DOM结构中发生任何变化时触发;
- DOMNodeInserted:在一个节点作为子节点被插入到另一个节点中时触发;
- DOMNodeRemoved:在节点从其父节点中被移除时触发;
- DOMNodeInsertedIntoDocument:在一个节点被直接插入文档中或者通过子树间接插入文档后触发。在DOMNodeInserted之后触发;
- DOMNodeRemovedFromDocument:在一个节点被直接从文档中删除或通过子树间接从文档中移除之前触发。在DOMNodeRemoved之后触发。
- DOMAttrModified:在特性被修改之后触发;
- DOMCharacterDataModified:在文本节点的值发生变化的时候触发。
//查找 getElementsByTagName() //通过标签名称 getElementsByClassName() //通过标签名称 getElementsByName() //通过元素的Name属性的值 getElementById() //通过元素Id,唯一性
子节点
Node.childNodes
//获取子节点列表NodeList; 注意换行在浏览器中被算作了text节点,如果用这种方式获取节点列表,需要进行过滤Node.firstChild
//返回第一个子节点Node.lastChild
//返回最后一个子节点
父节点
Node.parentNode
// 返回父节点Node.ownerDocument
//返回祖先节点(整个document)
同胞节点
Node.previousSibling
// 返回前一个节点,如果没有则返回nullNode.nextSibling
// 返回后一个节点
2. DOM事件
DOM事件模型分为捕获和冒泡。一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。
- 捕获阶段:事件从
window
对象自上而下目标节点传播的阶段; - 目标阶段:真正的目标节点正在处理事件的阶段;
- 冒泡阶段:事件从
目标
节点自下而上向window
对象传播的阶段。
2.1 事件捕获
捕获是从上到下,事件先从window
对象,然后再到document
(对象),然后是html
标签(通过document.documentElement获取html标签),然后是body
标签(通过document.body获取body标签),然后按照普通的html结构一层一层往下传,最后到达目标元素
。我们只需要将addEventListener
的第三个参数改为true就可以实现事件捕获。
//摘自xyyojl的《深入理解DOM事件机制》 <!-- CSS 代码 --> <style> body{margin: 0;} div{border: 1px solid #000;} #grandfather1{width: 200px;height: 200px;} #parent1{width: 100px;height: 100px;margin: 0 auto;} #child1{width: 50px;height: 50px;margin: 0 auto;} </style> <!-- HTML 代码 --> <div id="grandfather1"> 爷爷 <div id="parent1"> 父亲 <div id="child1">儿子</div> </div> </div> <!-- JS 代码 --> <script> var grandfather1 = document.getElementById('grandfather1'), parent1 = document.getElementById('parent1'), child1 = document.getElementById('child1'); grandfather1.addEventListener('click',function fn1(){ console.log('爷爷'); },true) parent1.addEventListener('click',function fn1(){ console.log('爸爸'); },true) child1.addEventListener('click',function fn1(){ console.log('儿子'); },true) /* 当我点击儿子的时候,触发顺序是爷爷 ——》父亲——》儿子 */ // 请问fn1 fn2 fn3 的执行顺序? // fn1 fn2 fn3 or fn3 fn2 fn1 </script>
2.2 事件冒泡
所谓事件冒泡就是事件像泡泡一样从最开始生成的地方一层一层往上冒。我们只需要将addEventListener
的第三个参数改为false就可以实现事件冒泡。
//html、css代码同上,js代码只是修改一下而已 var grandfather1 = document.getElementById('grandfather1'), parent1 = document.getElementById('parent1'), child1 = document.getElementById('child1'); grandfather1.addEventListener('click',function fn1(){ console.log('爷爷'); },false) parent1.addEventListener('click',function fn1(){ console.log('爸爸'); },false) child1.addEventListener('click',function fn1(){ console.log('儿子'); },false) /* 当点击儿子的时候,触发顺序:儿子——》爸爸——》爷爷 */ // 请问fn1 fn2 fn3 的执行顺序? // fn1 fn2 fn3 or fn3 fn2 fn1
3. 事件代理(事件委托)
由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)
。
3.1 优点
- 减少内存消耗,提高性能
如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的,效率上需要消耗很多性能。借助事件代理,我们只需要给父容器ul绑定方法即可,这样不管点击的是哪一个后代元素,都会根据冒泡传播的传递机制,把容器的click行为触发,然后把对应的方法执行,根据事件源,我们可以知道点击的是谁,从而完成不同的事。
- 动态绑定事件
在很多时候,我们需要通过用户操作动态的增删列表项元素,如果一开始给每个子元素绑定事件,那么在列表发生变化时,就需要重新给新增的元素绑定事件,给即将删去的元素解绑事件,如果用事件代理就会省去很多这样麻烦。
3.2 跨浏览器处理事件程序
标准事件对象:
- (1)
type
:事件类型 - (2)
target
:事件目标 - (3)
stopPropagation()
方法:阻止事件冒泡 - (4)
preventDefault()
方法:阻止事件的默认行为
IE中的事件对象:
-
(1)
type
:事件类型 -
(2)
srcElement
:事件目标 -
(3)
cancelBubble
属性:阻止事件冒泡 true表示阻止冒泡,false表示不阻止 -
(4)
returnValue
属性:阻止事件的默认行为
四、arguments
它是JS的一个内置对象,常被人们所忽略,但实际上确很重要,JS不像JAVA是显示传递参数,JS传的是形参,可以传也可以不传,若方法里没有写参数却传入了参数,那么就要用arguments来拿到这些参数了。每一个函数都有一个arguments对象,它包括了函数所要调的参数,通常我们把它当作数组使用,用它的length得到参数数量,但它却不是数组,若使用push添加数据将报错。
在函数调用的时候,浏览器每次都会传递进两个隐式参数:
1. 函数的上下文对象this
2. 封装实参的对象arguments
arguments还有属性callee,length和迭代器Symbol
1. 我们发现callee的值是函数fun,并且callee指向函数fun
function fun(){ // console.log(arguments); console.log('arguments.callee === fun的值:',arguments.callee === fun); } fun('tom',[1,2,3],{name:'Janny'});
2. 第二个属性length,我们经常在数组或者类数组中看到,可以看到arguments的原型索引__proto__的值为Object,故此我们推测arguments不是数组,而是一个类数组对象。
把arguments转换成一个真正的数组: var args = Array.prototype.slice.call(arguments);
3. 第三个属性是个Symbol类型的键,该类型的值都是独一无二的,该键指向的值是一个values函数,该值是一个生成迭代器的函数。
let arr = ['a', 'b', 'c']; let iter = arr[Symbol.iterator](); iter.next() // { value: 'a', done: false } iter.next() // { value: 'b', done: false } iter.next() // { value: 'c', done: false } iter.next() // { value: undefined, done: true }
在arguments中有同样的作用
function fun(){ console.log(arguments[Symbol.iterator]); let iterator = arguments[Symbol.iterator](); console.log('iterator:',iterator); console.log(iterator.next()); console.log(iterator.next()); console.log(iterator.next()); console.log(iterator.next()); } fun('tom',[1,2,3],{name:'Janny'});
五、数据类型判断
Boolean
Null
Undefined
Number
String
Symbol
(ECMAScript 6 新定义)Object
JS中的不可扩展对象、密封对象、冻结对象
在JavaScript
中,可以对对象的权限进行配置,通过配置,可将对象设置为不可扩展对象、密封对象、冻结对象等,以达到保护对象属性的目的。
- 如果一个对象可以添加新的属性,则这个对象是可扩展的。Object.preventExtensions()将对象标记为不再可扩展,因此它将永远不会具有超出它被标记为不可扩展的属性。注意,一般来说,不可扩展对象的属性可能仍然可被删除。尝试将新属性添加到不可扩展对象将静默失败或抛出TypeError(最常见但不排除其他情况,如在strict mode中)。
- Object.preventExtensions()仅阻止添加自身的属性。但属性仍然可以添加到对象原型。一旦使其不可扩展,就无法再对象进行扩展。
- 密封对象不可扩展,而且已有的属性成员
[[configurable]]
特性将被设置成false
(意味着不能删除属性和方法,但是可修改已有属性值),使用Object.seal()可以将对象密封 - 最严格的防止篡改级别是冻结对象,冻结的对象既不可以扩展,又是密封的,而且对象数据属性的[[writable]]特性会被设置为false。 如果定义[[Set]]函数,访问器属性仍然是可写的,使用Object.freeze()方法可以冻结对象
1. null和undefined的差异相同点:
- 在 if判断语句中,值都默认为 false
- 大体上两者都是代表无,具体看差异
差异:
null
转为数字类型值为0,而undefined转为数字类型为NaN(Not a Number)
undefined
是代表调用一个值而该值却没有赋值,这时候默认则为undefined
null
是一个很特殊的对象,最为常见的一个用法就是作为参数传入(说明该参数不是对象)- 设置为
null
的变量或者对象会被内存收集器回收
2. == 和 ===区别
==, 两边值类型不同的时候,要先进行类型转换,再比较;===,不做类型转换,类型不同的一定不等 。3、如果两个都是字符串,每个位置的字符都一样,那么[相等];否则[不相等]。
4、如果两个值都是true,或者都是false,那么[相等]。
5、如果两个值都引用同一个对象或函数,那么[相等];否则[不相等]。
6、如果两个值都是null,或者都是undefined,那么[相等]。
==规则:
1、如果两个值类型相同,进行 === 比较。
2、如果两个值类型不同,他们可能相等。根据下面规则进行类型转换再比较:
a、如果一个是null、一个是undefined,那么[相等]。
b、如果一个是字符串,一个是数值,把字符串转换成数值再进行比较。
c、如果任一值是 true,把它转换成 1 再比较;如果任一值是 false,把它转换成 0 再比较。
d、如果一个是对象,另一个是数值或字符串,把对象转换成基础类型的值再比较。对象转换成基础类型,利用它的toString或者valueOf方法。js核心内置类,会尝试valueOf先于toString;例外的是Date,Date利用的是toString转换。非js核心的对象,令说(比较麻烦,我也不大懂)
e、任何其他组合,都[不相等]。
null instanceof Object null === undefined null == undefined NaN == NaN 0 == "0" true == "20" //答案是: false false true false true false
//加法运算 console.dir(16+"5"); //156 console.dir(5+"a");//5a console.dir(5+NaN);//NaN console.dir(5+null);//5 console.dir('5'+null);//5null console.dir(5+undefined);//NaN console.dir(null+undefined);//NaN console.dir(5+5);//10 console.dir("两个数的和是"+5+5);//两个数的和是55 console.dir("两个数的和是"+(5+5));//两个数的和是10
补充:隐性转换规则
首先看双等号前后有没有NaN,如果存在NaN,一律返回false。
再看双等号前后有没有布尔,有布尔就将布尔转换为数字。(false是0,true是1)
接着看双等号前后有没有字符串, 有三种情况:
- 对方是对象,对象使用toString()或者valueOf()进行转换;
- 对方是数字,字符串转数字;(前面已经举例)
- 对方是字符串,直接比较;
- 其他返回false
如果是数字,对方是对象,对象取valueOf()或者toString()进行比较, 其他一律返回false
null, undefined不会进行类型转换, 但它们俩相等。
var undefined; undefined == null; // true 1 == true; // true 2 == true; // false 0 == false; // true 0 == ' '; // true NaN == NaN; // false [] == false; // true [] == ![]; // true // alert(!![]) //true // alert(![]) //false // alert([] == 0) //true // alert(false == 0) //true
现在来探讨 [] == ! [] 的结果为什么会是true
①、根据运算符优先级 ,! 的优先级是大于 == 的,所以先会执行 ![]
!可将变量转换成boolean类型,null、undefined、NaN以及空字符串('')取反都为true,其余都为false。
所以 ! [] 运算后的结果就是 false
也就是 [] == ! [] 相当于 [] == false
②、根据上面提到的规则(如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值——false转换为0,而true转换为1),则需要把 false 转成 0
也就是 [] == ! [] 相当于 [] == false 相当于 [] == 0
③、根据上面提到的规则(如果一个操作数是对象,另一个操作数不是,则调用对象的valueOf()方法,用得到的基本类型值按照前面的规则进行比较,如果对象没有valueOf()方法,则调用 toString())
而对于空数组,[].toString() -> '' (返回的是空字符串)
也就是 [] == 0 相当于 '' == 0
④、根据上面提到的规则(如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转换为数值)
Number('') -> 返回的是 0
相当于 0 == 0 自然就返回 true了
总结一下:
[] == ! [] -> [] == false -> [] == 0 -> '' == 0 -> 0 == 0 -> true
那么对于 {} == !{} 也是同理的
关键在于 {}.toString() -> NaN(返回的是NaN)
根据上面的规则(如果有一个操作数是NaN,则相等操作符返回 false)
总结一下:
{} == ! {} -> {} == false -> {} == 0 -> NaN == 0 -> false
关系类型:
console.dir(16>"5"); //true console.dir("16">"5");//false console.dir(5<"a");//false console.dir(5>=NaN);//false console.dir(5<NaN);//false console.dir(NaN>=NaN);//false console.dir(5>=null);//true console.dir(5>=undefined);//false console.dir(5>=5);//true console.dir(5>=true);//true console.dir(5>="true");//false console.dir(5>="");//true console.dir("Brick">"alphabet");//false B的字符串编码值是66 ,而a的字符串编码是97.因此false console.dir("brick">"alphabet");//true 小写字母b比a大,所以是true
3. 判断数据类型
typeof:用来判断各种数据类型。
typeof 2 //输出 number typeof null //输出 object typeof {} //输出 object typeof [] //输出 object typeof (function(){}) //输出 function typeof undefined //输出 undefined typeof '222' //输出 string typeof true //输出 boolean
instanceof:判断已知对象类型的方法.instanceof 后面一定要是对象类型,并且大小写不能错,该方法适合一些条件选择或分支。
var c= [1,2,3]; var d = new Date(); var e = function(){alert(111);}; var f = function(){this.name="22";}; console.log(c instanceof Array) //true console.log(d instanceof Date) //true console.log(e instanceof Function) //true // console.log(f instanceof function ) //false
instanceof是一个二元运算符,如:A instanceof B. 其中,A必须是一个合法的JavaScript对象,B必须是一个合法的JavaScript函数 (function)。如果函数B在对象A的原型链 (prototype chain) 中被发现,那么instanceof操作符将返回true,否则返回false.
console.log(Array instanceof Function);//true console.log(Object instanceof Function);//true
function Foo() { } var foo = new Foo(); alert(foo instanceof Foo);// true alert(foo instanceof Object);// true alert(foo instanceof Function);// false alert(Foo instanceof Function);// true alert(Foo instanceof Object);// true
为何Object instanceof Function和Function instanceof Object都返回true?
Object, Function, Array等等这些都被称作是构造“函数”,他们都是函数。而所有的函数都是构造函数Function的实例。从原型链机制的的角度来说,那就是说所有的函数都能通过原型链找到创建他们的Function构造函数的构造原型Function.protorype对象,所以:
与此同时,又因为Function.prototype是一个对象,所以他的构造函数是Object. 从原型链机制的的角度来说,那就是说所有的函数都能通过原型链找到创建他们的Object构造函数的构造原型Object.prototype对象,所以:
有趣的是根据我们通过原型链机制对instanceof进行的分析,我们不难得出一个结论:Function instanceof Function 依然返回true, 原理是一样的
1. Function是构造函数,所以它是函数对象
2. 函数对象都是由Function构造函数创建而来的,原型链机制解释为:函数对象的原型链中存在Function.prototype
3. instanceof查找原型链中的每一个节点,如果Function.prototype的构造函数Function的原型链中被查到,返回true
因此下面代码依然返回true
instanceof部分摘自:https://www.cnblogs.com/objectorl/archive/2010/01/11/Object-instancof-Function-clarification.html
constructor:据对象的constructor判断,返回对创建此对象的数组函数的引用。
var c= [1,2,3]; var d = new Date(); var e = function(){alert(111);}; alert(c.constructor === Array) //----------> true alert(d.constructor === Date) //-----------> true alert(e.constructor === Function) //-------> true //注意: constructor 在类继承时会出错
prototype:所有数据类型均可判断:Object.prototype.toString.call,这是对象的一个原生原型扩展函数,用来更精确的区分数据类型。
var gettype=Object.prototype.toString gettype.call('aaaa') //输出 [object String] gettype.call(2222) //输出 [object Number] gettype.call(true) //输出 [object Boolean] gettype.call(undefined) //输出 [object Undefined] gettype.call(null) //输出 [object Null] gettype.call({}) //输出 [object Object] gettype.call([]) //输出 [object Array] gettype.call(function(){}) //输出 [object Function]
六、作用域链、闭包、作用域
1. 作用域/链
变量作用域:一个变量可以使用的范围
JS中首先有一个最外层的作用域:称之为全局作用域
JS中还可以通过函数创建出一个独立的作用域,其中函数可以嵌套,所以作用域也可以嵌套
词法作用域就是在你写代码时将变量和块作用域写在哪里来决定,也就是词法作用域是静态的作用域,在你书写代码时就确定了。
作用域链是由当前作用域与上层一系列父级作用域组成,作用域的头部永远是当前作用域,尾部永远是全局作用域。作用域链保证了当前上下文对其有权访问的变量的有序访问。
作用域链的意义:查找变量(确定变量来自于哪里,变量是否可以访问)
引擎会在解释javascript代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来,引擎查询共分为两种:LHS查询和RHS查询
从字面意思去理解,当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询,更准确地讲,RHS查询与简单地查找某个变量的值没什么区别,而LHS查询则是试图找到变量的容器本身,从而可以对其赋值。
function foo(a){ console.log(a);//2 } foo( 2 );
这段代码中,总共包括4个查询,分别是:
1、foo(...)对foo进行了RHS引用
2、函数传参a = 2对a进行了LHS引用
3、console.log(...)对console对象进行了RHS引用,并检查其是否有一个log的方法
4、console.log(a)对a进行了RHS引用,并把得到的值传给了console.log(...)
在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。
RHS查询
【1】如果RHS查询失败,引擎会抛出ReferenceError(引用错误)异常
//对b进行RHS查询时,无法找到该变量。也就是说,这是一个“未声明”的变量 function foo(a){ a = b; } foo();//ReferenceError: b is not defined
【2】如果RHS查询找到了一个变量,但尝试对变量的值进行不合理操作,比如对一个非函数类型值进行函数调用,或者引用null或undefined中的属性,引擎会抛出另外一种类型异常:TypeError(类型错误)异常
function foo(){ var b = 0; b(); } foo();//TypeError: b is not a function
LHS查询
【1】当引擎执行LHS查询时,如果无法找到变量,全局作用域会创建一个具有该名称的变量,并将其返还给引擎
function foo(){ a = 1; } foo(); console.log(a);//1
【2】如果在严格模式中LHS查询失败时,并不会创建并返回一个全局变量,引擎会抛出同RHS查询失败时类似的ReferenceError异常
function foo(){ 'use strict'; a = 1; } foo(); console.log(a);//ReferenceError: a is not defined
原理解析:
function foo(a){ console.log(a); } foo(2);
【1】引擎需要为foo(...)函数进行RHS引用,在全局作用域中查找foo。成功找到并执行
【2】引擎需要进行foo函数的传参a=2,为a进行LHS引用,在foo函数作用域中查找a。成功找到,并把2赋值给a
【3】引擎需要执行console.log(...),为console对象进行RHS引用,在foo函数作用域中查找console对象。由于console是个内置对象,被成功找到
【4】引擎在console对象中查找log(...)方法,成功找到
【5】引擎需要执行console.log(a),对a进行RHS引用,在foo函数作用域中查找a,成功找到并执行
【6】于是,引擎把a的值,也就是2传到console.log(...)中
【7】最终,控制台输出2
2. 闭包
function fn1() { var name = 'iceman'; function fn2() { console.log(name); } return fn2; } var fn3 = fn1(); fn3();
-
fn2
的词法作用域能访问fn1
的作用域 -
将
fn2
当做一个值返回 -
fn1
执行后,将fn2
的引用赋值给fn3
-
执行
fn3
,输出了变量name
正常来说,当fn1
函数执行完毕之后,其作用域是会被销毁的,然后垃圾回收器会释放那段内存空间。而闭包却很神奇的将fn1
的作用域存活了下来,fn2
依然持有该作用域的引用,这个引用就是闭包。
闭包形成的条件
- 函数嵌套
- 内部函数引用外部函数的局部变量
闭包的内存泄漏
栈内存提供一个执行环境,即作用域,包括全局作用域和私有作用域,那他们什么时候释放内存的?
- 全局作用域----只有当页面关闭的时候全局作用域才会销毁
- 私有的作用域----只有函数执行才会产生
一般情况下,函数执行会形成一个新的私有的作用域,当私有作用域中的代码执行完成后,我们当前作用域都会主动的进行释放和销毁。但当遇到函数执行返回了一个引用数据类型的值,并且在函数的外面被一个其他的东西给接收了,这种情况下一般形成的私有作用域都不会销毁。
//经典面试题 function outer(){ var num=0;//内部变量 return function add(){//通过return返回add函数,就可以在outer函数外访问了 num++;//内部函数有引用,作为add函数的一部分了 console.log(num); }; } var func1=outer(); func1();//实际上是调用add函数, 输出1 func1();//输出2 因为outer函数内部的私有作用域会一直被占用 var func2=outer(); func2();// 输出1 每次重新引用函数的时候,闭包是全新的。 func2();// 输出2
闭包的作用
- 可以读取函数内部的变量。
- 可以使变量的值长期保存在内存中,生命周期比较长。因此不能滥用闭包,否则会造成网页的性能问题
- 可以用来实现JS模块。
JS模块:具有特定功能的js文件,将所有的数据和功能都封装在一个函数内部(私有的),只向外暴露一个包信n个方法的对象或函数,模块的使用者,只需要通过模块暴露的对象调用方法来实现对应的功能。
闭包的运用
应用闭包的主要场合是:设计私有的方法和变量。
补充:块级作用域
通常是因为只想在for循环内部的上下文中使用变量i,但实际上i可以在全局作用域中访问,污染了整个作用域:
for (var i= 0; i<10; i++) { console.log(i); } console.log(i);//10
//立即执行匿名函数(IIFE) (function(){ var i = 1; })(); console.log(i);//ReferenceError: i is not defined //for循环的代码中变量i用let声明,将会避免作用域污染问题 for (let i= 0; i<10; i++) { console.log(i); } console.log(i);////ReferenceError: i is not defined
下面代码中,由于闭包只能取得包含函数中的任何变量的最后一个值,所以控制台输出5,而不是0
var a = []; for(var i = 0; i < 5; i++){ a[i] = function(){ return i; } } console.log(a[0]());//5 //可以通过函数传参,来保存每次循环的值 var a = []; for(var i = 0; i < 5; i++){ a[i] = (function(j){ return function(){ return j; } })(i); } console.log(a[0]());//0 //而使用let则更方便,由于let循环有一个重新赋值的过程,相当于保存了每一次循环时的值 var a = []; for(let i = 0; i < 5; i++){ a[i] = function(){ return i; } } console.log(a[0]());//0
补充:变量/函数提升
var a = 2 ; 这个代码片段实际上包括两个操作: var a 和 a = 2 ,第一个定义声明是在编译阶段由编译器进行的。第二个赋值操作会被留在原地等待引擎在执行阶段执行。
console.log(a); var a = 0; function fn(){ console.log(b); var b = 1; function test(){ console.log(c); var c = 2; } test(); } fn();
//变量声明提升后,变成下面这样 var a ; console.log(a); a = 0; function fn(){ var b; console.log(b); b = 1; function test(){ var c ; console.log(c); c = 2; } test(); } fn();
函数声明会提升,但函数表达式却不会提升
foo(); function foo(){ console.log(1);//1 } //提升后 function foo(){ console.log(1); } foo(); //函数表达式不会提升 foo(); var foo = function(){ console.log(1);//TypeError: foo is not a function } //变量提升后,代码如下所示,依然会报错: var foo; foo(); foo = function(){ console.log(1); }
函数声明和变量声明都会被提升。但是,函数声明会覆盖变量声明
var a; function a(){} console.log(a);//'function a(){}'
但是,如果变量存在赋值操作,则最终的值为变量的值
var a=1; function a(){} console.log(a);//1 var a; function a(){}; console.log(a);//'function a(){}' a = 1; console.log(a);//1
注意:变量的重复声明是无用的,但函数的重复声明会覆盖前面的声明(无论是变量还是函数声明)
【1】变量的重复声明无用
var a = 1; var a; console.log(a);//1
【2】由于函数声明提升优先于变量声明提升,所以变量的声明无作用
var a; function a(){ console.log(1); } a();//1
【3】后面的函数声明会覆盖前面的函数声明
a();//2 function a(){ console.log(1); } function a(){ console.log(2); }
本部分参考博主链接:https://www.cnblogs.com/xiaohuochai/p/5699739.html
七、Ajax的原生写法
1. Ajax介绍
- 全称Asynchronous JavaScript and XML;
- 异步的 JavaScript 和 XML;
- 可以在不重新加载整个页面的情况下,与服务器交换数据并更新部分网页内容;
- 能够实现局部刷新,大大降低了资源的浪费;
- 不需要任何浏览器插件,但需要用户允许JavaScript在浏览器上执行;
- 是一门用于创建快速动态网页的技术;
- 传统的网页(不使用 AJAX)如果需要更新内容,必须重载整个网页;
2. XMLHttpRequest 对象的三个常用的属性
- onreadystatechange 属性存有处理服务器响应的函数;
- readyState 属性存有服务器响应的状态信息。每当 readyState 改变时,onreadystatechange 函数就会被执行;
- 可以通过 responseText 属性来取回由服务器返回的数据。
状态 | 描述 |
---|---|
0 | 请求未初始化(在调用 open() 之前) |
1 | 请求已提出(调用 send() 之前) |
2 | 请求已发送(这里通常可以从响应得到内容头部) |
3 | 请求处理中(响应中通常有部分数据可用,但是服务器还没有完成响应) |
4 | 请求已完成(可以访问服务器响应并使用它) |
3. xmlhttprequst的方法
- open() 有三个参数。第一个参数定义发送请求所使用的方法,第二个参数规定服务器端脚本的URL,第三个参数规定应当对请求进行异步地处理。 xmlHttp.open("GET","test.php",true);
- send() 方法将请求送往服务器。如果我们假设 HTML 文件和 PHP 文件位于相同的目录,那么代码是这样的: xmlHttp.send(null);
4. 实现Ajax
- 创建XMLHttpRequest对象。
- 设置请求方式。
- 调用回调函数。
- 发送请求。
var Ajax = { get: function(url, fn) { //创建XMLHttpRequest对象 var xhr = new XMLHttpRequest(); //true表示异步 xhr.open('GET', url, true); xhr.onreadystatechange = function() { // readyState == 4说明请求已完成 if(xhr.readyState == 4 && xhr.status == 200 || xhr.status == 304) { //responseText:从服务器获得数据 fn.call(this, xhr.responseText); } }; xhr.send(); }, post: function(url, data, fn) { //datat应为'a=a1&b=b1'这种字符串格式 var xhr = new XMLHttpRequest(); xhr.open("POST", url, true); // 添加http头,发送信息至服务器时内容编码类型 xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.onreadystatechange = function() { if(xhr.readyState == 4 && (xhr.status == 200 || xhr.status == 304)) { fn.call(this, xhr.responseText); } }; xhr.send(data); }
}
八、对象深拷贝、浅拷贝
浅拷贝就是把属于源对象的值都复制一遍到新的对象,不会开辟两者独立的内存区域;深度拷贝则是完完全全两个独立的内存区域,互不干扰
//js的深拷贝
function deepCopy(obj){
//判断是否是简单数据类型,
if(typeof obj == "object"){
//复杂数据类型
var result = obj.constructor == Array ? [] : {};
for(let i in obj){
result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
}
}else {
//简单数据类型 直接 == 赋值
var result = obj;
}
return result;
}
/** * deep clone * @param {[type]} parent object 需要进行克隆的对象 * @return {[type]} 深克隆后的对象 */ const clone = parent => { // 维护两个储存循环引用的数组 const parents = []; const children = []; const _clone = parent => { if (parent === null) return null; if (typeof parent !== 'object') return parent; let child, proto; if (isType(parent, 'Array')) { // 对数组做特殊处理 child = []; } else if (isType(parent, 'RegExp')) { // 对正则对象做特殊处理 child = new RegExp(parent.source, getRegExp(parent)); if (parent.lastIndex) child.lastIndex = parent.lastIndex; } else if (isType(parent, 'Date')) { // 对Date对象做特殊处理 child = new Date(parent.getTime()); } else { // 处理对象原型 proto = Object.getPrototypeOf(parent); // 利用Object.create切断原型链 child = Object.create(proto); } // 处理循环引用 const index = parents.indexOf(parent); if (index != -1) { // 如果父数组存在本对象,说明之前已经被引用过,直接返回此对象 return children[index]; } parents.push(parent); children.push(child); for (let i in parent) { // 递归 child[i] = _clone(parent[i]); } return child; }; return _clone(parent); };
九、图片懒加载、预加载
懒加载即延迟,对于图片过多的页面,为了加快页面加载速度,我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样一来页面加载性能大幅提升,提高了用户体验。
<img src="https://i.loli.net/2017/08/08/5989307b6c87b.gif" data-xxx="${data.content[i].url}"> let images = document.querySelectorAll('img[data-xxx]') for(let i = 0; i <images.length; i++){ if(出现在屏幕里(images[i])){ images[i].src = images[i].getAttribute('data-xxx') images[i].removeAttribute('data-xxx') }
预加载:图片预加载就是在网页全部加载之前,提前加载图片。当用户需要查看时可直接从本地缓存中渲染,以提供给用户更好的体验,减少等待的时间。否则,如果一个页面的内容过于庞大,没有使用预加载技术的页面就会长时间的展现为一片空白,这样浏览者可能以为图片预览慢而没兴趣浏览,把网页关掉,这时,就需要图片预加载。当然这种做法实际上牺牲了服务器的性能换取了更好的用户体验。
实现预载的方法非常多,可以用CSS(background)、JS(Image)、HTML(<img />)都可以。常用的是new Image();,设置其src来实现预载,再使用onload方法回调预载完成事件。只要浏览器把图片下载到本地,同样的src就会使用缓存,这是最基本也是最实用的预载方法。当Image下载完图片头后,会得到宽和高,因此可以在预载前得到图片的大小(我所知的方法是用记时器轮循宽高变化)。一般实现预载的工具类,都实现一个Array来存需要预载的URL,然后实现Finish、Error、SizeChange等常用事件,可以由用户选择是顺序预载或假并发预载。Jquery的PreLoad可以用于预载。
JS获取宽高的方式
获取屏幕的高度和宽度(屏幕分辨率): window.screen.height/width
获取屏幕工作区域的高度和宽度(去掉状态栏): window.screen.availHeight/availWidth
网页全文的高度和宽度: document.body.scrollHeight/Width
滚动条卷上去的高度和向右卷的宽度: document.body.scrollTop/scrollLeft
网页可见区域的高度和宽度(不加边线): document.body.clientHeight/clientWidth
网页可见区域的高度和宽度(加边线): document.body.offsetHeight/offsetWidth
十、实现页面加载进度条
document.onreadystatechange页面加载状态改变时的事件;
document.readyState返回当前文档的状态(uninitialized--还未开始载入;loading--载入中;interactive--已加载,文档与用户可以开始交互;complete--载入完成)
<script type="text/javascript"> //页面加载状态改变时的事件 document.onreadystatechange = function () { if(document.readyState == 'complete'){ //判断页面加载完成,加载的图标就隐藏 $(".loading").fadeOut(); } } </script>
十一、this关键字
在Javascript中,当一个函数被调用时,会创建一个活动记录(也称为执行上下文)。它包含函数在哪里调用、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。this关键字是在运行时进行绑定的,与函数声明的位置没有任何关系,它指向什么完全取决于函数在哪里被调用。
1. this四大绑定规则
函数绑定(默认绑定)
当直接调用函数时就是函数绑定模式。
function fn() { console.log( this.a ); } var a = 2; fn(); // 2 -- fn单独调用,this引用window
注意:在非严格模式下,this将绑定到全局对象window。然而,在严格模式下,this将绑定到undefined。
隐式绑定(方法调用)
当函数作为一个对象的属性被调用的时候就属于隐式绑定模式,此时,this指向是调用这个函数的对象。
function test(){ alert(this.x); } var obj = {}; obj.x = 1; obj.m = test; obj.m(); // 1
注意:被隐式绑定的函数会丢失绑定对象,此时,将会应用默认绑定,从而把this绑定到全局对象或undefined上
显式绑定(硬绑定)
在Javascript中,通常使用call/apply/bind方法来进行显示绑定。
var x = 0; function test(){ alert(this.x);
}
var obj={}; obj.x = 1; obj.m = test; obj.m.apply(); //0 //apply()的参数为空时,默认调用全局对象。因此,这时的运行结果为0,证明this指的是全局对象。如果把最后一行代码修改为 obj.m.apply(o); //1
new绑定(构造器绑定)
通过new关键字调用的函数,属于new绑定模式。这时this关键字指向这个新创建的对象。
function test(){ this.x = 1; } var obj = new test(); alert(obj.x); // 1 //运行结果为1。为了表明这时this不是全局对象,我对代码做一些改变: var x = 2; function test(){ this.x = 1; } var obj = new test(); alert(x); //2
this关键字绑定规则的判定顺序
- 函数是否是new绑定?如果是,则this指向新创建的对象;
- 函数是否通过call/apply/bind显式绑定或硬绑定?如果是,则this指向指定的对象;
- 函数是否在某个上下文对象中隐式调用?如果是,this绑定的是那个上下文对象;
- 上述全不是,则使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局window对象。
2. this绑定指向改变
call:
function.call(obj,[param1[,param2[,…[,paramN]]]])
obj:将代替function类里的this对象
parms:这是一个参数列表
立即执行
apply:
function.apply(obj,args)
obj:将代替function类里this对象
args:数组,它将作为参数传给function
立即执行
bind:
function.bind(obj,arg1,arg2,...)
不会立即执行,而是返回一个新的函数
3. 被忽略的this
如果将null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
非常常见的做法是使用apply来“展开”一个数组,并当作参数传入一个函数如:求数组最大最小值,合并数组等,具体用法如下:
var min = Math.min.apply(null, arr); var max = Math.max.apply(null, arr); Array.prototype.push.apply(arrA, arrB);
箭头函数不使用this的四种绑定规则,而是根据外层(函数或者全局)作用域来决定this的指向。
箭头函数中的this只和定义它的作用域的this有关,而与在哪里以及如何调用它无关,同时它的this指向是不可以改变的。
本部分摘自:https://mp.weixin.qq.com/s/31HlZRug9RjcKBXCtfbBXA
十二、函数式编程
函数式编程的历史已经很悠久了,但是最近几年却频繁的出现在大众的视野,很多不支持函数式编程的语言也在积极加入闭包,匿名函数等非常典型的函数式编程特性。大量的前端框架也标榜自己使用了函数式编程的特性,好像一旦跟函数式编程沾边,就很高大上一样,而且还有一些专门针对函数式编程的框架和库,比如:RxJS、cycleJS、ramdaJS、lodashJS、underscoreJS等。函数式编程变得越来越流行,掌握这种编程范式对书写高质量和易于维护的代码都大有好处。
函数式编程常用核心概念
•纯函数
•函数的柯里化(柯里化是一种“预加载”函数的方法,通过传递较少的参数,得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种对参数的“缓存”,是一种非常高效的编写函数的方法。)
•函数组合
•Point Free
•声明式与命令式代码
//命令式 let CEOs = []; for (var i = 0; i < companies.length; i++) { CEOs.push(companies[i].CEO) } //声明式 let CEOs = companies.map(c => c.CEO);
简单来说,也就是当一个函数的输出不受外部环境影响,同时也不影响外部环境时,该函数就是纯函数,也就是它只关注逻辑运算和数学运算,同一个输入总得到同一个输出。
javascript内置函数有不少纯函数,也有不少非纯函数。
纯函数:
Array.prototype.slice
Array.prototype.map
String.prototype.toUpperCase
非纯函数:
Math.random
Date.now
Array.ptototype.splice
调用数组的slice方法每次返回的结果完全相同,同时数组不会被改变,而调用splice方法每次返回值都不一样,同时会改变原数组。
这就是我们强调使用纯函数的原因,因为纯函数相对于非纯函数来说,在可缓存性、可移植性、可测试性以及并行计算方面都有着巨大的优势。
把一个函数变纯的基本手段是不要依赖系统状态。
本部分参考链接:https://www.cnblogs.com/fengyuqing/p/functional_programming_1.html https://www.cnblogs.com/tjyoung/p/8976013.html
十三、手动实现parseInt
parseInt
是ECMAScript核心的一个全局函数,可以在实现了ECMAScript的宿主环境全局调用。
console.log(parseInt('12')); console.log(parseInt('08')); console.log(parseInt('0x16')); console.log(parseInt('-12')); console.log(parseInt(' -12')); console.log(parseInt(' - 12')); console.log(parseInt('124ref')); console.log(parseInt('ref'));
parseInt(string, [int radix])
第二个形参是可以忽略的,忽略时默认赋值为10也就是十进制。
radix就是指定第一个形参的进制类型,然后根据这个进制类型再转换为十进制整数
radix形参没指定的时候是10,有效范围:[2, 36]和特殊值0
1. 将第一个形参转换为字符串
2. 识别string转换是否有code unit,如果有 -, -标记为负数,0x或0X则把radix赋值为16
3. radix形参(int类型)是否存在,存在则重新赋值(会对实参进行Int32转化,无法转换成int类型则不会重新赋值radix)
4. radix为0,则设置radix为默认值10
5. 如果radix为1,或者大于等于37,parseInt直接返回NaN
6. 如果radix为[2, 36]时则代表,string参数分别是二进制,三进制(如果有得话~)…三十六进制类型
7. 然后对string进行的radix进制进行十进制转换,例如,按二进制对string来进行十进制转换
['1', '2', '3'].map(parseInt) //[1, NaN, NaN] //内部执行的剖析 (function (){ var ret = ['1', '2', '3'].map((value, index)=>{ console.log(value, index); return parseInt(value, index); }); console.log(ret); })(); //因此,实际上是 parseInt('1', 0); parseInt('2', 1); parseInt('3', 2);
parseInt('13', 2)
,这个结果是……1
,因为string参数如果最开始的code符合radix进制的话是可以进行解析转换的,正如这里’1’是符合二进制的,’3’是不符合二进制的,但1
处于优先位置,所以可以进行转换解析,而3
被无情地忽略~
function l(obj) { return console.log(obj) } function _parseInt(str,radix){ var res = 0; if(typeof str !="string" && typeof str !="number"){ return NaN; } str =String(str).trim().split(".")[0]; // l(str) let len = str.length; if(!len){ return NaN; } if(!radix){ return radix = 10; } if(typeof radix !=="number" || radix < 2 || radix >36){ return NaN; } for(let i = 0; i < len; i++){ let arr = str.split(""); l(arr instanceof Array) l(typeof arr) res += Math.floor(arr[i])*Math.pow(radix,i) } l(res); } _parseInt("654646",10)
十四、为什么会有同源策略
同源策略是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。只有同一个源的脚本赋予dom、读写cookie、session、ajax等操作的权限。url由协议、域名、端口和路径组成、如果两个url的协议、域名和端口相同,则这两个url是同源的。限制来源不用源的“document”,对当前的“document”读取或设置某些属性。在不受同源策略限制,带有“src”属性的标签加载是,实际上是由游览器发起一次GET请求,不同于XMLHTTPRequest,它们通过src属性加载的资源。但游览器限制了JavaScript的权限,使其不能读,写其中返回的内容。
如果没有同源策略,不同源的数据和资源(如HTTP头、Cookie、DOM、localStorage等)就能相互随意访问,根本没有隐私和安全可言。为了安全起见和资源的有效管理,浏览器当然要采用这种策略。
同源策略是一种约定,它是浏览器最核心和最基本的安全功能,可以用于隔离潜在恶意文件,如果没有了同源策略,浏览器的正常使用将受到影响。
浏览器采用同源策略,禁止页面加载或执行与自身不同源的任何脚本。如果没有同源策略,那么恶意网页可以读取银行网站、网上商城等里面的用户信息,甚至篡改账号密码等。所以所有支持JavaScript的浏览器都采用了同源策略。
十五、怎么判断两个对象是否相等
ES6有一个方法来判断两个对象是否相等 console.log(Object.is(a,b)) ,但是这个相等,和我们平时要的相等可能不一样,这个方法判断的是a和b是不是同一个指针的对象。
var a = { id:1 }; var b = a; console.log(Object.is(a,b)); //true //当我们只需要两个对象的内容相同的时候,他就没效果了 var a = { id:1 }; var b = { id:1 } console.log(Object.is(a,b)); //false
思路:只要两个对象的名和键值都相同。那么两个对象的内容就相同了(考虑如果键值也是对象的情况——用递归,递归的时候要判断prop是不是Object)
1.用Object.getOwnPropertyNames拿到对象的所以键名数组
2.比对键名数组的长度是否相等。否=>false。真=>3
3.比对键名对应的键值是否相等
function isObjectValueEqual(a, b) { var aProps = Object.getOwnPropertyNames(a); var bProps = Object.getOwnPropertyNames(b); if (aProps.length != bProps.length) { return false; } for (var i = 0; i < aProps.length; i++) { var propName = aProps[i] var propA = a[propName] var propB = b[propName] if (propA !== propB) { if ((typeof (propA) === 'object')) { if (this.isObjectValueEqual(propA, propB)) { return true } else { return false } } else { return false } } else { return false } } return true; } var a = { id:1, name:2, c:{ age:3 } }; var b = { id:1, name:2, c:{ age:3 } } console.log(isObjectValueEqual(a,b));//true
本部分摘自:https://www.jianshu.com/p/7407bd65b15d
十六、事件模型
1.原始事件模型(DOM0级)
这是一种被所有浏览器都支持的事件模型,对于原始事件而言,没有事件流,事件一旦发生将马上进行处理,有两种方式可以实现原始事件:
(1)在html代码中直接指定属性值:<button id="demo" type="button" onclick="doSomeTing()" />
(2)在js代码中为 document.getElementsById("demo").onclick = doSomeTing()
优点:所有浏览器都兼容
缺点:1)逻辑与显示没有分离;2)相同事件的监听函数只能绑定一个,后绑定的会覆盖掉前面的,如:a.onclick = func1; a.onclick = func2;将只会执行func2中的内容。3)无法通过事件的冒泡、委托等机制完成更多事情。
因为这些缺点,虽然原始事件类型兼容所有浏览器,但仍不推荐使用。
2.DOM2事件模型
此模型是W3C制定的标准模型,现代浏览器(IE6~8除外)都已经遵循这个规范。W3C制定的事件模型中,一次事件的发生包含三个过程:(1).事件捕获阶段,(2).事件目标阶段,(3).事件冒泡阶段
事件捕获:当某个元素触发某个事件(如onclick),顶层对象document就会发出一个事件流,随着DOM树的节点向目标元素节点流去,直到到达事件真正发生的目标元素。在这个过程中,事件相应的监听函数是不会被触发的。
事件目标:当到达目标元素之后,执行目标元素该事件相应的处理函数。如果没有绑定监听函数,那就不执行。
事件冒泡:从目标元素开始,往顶层元素传播。途中如果有节点绑定了相应的事件处理函数,这些函数都会被一次触发。
所有的事件类型都会经历事件捕获但是只有部分事件会经历事件冒泡阶段,例如submit事件就不会被冒泡。
事件的传播是可以阻止的:
• 在W3c中,使用stopPropagation()方法
• 在IE下设置cancelBubble = true;
在捕获的过程中stopPropagation();后,后面的冒泡过程就不会发生了。
标准的事件监听器该如何绑定:
addEventListener("eventType","handler","true|false");其中eventType指事件类型,注意不要加‘on’前缀,与IE下不同。第二个参数是处理函数,第三个即用来指定是否在捕获阶段进行处理,一般设为false来与IE保持一致(默认设置),除非你有特殊的逻辑需求。监听器的解除也类似:removeEventListner("eventType","handler","true!false");
3.IE事件模型
IE不把该对象传入事件处理函数,由于在任意时刻只会存在一个事件,所以IE把它作为全局对象window的一个属性,为求证其真伪,使用IE8执行代码alert(window.event),结果弹出是null,说明该属性已经定义,只是值为null(与undefined不同)。难道这个全局对象的属性是在监听函数里才加的?于是执行下面代码:
window.onload = function (){alert(window.event);}
setTimeout(function(){alert(window.event);},2000);
结果第一次弹出【object event】,两秒后弹出依然是null。由此可见IE是将event对象在处理函数中设为window的属性,一旦函数执行结束,便被置为null了。IE的事件模型只有两步,先执行元素的监听函数,然后事件沿着父节点一直冒泡到document。冒泡已经讲解过了,这里不重复。IE模型下的事件监听方式也挺独特,绑定监听函数的方法是:attachEvent( "eventType","handler"),其中evetType为事件的类型,如onclick,注意要加’on’。解除事件监听器的方法是 detachEvent("eventType","handler" )
IE的事件模型已经可以解决原始模型的三个缺点,但其自己的缺点就是兼容性,只有IE系列浏览器才可以这样写。
以上就是3种事件模型,在我们写代码的时候,为了兼容ie,通常使用以下写法:
var demo = document.getElementById('demo'); if(demo.attachEvent){ demo.attachEvent('onclick',func); }else{ demo.addEventListener('click',func,false); }
事件被封装成一个event对象,包含了该事件发生时的所有相关信息(event的属性)以及可以对事件进行的操作(event的方法)。
1. 事件定位相关属性
x/y与clientX/clientY值一样,表示距浏览器可视区域(工具栏除外区域)左/上的距离;
pageX/pageY,距页面左/上的距离,它与clientX/clientY的区别是不随滚动条的位置变化;
screenX/screenY,距计算机显示器左/上的距离,拖动你的浏览器窗口位置可以看到变化;
layerX/layerY与offsetX/offsetY值一样,表示距有定位属性的父元素左/上的距离。
2.其他常用属性
target:发生事件的节点;
currentTarget:当前正在处理的事件的节点,在事件捕获或冒泡阶段;
timeStamp:事件发生的时间,时间戳。
bubbles:事件是否冒泡。
cancelable:事件是否可以用preventDefault()方法来取消默认的动作;
keyCode:按下的键的值;
3. event对象的方法
event. preventDefault()//阻止元素默认的行为,如链接的跳转、表单的提交;
event. stopPropagation()//阻止事件冒泡
event.initEvent()//初始化新事件对象的属性,自定义事件会用,不常用
event. stopImmediatePropagation()//可以阻止掉同一事件的其他优先级较低的侦听器的处理(这货表示没用过,优先级就不说明了,谷歌或者问度娘吧。)
event.target与event.currentTarget他们有什么不同?
target在事件流的目标阶段;currentTarget在事件流的捕获,目标及冒泡阶段。只有当事件流处在目标阶段的时候,两个的指向才是一样的, 而当处于捕获和冒泡阶段的时候,target指向被单击的对象而currentTarget指向当前事件活动的对象(一般为父级)。
本部分摘自:https://www.cnblogs.com/hngdlxy143/p/9068282.html
十七、window的onload事件和DOMContentLoaded
1、当 onload
事件触发时,页面上所有的DOM,样式表,脚本,图片,flash都已经加载完成了。
2、当 DOMContentLoaded
事件触发时,仅当DOM加载完成,不包括样式表,图片,flash。
onload事件是DOM事件,onDOMContentLoaded是HTML5事件。
onload事件会被样式表、图像和子框架阻塞,而onDOMContentLoaded不会。
当加载的脚本内容并不包含立即执行DOM操作时,使用onDOMContentLoaded事件是个更好的选择,会比onload事件执行时间更早。
十八、for...in迭代和for...of有什么区别
1. for…in 语句以原始插入顺序迭代对象的可枚举属性。
2. for…of 语句遍历可迭代对象定义要迭代的数据。
Object.prototype.objCustom = function() {}; Array.prototype.arrCustom = function() {}; let iterable = [3, 5, 7]; iterable.foo = 'hello'; //for in 会继承 for (let i in iterable) { console.log(i); // logs 0, 1, 2, "foo", "arrCustom", "objCustom" } for (let i in iterable) { if (iterable.hasOwnProperty(i)) { console.log(i); // logs 0, 1, 2, "foo" } } // for of for (let i of iterable) { console.log(i); // logs 3, 5, 7 }
for...of循环是ES6引入的新的语法。
for...in遍历拿到的x是键(下标)。而for...of遍历拿到的x是值,但在对象中会提示不是一个迭代器报错。
let x; let a = ['A','B','C']; let b = {name: '刘德华',age: '18'}; console.log(a.length); for(x of a){ console.log(x); //A,B,C } for(x in a){ console.log(x+':'+a[x]); //0:A,1:B,2:C } /*for(x of b){ console.log(x); //报错 }*/ for(x in b){ console.log(x); //name,age } a.name = "Hello"; for(x in a){ console.log(x); //0,1,2,name } console.log(a.length); //3
for...in由于历史遗留问题,它遍历的实际上是对象的属性名称,一个Array数据也是一个对象,数组中的每个元素的索引被视为属性名称。
所以我们可以看到使用for...in循环Array数组时,拿到的其实是每个元素的索引。如下,把name包括在内,但是Array的length属性却不包括在内,for...of循环则完全修复了这些问题,它只循环集合本身的元素。
十九、函数柯里化
将一个低阶函数转换为高阶函数的过程就叫柯里化。比如对于加法操作: var add = (x, y) => x + y ,我们可以这样柯里化
function curryingAdd(x) { return function (y) { return x + y } }
只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数
// 经典面试题,实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
function add() { // 第一次执行时,定义一个数组专门用来存储所有的参数 var _args = Array.prototype.slice.call(arguments); // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值 var _adder = function() { _args.push(...arguments); return _adder; }; // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回 _adder.toString = function () { return _args.reduce(function (a, b) { return a + b; }); } return _adder; }
二十、call apply区别,原生实现bind
call,apply,bind 三者用法和区别:角度可为参数、绑定规则(显示绑定和强绑定),运行效率、运行情况。
//bind的实现就是柯里化 Function.prototype.bind = function (context) { var _this = this var args = Array.prototype.slice.call(arguments, 1) return function() { return _this.apply(context, args) } }
Function.prototype.myCall = function(context = window, ...rest) { context.fn = this; //此处this是指调用myCall的function let result = context.fn(...rest); //将this指向销毁 delete context.fn; return result; };
Function.prototype.myApply = function(context = window, params = []) { context.fn = this; //此处this是指调用myCall的function let result if (params.length) { result = context.fn(...params) }else { result = context.fn() } //将this指向销毁 delete context.fn; return result; };
Function.prototype.myBind = function(thisArg) { if (typeof this !== 'function') { return; } var _self = this; var args = Array.prototype.slice.call(arguments, 1) var fnBound = function () { // 检测 New // 如果当前函数的this指向的是构造函数中的this 则判定为new 操作 var _this = this instanceof _self ? this : thisArg; return _self.apply(_this, args.concat(Array.prototype.slice.call(arguments))); } // 为了完成 new操作 // 还需要做一件事情 执行原型 链接 (思考题,为什么? fnBound.prototype = this.prototype; return fnBound; }
二十一、async/await
async/await特点
-
async/await更加语义化,async 是“异步”的简写,async function 用于申明一个 function 是异步的; await,可以认为是async wait的简写, 用于等待一个异步方法执行完成;
-
async/await是一个用同步思维解决异步问题的方案(等结果出来之后,代码才会继续往下执行)
-
可以通过多层 async function 的同步写法代替传统的callback嵌套
async function语法
-
自动将常规函数转换成Promise,返回值也是一个Promise对象
-
只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数
-
异步函数内部可以使用await
await语法
-
await 放置在Promise调用之前,await 强制后面点代码等待,直到Promise对象resolve,得到resolve的值作为await表达式的运算结果
-
await只能在async函数内部使用,用在普通函数里就会报错
const timeoutFn = function(timeout){ return new Promise(function(resolve){ return setTimeout(resolve, timeout); }); } async function fn(){ await timeoutFn(1000); await timeoutFn(2000); return '完成'; } fn().then(success => console.log(success));
二十二、立即执行函数和使用场景
你的代码在页面加载完成之后,不得不执行一些设置工作,比如时间处理器,创建对象等等。所有的这些工作只需要执行一次,比如只需要显示一个时间。但是这些代码也需要一些临时的变量,但是初始化过程结束之后,就再也不会被用到,如果将这些变量作为全局变量,不是一个好的注意,我们可以用立即执行函数——去将我们所有的代码包裹在它的局部作用域中,不会让任何变量泄露成全局变量。
二十三、设计模式(要求说出如何实现,应用,优缺点)/单例模式实现
1. 单例模式
单例模式的定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。
适用场景:一个单一对象。比如:弹窗,无论点击多少次,弹窗只应该被创建一次。
// 单例模式 var Singleton = function(name){ this.name = name; }; Singleton.prototype.getName = function(){ return this.name; } // 获取实例对象,代理实现单例模式 var getInstance = (function() { var instance = null; return function(name) { if(!instance) { instance = new Singleton(name); } return instance; } })(); // 测试单例模式的实例 var a = getInstance("aa"); var b = getInstance("bb");
// 实现单例模式弹窗 var createWindow = (function(){ var div; return function(){ if(!div) { div = document.createElement("div"); div.innerHTML = "我是弹窗内容"; div.style.display = 'none'; document.body.appendChild(div); } return div; } })(); document.getElementById("Id").onclick = function(){ // 点击后先创建一个div元素 var win = createWindow(); win.style.display = "block"; }
2. 策略模式
策略模式的定义:定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换。
/*策略类*/ var levelOBJ = { "A": function(money) { return money * 4; }, "B" : function(money) { return money * 3; }, "C" : function(money) { return money * 2; } }; /*环境类*/ var calculateBouns =function(level,money) { return levelOBJ[level](money); }; console.log(calculateBouns('A',10000)); // 40000
3. 代理模式
代理模式的定义:为一个对象提供一个代用品或占位符,以便控制对它的访问。
常用的虚拟代理形式:某一个花销很大的操作,可以通过虚拟代理的方式延迟到这种需要它的时候才去创建(例:使用虚拟代理实现图片懒加载)
图片懒加载的方式:先通过一张loading图占位,然后通过异步的方式加载图片,等图片加载好了再把完成的图片加载到img标签里面。
var imgFunc = (function() { var imgNode = document.createElement('img'); document.body.appendChild(imgNode); return { setSrc: function(src) { imgNode.src = src; } } })(); var proxyImage = (function() { var img = new Image(); img.onload = function() { imgFunc.setSrc(this.src); } return { setSrc: function(src) { imgFunc.setSrc('./loading,gif'); img.src = src; } } })(); proxyImage.setSrc('./pic.png');
4. 装饰者模式
装饰者模式的定义:在不改变对象自身的基础上,在程序运行期间给对象动态地添加方法。
例如:现有4种型号的自行车分别被定义成一个单独的类,如果给每辆自行车都加上前灯、尾灯、铃铛这3个配件,如果用类继承的方式,需要创建4*3=12个子类。但如果通过装饰者模式,只需要创建3个类。
装饰者模式适用的场景:原有方法维持不变,在原有方法上再挂载其他方法来满足现有需求;函数的解耦,将函数拆分成多个可复用的函数,再将拆分出来的函数挂载到某个函数上,实现相同的效果但增强了复用性。
//用AOP装饰函数实现装饰者模式 Function.prototype.before = function(beforefn) { var self = this; //保存原函数引用 return function(){ //返回包含了原函数和新函数的 '代理函数' beforefn.apply(this, arguments); //执行新函数,修正this return self.apply(this,arguments); //执行原函数 } } Function.prototype.after = function(afterfn) { var self = this; return function(){ var ret = self.apply(this,arguments); afterfn.apply(this, arguments); return ret; } } var func = function() { console.log('2'); } //func1和func3为挂载函数 var func1 = function() { console.log('1'); } var func3 = function() { console.log('3'); } func = func.before(func1).after(func3); func();
本部分摘自:https://blog.csdn.net/song_mou_xia/article/details/80763833 https://juejin.im/post/59df4f74f265da430f311909
二十四、iframe的缺点有哪些
1.iframe能够原封不动的把嵌入的网页展现出来。
2.如果有多个网页引用iframe,那么你只需要修改iframe的内容,就可以实现调用的每一个页面内容的更改,方便快捷。
3.网页如果为了统一风格,头部和版本都是一样的,就可以写成一个页面,用iframe来嵌套,可以增加代码的可重用。
4.如果遇到加载缓慢的第三方内容如图标和广告,这些问题可以由iframe来解决。
iframe的缺点:
1.会产生很多页面,
不容易管理
。2.iframe框架结构有时会让人感到迷惑,如果框架个数多的话,可能会出现上下、左右滚动条,会分散访问者的注意力,
用户体验度差
。3.代码复杂,无法被一些搜索引擎索引到,这一点很关键,现在的搜索引擎爬虫还不能很好的处理iframe中的内容,所以使用iframe会
不利于搜索引擎优化
。4.很多的移动设备(PDA手机)无法完全显示框架,
设备兼容性
差。5.iframe框架页面会
增加服务器的http请求
,对于大型网站是不可取的。分析了这么多,
现在基本上都是用Ajax来代替iframe,所以iframe已经渐渐的退出了前端开发
。二十五、数组问题
1. 数组操作
shift:删除原数组第一项,并返回删除元素的值;如果数组为空则返回undefined
unshift:将参数添加到原数组开头,并返回数组的长度
pop:删除原数组最后一项,并返回删除元素的值;如果数组为空则返回undefined
push:将参数添加到原数组末尾,并返回数组的长度
concat:返回一个新数组,是将参数添加到原数组中构成的
splice(start,deleteCount,val1,val2,...):从start位置开始删除deleteCount项,并从该位置起插入val1,val2,..会改变原数组
reverse:将数组反序
sort(orderfunction):按指定的参数对数组进行排序
slice(start,end):返回从原数组中指定开始下标到结束下标之间的项组成的新数组
join(separator):将数组的元素组起一个字符串,以separator为分隔符,省略的话则用默认用逗号为分隔符
var a = [1,2,3,4,5]; var b = a.shift(); //a:[2,3,4,5] b:1 var a = [1,2,3,4,5]; var b = a.unshift(-2,-1); //a:[-2,-1,1,2,3,4,5] b:7 var a = [1,2,3,4,5]; var b = a.pop(); //a:[1,2,3,4] b:5 var a = [1,2,3,4,5]; var b = a.push(6,7); //a:[1,2,3,4,5,6,7] b:7 var a = [1,2,3,4,5]; var b = a.concat(6,7); //a:[1,2,3,4,5] b:[1,2,3,4,5,6,7] var a = [1,2,3,4,5]; var b = a.splice(2,2,7,8,9); //a:[1,2,7,8,9,5] b:[3,4] var b = a.splice(0,1); //同shift a.splice(0,0,-2,-1); var b = a.length; //同unshift var b = a.splice(a.length-1,1); //同pop a.splice(a.length,0,6,7); var b = a.length; //同push var a = [1,2,3,4,5]; var b = a.reverse(); //a:[5,4,3,2,1] b:[5,4,3,2,1] var a = [1,2,3,4,5]; var b = a.sort(); //a:[1,2,3,4,5] b:[1,2,3,4,5] var a = [1,2,3,4,5]; var b = a.slice(2,5); //a:[1,2,3,4,5] b:[3,4,5] var a = [1,2,3,4,5]; var b = a.join("|"); //a:[1,2,3,4,5] b:"1|2|3|4|5"
2. 数组遍历
for循环
使用临时变量,将长度缓存起来,避免重复获取数组长度,当数组较大时优化效果才会比较明显。
foreach循环
遍历数组中的每一项,没有返回值,对原数组没有影响,不支持IE
//1 没有返回值 arr.forEach((item,index,array)=>{ //执行代码 }) //参数:value数组中的当前项, index当前项的索引, array原始数组; //数组中有几项,那么传递进去的匿名回调函数就需要执行几次; //基本用法 var ary = ["JavaScript", "Java", "CoffeeScript", "TypeScript"]; ary.forEach(function(value, index, _ary) { console.log(index + ": " + value); return false; });
使用some或every可以中断循环
//使用some函数 var ary = ["JavaScript", "Java", "CoffeeScript", "TypeScript"]; ary.some(function (value, index, _ary) { console.log(index + ": " + value); return value === "CoffeeScript"; }); // logs: //0: JavaScript //1: Java //2: CoffeeScript //使用every var ary = ["JavaScript", "Java", "CoffeeScript", "TypeScript"]; ary.every(function(value, index, _ary) { console.log(index + ": " + value); return value.indexOf("Script") > -1; }); // logs: //0: JavaScript //1: Java
补充:如何中断foreach循环呢?
//循环外使用try.. catch,当需要中断时throw 一个异常,然后catch进行捕获; var BreakException = {}; try { [1, 2, 3].forEach(function(el) { console.log(el); if (el === 2) throw BreakException; }); } catch (e) { if (e !== BreakException) throw e; }
map循环
有返回值,可以return出来
map的回调函数中支持return返回值;return的是啥,相当于把数组中的这一项变为啥(并不影响原来的数组,只是相当于把原数组克隆一份,把克隆的这一份的数组中的对应项改变了);
var ary = [12,23,24,42,1]; var res = ary.map(function (item,index,ary ) { return item*10; }) console.log(res);//-->[120,230,240,420,10]; 原数组拷贝了一份,并进行了修改 console.log(ary);//-->[12,23,24,42,1]; 原数组并未发生变化
for of遍历
可以正确响应break、continue和return语句
for (var value of myArray) { console.log(value); }
//基本用法 let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; for (let el of arr) { console.log(el); if (el === 5) { break; } }
filter遍历
不会改变原始数组,返回新数组
var arr = [ { id: 1, text: 'aa', done: true }, { id: 2, text: 'bb', done: false } ] console.log(arr.filter(item => item.done)) //es5写法 arr.filter(function (item) { return item.done; }); var arr = [73,84,56, 22,100] var newArr = arr.filter(item => item>80) //得到新数组 [84, 100] console.log(newArr,arr)
reduce
reduce()
方法接收一个函数作为累加器(accumulator),数组中的每个值(从左到右)开始缩减,最终为一个值。
var total = [0,1,2,3,4].reduce((a, b)=>a + b); //10 //reduce接受一个函数,函数有四个参数,分别是:上一次的值,当前值,当前值的索引,数组 [0, 1, 2, 3, 4].reduce(function(previousValue, currentValue, index, array){ return previousValue + currentValue; }); //reduce还有第二个参数,我们可以把这个参数作为第一次调用callback时的第一个参数,上面这个例子因为没有第二个参数,所以直接从数组的第二项开始,如果我们给了第二个参数为5,那么结果就是这样的: [0, 1, 2, 3, 4].reduce(function(previousValue, currentValue, index, array){ return previousValue + currentValue; },5);
find
find()方法返回数组中符合测试函数条件的第一个元素。否则返回undefined
stu.find((element) => (element.name == '李四')) //es5 function getStu(element){ return element.name == '李四' } stu.find(getStu)
keys,values,entries
ES6 提供三个新的方法 —— entries(),keys()和values() —— 用于遍历数组。它们都返回一个遍历器对象,可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历
for (let index of ['a', 'b'].keys()) { console.log(index); } // 0 // 1 for (let elem of ['a', 'b'].values()) { console.log(elem); } // 'a' // 'b' for (let [index, elem] of ['a', 'b'].entries()) { console.log(index, elem); } // 0 "a" // 1 "b"
本部分参考链接:https://blog.csdn.net/deng1456694385/article/details/80295988 https://www.cnblogs.com/woshidouzia/p/9304603.html
对象遍历方法
for...in...
Object.keys(obj)
Object.values(obj)
Object.getOwnPropertyNames(obj)
const obj = { id:1, name:'zhangsan', age:18 } for(let key in obj){ console.log(key + '---' + obj[key]) } console.log(Object.keys(obj)) console.log(Object.values(obj)) Object.getOwnPropertyNames(obj).forEach(function(key){ console.log(key+ '---'+obj[key]) })
二十六、BOM属性对象方法
二十七、服务端渲染
二十八、垃圾回收机制
什么是垃圾:一般来说没有被引用的对象就是垃圾,就是要被清除, 有个例外如果几个对象引用形成一个环,互相引用,但根访问不到它们,这几个对象也是垃圾,也要被清除。
JavaScript 中的内存管理是自动执行的,而且是不可见的。我们创建基本类型、对象、函数……所有这些都需要内存。
JavaScript 中内存管理的主要概念是可达性。
简单地说,“可达性” 值就是那些以某种方式可访问或可用的值,它们被保证存储在内存中。
1. 有一组基本的固有可达值,由于显而易见的原因无法删除。例如:
- 本地函数的局部变量和参数
- 当前嵌套调用链上的其他函数的变量和参数
- 全局变量
- 还有一些其他的,内部的
这些值称为根。
2. 如果引用或引用链可以从根访问任何其他值,则认为该值是可访问的。
例如,如果局部变量中有对象,并且该对象具有引用另一个对象的属性,则该对象被视为可达性, 它引用的那些也是可以访问的,详细的例子如下。
JavaScript 引擎中有一个后台进程称为垃圾回收器,它监视所有对象,并删除那些不可访问的对象。
// user 具有对象的引用 let user = { name: "John" }; user = null; //此时,user 的值被覆盖,则引用丢失 //现在 John 变成不可达的状态,没有办法访问它,没有对它的引用。垃圾回收器将丢弃 John 数据并释放内存。
内存生命周期
JS环境中分配的内存一般有如下生命周期:
- 内存分配:当我们申明变量、函数、对象,并执行的时候,系统会自动为他们分配内存
- 内存使用:即读写内存,也就是使用变量、函数等
- 内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存
基本的垃圾回收算法称为“标记-清除”,定期执行以下“垃圾回收”步骤:
- 垃圾回收器获取根并“标记”(记住)它们。
- 然后它访问并“标记”所有来自它们的引用。
- 然后它访问标记的对象并标记它们的引用。所有被访问的对象都被记住,以便以后不再访问同一个对象两次。
- 以此类推,直到有未访问的引用(可以从根访问)为止。
- 除标记的对象外,所有对象都被删除。
一些优化:
- 分代回收——对象分为两组:“新对象”和“旧对象”。许多对象出现,完成它们的工作并迅速结 ,它们很快就会被清理干净。那些活得足够久的对象,会变“老”,并且很少接受检查。
- 增量回收——如果有很多对象,并且我们试图一次遍历并标记整个对象集,那么可能会花费一些时间,并在执行中会有一定的延迟。因此,引擎试图将垃圾回收分解为多个部分。然后,各个部分分别执行。这需要额外的标记来跟踪变化,这样有很多微小的延迟,而不是很大的延迟。
- 空闲时间收集——垃圾回收器只在 CPU 空闲时运行,以减少对执行的可能影响。
本部分摘自:https://segmentfault.com/a/1190000018605776?utm_source=tag-newest
二十九、eventloop
Javascript的事件分为同步任务和异步任务.
遇到同步任务就放在执行栈中执行.
遇到异步任务就放到任务队列之中,等到执行栈执行完毕之后再去执行任务队列之中的事件。
JS调用栈
Javascript 有一个 主线程(main thread)和 调用栈(call-stack),所有的代码都要通过函数,放到调用栈(也被称为执行栈)中的任务等待主线程执行。
JS调用栈采用的是后进先出的规则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。
WebAPIs
MDN的解释: Web 提供了各种各样的 API 来完成各种的任务。这些 API 可以用 JavaScript 来访问,令你可以做很多事儿,小到对任意 window 或者 element做小幅调整,大到使用诸如 WebGL 和 Web Audio 的 API 来生成复杂的图形和音效。
总结: 就是浏览器提供一些接口,让JavaScript可以调用,这样就可以把任务甩给浏览器了,这样就可以实现异步了!
任务队列(Task Queue)
"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,如果存在"定时器",主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
同步任务和异步任务
Javascript单线程任务被分为同步任务和异步任务.
- 同步任务会在调用栈 中按照顺序等待主线程依次执行.
- 异步任务会甩给在WebAPIs处理,处理完后有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。
宏任务(MacroTask)和 微任务(MicroTask):在JavaScript
中,任务被分为两种,一种宏任务(MacroTask
)也叫Task
,一种叫微任务(MicroTask
)。
宏任务(MacroTask):script(整体代码)
、setTimeout
、setInterval
、setImmediate
(浏览器暂时不支持,只有IE10支持,具体可见MDN
)、I/O
、UI Rendering
。
微任务(MicroTask):Process.nextTick(Node独有)
、Promise
、Object.observe(废弃)
、MutationObserver
(具体使用方式查看这里)
注意:只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。
console.log('script start') async function async1() { await async2() console.log('async1 end') } async function async2() { console.log('async2 end') } async1() setTimeout(function() { console.log('setTimeout') }, 0) new Promise(resolve => { console.log('Promise') resolve() }) .then(function() { console.log('promise1') }) .then(function() { console.log('promise2') }) console.log('script end') //script start //async2 end //Promise //script end //async1 end //promise1 //promise2 //undefined //setTimeout
- 首先,打印
script start
,调用async1()
时,返回一个Promise
,所以打印出来async2 end
。 - 每个
await
,会新产生一个promise
,但这个过程本身是异步的,所以该await
后面不会立即调用。 - 继续执行同步代码,打印
Promise
和script end
,将then
函数放入微任务队列中等待执行。 - 同步执行完成之后,检查微任务队列是否为
null
,然后按照先入先出规则,依次执行。 - 然后先执行打印
promise1
,此时then
的回调函数返回undefinde
,此时又有then
的链式调用,又放入微任务队列中,再次打印promise2
。 - 再回到
await
的位置执行返回的Promise
的resolve
函数,这又会把resolve
丢到微任务队列中,打印async1 end
。 - 当微任务队列为空时,执行宏任务,打印
setTimeout
。
浏览器执行过程
执行完主执行线程中的任务。
取出Microtask Queue中任务执行直到清空。
取出Macrotask Queue中一个任务执行。
取出Microtask Queue中任务执行直到清空。
重复3和4。
即为同步完成,一个宏任务,所有微任务,一个宏任务,所有微任务......
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end'); //结果 //script start //script end //promise1 //promise2 //setTimeout
解析:一开始task队列中只有script,则script中所有函数放入函数执行栈执行,代码按顺序执行。
接着遇到了setTimeout,它的作用是0ms后将回调函数放入task队列中,也就是说这个函数将在下一个事件循环中执行(注意这时候setTimeout执行完毕就返回了)。
接着遇到了Promise,按照前面所述Promise属于microtask,所以第一个.then()会放入microtask队列。
当所有script代码执行完毕后,此时函数执行栈为空。开始检查microtask队列,此时队列不为空,执行.then()的回调函数输出'promise1',由于.then()返回的依然是promise,所以第二个.then()会放入microtask队列继续执行,输出'promise2'。
此时microtask队列为空了,进入下一个事件循环,检查task队列发现了setTimeout的回调函数,立即执行回调函数输出'setTimeout',代码执行完毕。
以上便是浏览器事件循环的过程
Node.js执行过程
事件循环能让 Node.js 执行非阻塞 I/O 操作,尽管JavaScript事实上是单线程的,通过在可能的情况下把操作交给操作系统内核来实现。
由于大多数现代系统内核是多线程的,内核可以处理后台执行的多个操作。当其中一个操作完成的时候,内核告诉 Node.js,相应的回调就被添加到轮询队列(poll queue)并最终得到执行。
在node中事件每一轮循环按照顺序分为6个阶段,来自libuv的实现:
- timers 阶段: 这个阶段执行setTimeout(callback) and setInterval(callback)预定的callback;
- I/O callbacks 阶段: 是否有已完成的I/O操作的回调函数,来自上一轮的poll残留;
- idle, prepare 阶段: 仅node内部使用;
- poll 阶段: 获取新的I/O事件, 适当的条件下node将阻塞在这里;
- check 阶段: 执行setImmediate() 设定的callbacks;
- close callbacks 阶段: 比如socket.on(‘close’, callback)的callback会在这个阶段执行.
每一个阶段都有一个装有callbacks的fifo queue(队列),当event loop运行到一个指定阶段时,node将执行该阶段的fifo queue(队列),当队列callback执行完或者执行callbacks数量超过该阶段的上限时,event loop会转入下一下阶段.
上面六个阶段都不包括 process.nextTick(),process.nextTick不是基于libuv事件机制的,而timers一系列的api全部是基于libuv开放出来的api实现的。
定时器(timers)
定时器的用途是让指定的回调函数在某个阈值后会被执行,具体的执行时间并不一定是那个精确的阈值。定时器的回调会在制定的时间过后尽快得到执行,然而,操作系统的计划或者其他回调的执行可能会延迟该回调的执行。
轮询(poll)
轮询阶段有两个主要功能:
1,执行已经到时的定时器脚本
2,处理轮询队列中的事件
当事件循环进入到轮询阶段却没有发现定时器时:
如果轮询队列非空,事件循环会迭代回调队列并同步执行回调,直到队列空了或者达到了上限(前文说过的根据操作系统的不同而设定的上限)。
如果轮询队列是空的:
如果有setImmediate()定义了回调,那么事件循环会终止轮询阶段并进入检查阶段去执行定时器回调;
如果没有setImmediate(),事件回调会等待回调被加入队列并立即执行。
一旦轮询队列空了,事件循环会查找已经到时的定时器。如果找到了,事件循环就回到定时器阶段去执行回调。
I/O callbacks
这个阶段执行一些诸如TCP错误之类的系统操作的回调。例如,如果一个TCP socket 在尝试连接时收到了 ECONNREFUSED错误,某些 *nix 系统会等着报告这个错误。这个就会被排到本阶段的队列中。
检查(check)
这个阶段允许回调函数在轮询阶段完成后立即执行。如果轮询阶段空闲了,并且有回调已经被 setImmediate() 加入队列,事件循环会进入检查阶段而不是在轮询阶段等待。
setImmediate() 是个特殊的定时器,在事件循环中一个单独的阶段运行。它使用libuv的API 来使得回调函数在轮询阶段完成后执行。
关闭事件的回调(close callbacks)
如果一个 socket 或句柄(handle)被突然关闭(is closed abruptly),例如 socket.destroy(), 'close' 事件会被发出到这个阶段。否则这种事件会通过 process.nextTick() 被发出。
setTimeout VS setImmediate
二者非常相似,但是二者区别取决于他们什么时候被调用.
setImmediate 设计在poll阶段完成时执行,即check阶段;
setTimeout 设计在poll阶段为空闲时,且设定时间到达后执行;但其在timer阶段执行
其二者的调用顺序取决于当前event loop的上下文,如果他们在异步i/o callback之外调用,其执行先后顺序是不确定的
setTimeout(function timeout () { console.log('timeout'); },0); setImmediate(function immediate () { console.log('immediate'); }); $ node timeout_vs_immediate.js timeout immediate $ node timeout_vs_immediate.js immediate timeout
为什么结果不确定呢?
解释:setTimeout/setInterval 的第二个参数取值范围是:[1, 2^31 - 1],如果超过这个范围则会初始化为 1,即 setTimeout(fn, 0) === setTimeout(fn, 1)。我们知道 setTimeout 的回调函数在 timer 阶段执行,setImmediate 的回调函数在 check 阶段执行,event loop 的开始会先检查 timer 阶段,但是在开始之前到 timer 阶段会消耗一定时间,所以就会出现两种情况:
timer 前的准备时间超过 1ms,满足 loop->time >= 1,则执行 timer 阶段(setTimeout)的回调函数
timer 前的准备时间小于 1ms,则先执行 check 阶段(setImmediate)的回调函数,下一次 event loop 执行 timer 阶段(setTimeout)的回调函数
再看个例子:
setTimeout(() => { console.log('setTimeout') }, 0) setImmediate(() => { console.log('setImmediate') }) const start = Date.now() while (Date.now() - start < 10); //结果 //setTimeout //setImmediate
本部分摘自:https://juejin.im/post/5c72307551882562e74812dc 浅析Nodejs Event Loop
三十、如何快速让字符串变成以千为精度的数字
toLocalString()
三十一、正则表达式
\ 做为转意,即通常在"\"后面的字符不按原来意义解释,如/b/匹配字符"b",当b前面加了反斜杆后/\b/,转意为匹配一个单词的边界。
-或-
对正则表达式功能字符的还原,如"*"匹配它前面元字符0次或多次,/a*/将匹配a,aa,aaa,加了"\"后,/a\*/将只匹配"a*"。
^ 匹配一个输入或一行的开头,/^a/匹配"an A",而不匹配"An a"
$ 匹配一个输入或一行的结尾,/a$/匹配"An a",而不匹配"an A"
* 匹配前面元字符0次或多次,/ba*/将匹配b,ba,baa,baaa
+ 匹配前面元字符1次或多次,/ba*/将匹配ba,baa,baaa
? 匹配前面元字符0次或1次,/ba*/将匹配b,ba
(x) 匹配x保存x在名为$1...$9的变量中
x|y 匹配x或y
{n} 精确匹配n次
{n,} 匹配n次以上
{n,m} 匹配n-m次
[xyz] 字符集(character set),匹配这个集合中的任一一个字符(或元字符)
[^xyz] 不匹配这个集合中的任何一个字符
[\b] 匹配一个退格符
\b 匹配一个单词的边界
\B 匹配一个单词的非边界
\cX 这儿,X是一个控制符,/\cM/匹配Ctrl-M
\d 匹配一个字数字符,/\d/ = /[0-9]/
\D 匹配一个非字数字符,/\D/ = /[^0-9]/
\n 匹配一个换行符
\r 匹配一个回车符
\s 匹配一个空白字符,包括\n,\r,\f,\t,\v等
\S 匹配一个非空白字符,等于/[^\n\f\r\t\v]/
\t 匹配一个制表符
\v 匹配一个重直制表符
\w 匹配一个可以组成单词的字符(alphanumeric,这是我的意译,含数字),包括下划线,如[\w]匹配"$5.98"中的5,等于[a-zA-Z0-9]
\W 匹配一个不可以组成单词的字符,如[\W]匹配"$5.98"中的$,等于[^a-zA-Z0-9]。
var pattern = /s$/; //正则表达式直接量 var pattern = new RegExp("s$"); //构造函数RegExp()
匹配Email地址的正则表达式:w+([-+.]w+)*@w+([-.]w+)*.w+([-.]w+)*
评注:表单验证时很实用
匹配网址URL的正则表达式:[a-zA-z]+://[^s]*
/^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\*\+,;=.]+$/
评注:网上流传的版本功能很有限,上面这个基本可以满足需求
匹配帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$
评注:表单验证时很实用
匹配国内电话号码:d{3}-d{8}|d{4}-d{7}
评注:匹配形式如 0511-4405222 或 021-87888822
匹配腾讯QQ号:[1-9][0-9]{4,}
评注:腾讯QQ号从10000开始
匹配中国邮政编码:[1-9]d{5}(?!d)
评注:中国邮政编码为6位数字
匹配身份证:d{15}|d{18}
评注:中国的身份证为15位或18位
手机号:/^1[3|4|5|8][0-9]\d{4,8}$/
^1代表以1开头,现在中国的手机号没有是其它开头的,以后不一定啊
[3|4|5|8] 紧跟上面的1后面,可以是3或4或5或8的一个数字,如果以后出现190开始的手机号码了,就需要如下[3|4|5|8|9]
[0-9]表示0-9中间的任何数字,可以是0或9
\d{4,8} 这个\d跟[0-9]意思一样,都是0-9中间的数字。{4,8}表示匹配前面的最低4位数字最高8位数字。这里为什么不是直接的8呢,因为手机号码归属地查询的时候,根据前7位就可以知道具体的地址了,后面的4位没有影响的。
三十二、toString()、toLocaleString()、valueOf()
Array、Boolean、Date、Number等对象都具有toString()、toLocaleString()、valueOf()三个方法
1. JS Array
var array = new Array("niu","li","na"); console.log(array.valueOf());//Array【3】 console.log(array.toString());//niu,li,na console.log(array.toLocaleString());//niu,li,na
-
valueOf:返回数组本身
-
toString():把数组转换为字符串,并返回结果,每一项以逗号分割。
-
toLocalString():把数组转换为本地数组,并返回结果。
2. JS Boolean
var boolean = new Boolean(); console.log(boolean.valueOf());//false console.log(boolean.toString());//false
-
valueOf:返回 Boolean 对象的原始值。
-
toString():根据原始布尔值或者 booleanObject 对象的值返回字符串 "true" 或 "false"。默认为"false"。
-
toLocalString():Boolean对象没有toLocalString()方法。但是在Boolean对象上使用这个方法也不会报错。
3. JS Date
var date = new Date(); console.log(date.valueOf()); console.log(date.toString()); console.log(date.toLocaleString());
-
valueOf:返回 Date 对象的原始值,以毫秒表示。
-
toString():把 Date 对象转换为字符串,并返回结果。使用本地时间表示。
-
toLocalString():可根据本地时间把 Date 对象转换为字符串,并返回结果,返回的字符串根据本地规则格式化。
4. JS Math
console.log(Math.PI.valueOf());//3.141592653589793
- valueOf:返回 Math 对象的原始值。
5. JS Number
var num = new Number(1337); console.log(num.valueOf());//1337 console.log(num.toString());//1337 console.log(num.toLocaleString());//1,337
-
valueOf:返回一个 Number 对象的基本数字值。
-
toString():把数字转换为字符串,使用指定的基数。
-
toLocalString():把数字转换为字符串,使用本地数字格式顺序。
6. JS String
var string = new String("abc"); console.log(string.valueOf());//abc console.log(string.toString());//abc
-
valueOf:返回某个字符串对象的原始值。
-
toString():返回字符串。
toString()方法与toLocalString()方法区别:
-
toLocalString()是调用每个数组元素的 toLocaleString() 方法,然后使用地区特定的分隔符把生成的字符串连接起来,形成一个字符串。
-
toString()方法获取的是String(传统字符串),而toLocaleString()方法获取的是LocaleString(本地环境字符串)。
-
如果你开发的脚本在世界范围都有人使用,那么将对象转换成字符串时请使用toString()方法来完成。
-
LocaleString()会根据你机器的本地环境来返回字符串,它和toString()返回的值在不同的本地环境下使用的符号会有微妙的变化。
-
所以使用toString()是保险的,返回唯一值的方法,它不会因为本地环境的改变而发生变化。如果是为了返回时间类型的数据,推荐使用LocaleString()。若是在后台处理字符串,请务必使用toString()。
本部分摘自:https://www.cnblogs.com/niulina/p/5699031.html
未完待续·······