javaScript面试高频技术点(多为原生基础+框架集合)

tech2024-12-21  0

此文章为面试准备

1、数据类型的分类和判断

一:JS数据类型的分类

(1)基本数据类型(值类型)

String、Boolean、Number、Undefined、Null、Symbol(ES6引入)

(2)对象数据类型(引用类型)

Array、Function、Object

二:JS数据类型的判断

(1)typeof

用来判断基本数据类型,它的返回值是一个代表数据类型的字符串。用法:typeof 变量名 typeof的返回值有以下几种: (1)string (2)number (3)boolean (4)undefined (5)object (6)function

需要注意的是,用typeof判断不了Array,Null类型值的具体类型,因为返回值都是’obejct’。

let str = 'zyy' console.log(typeof str) //'string' let bool = false console.log(typeof bool) // 'boolean' let num = 999 console.log(typeof num) //'number' let age console.log(typeof age) //'undefined' let obj = null console.log(typeof obj) //'object' let obj1 = { name:'zyy' } console.log(typeof obj) //'object' let arr = [1,2] console.log(typeof arr) //'object' let func = function() { console.log('give me a big offer') } console.log(typeof func) //'function'

(2)instanceof

用来判断对象的具体类型,用法:a instanceof A,意思是判断a是否是A的实例,返回值是布尔值。

let arr = [1,2] console.log(arr instanceof Array) //true let obj = { name: 'zyy' } console.log(obj instanceof Object) //true let func = function() { console.log('give me a big offer') } console.log(func instanceof Function) //true let obj = null console.log(null instanceof Object)//false

需要注意的是,null instanceof Object结果为false。


2、原型、原型链、继承、构造函数、实例

转载,关于原型、原型链、继承、构造函数、实例的理解一

转载,关于原型、原型链、继承、构造函数、实例的理解二(推荐)


3、有几种方式可以实现继承,用原型实现继承有什么缺点,解决办法

前言:

JS作为面向对象的弱类型语言,继承也是其非常强大的特性之一。那么如何在JS中实现继承呢?让我们拭目以待。

JS继承的实现方式:

既然要实现继承,那么首先我们得有一个父类,代码如下:

// 定义一个动物类 function Animal (name) { // 属性 this.name = name || 'Animal'; // 实例方法 this.sleep = function(){ console.log(this.name + '正在睡觉!'); } } // 原型方法 Animal.prototype.eat = function(food) { console.log(this.name + '正在吃:' + food); };

(1)原型链继承

核心: 将父类的实例作为子类的原型

function Cat(){ } Cat.prototype = new Animal(); Cat.prototype.name = 'cat'; // Test Code var cat = new Cat(); console.log(cat.name); console.log(cat.eat('fish')); console.log(cat.sleep()); console.log(cat instanceof Animal); //true console.log(cat instanceof Cat); //true

特点:

非常纯粹的继承关系,实例是子类的实例,也是父类的实例 父类新增原型方法/原型属性,子类都能访问到 简单,易于实现

缺点: 1、要想为子类新增属性和方法,可以在Cat构造函数中,为Cat实例增加实例属性。如果要新增原型属性和方法,则必须放在new Animal()这样的语句之后执行。 2、无法实现多继承 3、来自原型对象的引用属性是所有实例共享的(详细请看附录代码: 示例1) 4、创建子类实例时,无法向父类构造函数传参

推荐指数:★★(3、4两大致命缺陷)

(2)构造继承

核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型) 示例1:

function Cat(name){ Animal.call(this); this.name = name || 'Tom'; } // Test Code var cat = new Cat(); console.log(cat.name); console.log(cat.sleep()); console.log(cat instanceof Animal); // false console.log(cat instanceof Cat); // true

示例2:

function Person(name,age) { this.name=name; this.age=age; } var Student=function(name,age,gender) { Person.call(this,name,age);//this继承了person的属性和方法 this.gender=gender; } var student=new Student("陈安东", 20, "男"); alert("姓名:"+student.name+"\n"+"年龄:"+student.age+"\n"+"性别:"+student.gender);

注:JS中call用法理解、call的其他理解、理解call的函数原理

特点:

1、解决了(1)中,子类实例共享父类引用属性的问题 2、创建子类实例时,可以向父类传递参数 3、可以实现多继承(call多个父类对象)

缺点: 1、实例并不是父类的实例,只是子类的实例 2、只能继承父类的实例属性和方法,不能继承原型属性/方法 3、无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

推荐指数:★★(缺点3)


(3)实例继承

核心:为父类实例添加新特性,作为子类实例返回

function Cat(name){ var instance = new Animal(); instance.name = name || 'Tom'; return instance; } // Test Code var cat = new Cat(); console.log(cat.name); console.log(cat.sleep()); console.log(cat instanceof Animal); // true console.log(cat instanceof Cat); // false

特点: 1、不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果

缺点: 1、实例是父类的实例,不是子类的实例 2、不支持多继承

推荐指数:★★


(4)拷贝继承

function Cat(name){ var animal = new Animal(); for(var p in animal){ Cat.prototype[p] = animal[p]; } Cat.prototype.name = name || 'Tom'; } // Test Code var cat = new Cat(); console.log(cat.name); console.log(cat.sleep()); console.log(cat instanceof Animal); // false console.log(cat instanceof Cat); // true

特点: 1、支持多继承

特点: 1、效率较低,内存占用高(因为要拷贝父类的属性) 2、无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)

推荐指数:★(缺点1)


(5)组合继承

核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

function Cat(name){ Animal.call(this); this.name = name || 'Tom'; } Cat.prototype = new Animal(); // 感谢 @学无止境c 的提醒,组合继承也是需要修复构造函数指向的。 Cat.prototype.constructor = Cat; // Test Code var cat = new Cat(); console.log(cat.name); console.log(cat.sleep()); console.log(cat instanceof Animal); // true console.log(cat instanceof Cat); // true

特点: 1、弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法 2、既是子类的实例,也是父类的实例 3、不存在引用属性共享问题 4、可传参 5、函数可复用

缺点: 1、调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

推荐指数:★★★★(仅仅多消耗了一点内存)


(6)寄生组合继承

前言: 理解寄生组合继承原理

核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

function Cat(name){ Animal.call(this); this.name = name || 'Tom'; } (function(){ // 创建一个没有实例方法的类 var Super = function(){}; Super.prototype = Animal.prototype; //将实例作为子类的原型 Cat.prototype = new Super(); })(); // Test Code var cat = new Cat(); console.log(cat.name); console.log(cat.sleep()); console.log(cat instanceof Animal); // true console.log(cat instanceof Cat); //true //感谢 @bluedrink 提醒,该实现没有修复constructor。 Cat.prototype.constructor = Cat; // 需要修复下构造函数

特点: 1、堪称完美

缺点: 1、实现较为复杂

推荐指数:★★★★(实现复杂,扣掉一颗星)

附录代码: 示例一:

function Animal (name) { // 属性 this.name = name || 'Animal'; // 实例方法 this.sleep = function(){ console.log(this.name + '正在睡觉!'); } //实例引用属性 this.features = []; } function Cat(name){ } Cat.prototype = new Animal(); var tom = new Cat('Tom'); var kissy = new Cat('Kissy'); console.log(tom.name); // "Animal" console.log(kissy.name); // "Animal" console.log(tom.features); // [] console.log(kissy.features); // [] tom.name = 'Tom-New Name'; tom.features.push('eat'); //针对父类实例值类型成员的更改,不影响 console.log(tom.name); // "Tom-New Name" console.log(kissy.name); // "Animal" //针对父类实例引用类型成员的更改,会通过影响其他子类实例 console.log(tom.features); // ['eat'] console.log(kissy.features); // ['eat'] 原因分析: 关键点:属性查找过程 执行tom.features.push,首先找tom对象的实例属性(找不到), 那么去原型对象中找,也就是Animal的实例。发现有,那么就直接在这个对象的 features属性中插入值。 在console.log(kissy.features); 的时候。同上,kissy实例上没有,那么去原型上找。 刚好原型上有,就直接返回,但是注意,这个原型对象中features属性值已经变化了。

4、作用域、作用域链、闭包

作用域

变量的作用域

先来谈谈变量的作用域 变量的作用域无非就是两种:全局变量和局部变量。

全局作用域: 最外层函数定义的变量拥有全局作用域,即对任何内部函数来说,都是可以访问的:

<script> var outerVar = "outer"; function fn(){ console.log(outerVar); } fn();//outer </script>

局部作用域: 和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,而对于函数外部是无法访问的,最常见的例如函数内部

<script> function fn(){ var innerVar = "inner"; } fn(); console.log(innerVar);// ReferenceError: innerVar is not defined </script>

需要注意的是,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!

<script> function fn(){ innerVar = "inner"; } fn(); console.log(innerVar);//inner </script>

注: 作用域中变量提前声明及预解析包括js执行顺序相对简单,这里不做过多赘述。有兴趣深入的同学可以移步该文章:JS-预解析(提升)与代码执行过程

作用域链(Scope Chain)

那什么是作用域链? 我的理解就是,根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问。 想要知道js怎么链式查找,就得先了解js的执行环境

执行环境(execution context)

每个函数运行时都会产生一个执行环境,而这个执行环境怎么表示呢?js为每一个执行环境关联了一个变量对象。环境中定义的所有变量和函数都保存在这个对象中。 全局执行环境是最外围的执行环境,全局执行环境被认为是window对象,因此所有的全局变量和函数都作为window对象的属性和方法创建的。 js的执行顺序是根据函数的调用来决定的,当一个函数被调用时,该函数环境的变量对象就被压入一个环境栈中。而在函数执行之后,栈将该函数的变量对象弹出,把控制权交给之前的执行环境变量对象。

举个例子:

<script> var scope = "global"; function fn1(){ return scope; } function fn2(){ return scope; } fn1(); fn2(); </script>

上面代码执行情况演示: 执行环境

了解了环境变量,再详细讲讲作用域链。 当某个函数第一次被调用时,就会创建一个执行环境(execution context)以及相应的作用域链,并把作用域链赋值给一个特殊的内部属性([scope])。然后使用this,arguments(arguments在全局环境中不存在)和其他命名参数的值来初始化函数的活动对象(activation object)。当前执行环境的变量对象始终在作用域链的第0位。 以上面的代码为例,当第一次调用fn1()时的作用域链如下图所示: (因为fn2()还没有被调用,所以没有fn2的执行环境)

作用域链 可以看到fn1活动对象里并没有scope变量,于是沿着作用域链(scope chain)向后寻找,结果在全局变量对象里找到了scope,所以就返回全局变量对象里的scope值。

注: 标识符解析是沿着作用域链一级一级地搜索标识符地过程。搜索过程始终从作用域链地前端开始,然后逐级向后回溯,直到找到标识符为止(如果找不到标识符,通常会导致错误发生)—-《JavaScript高级程序设计》

那作用域链地作用仅仅只是为了搜索标识符吗? 再来看一段代码:

<script> function outer(){ var scope = "outer"; function inner(){ return scope; } return inner; } var fn = outer(); fn(); </script>

outer()内部返回了一个inner函数,当调用outer时,inner函数的作用域链就已经被初始化了(复制父函数的作用域链,再在前端插入自己的活动对象),具体如下图:

一般来说,当某个环境中的所有代码执行完毕后,该环境被销毁(弹出环境栈),保存在其中的所有变量和函数也随之销毁(全局执行环境变量直到应用程序退出,如网页关闭才会被销毁) 但是像上面那种有内部函数的又有所不同,当outer()函数执行结束,执行环境被销毁,但是其关联的活动对象并没有随之销毁,而是一直存在于内存中,因为该活动对象被其内部函数的作用域链所引用。 具体如下图: outer执行结束,内部函数开始被调用 outer执行环境等待被回收,outer的作用域链对全局变量对象和outer的活动对象引用都断了

像上面这种内部函数的作用域链仍然保持着对父函数活动对象的引用,就是闭包(closure)


闭包

作用: 1、可以读取自身函数外部的变量(沿着作用域链寻找) 2、让这些外部变量始终保存在内存中 关于第二点,来看一下以下的代码:

<script> function outer(){ var result = new Array(); for(var i = 0; i < 2; i++){//注:i是outer()的局部变量 result[i] = function(){ return i; } } return result;//返回一个函数对象数组 //这个时候会初始化result.length个关于内部函数的作用域链 } var fn = outer(); console.log(fn[0]());// 2 console.log(fn[1]());// 2 </script>

返回结果很出乎意料吧,你肯定以为依次返回0,1,但事实并非如此 来看一下调用fn0的作用域链图: 可以看到result[0]函数的活动对象里并没有定义i这个变量,于是沿着作用域链去找i变量,结果在父函数outer的活动对象里找到变量i(值为2),而这个变量i是父函数执行结束后将最终值保存在内存里的结果。 由此也可以得出,js函数内的变量值不是在编译的时候就确定的,而是等在运行时期再去寻找的。

那怎么才能让result数组函数返回我们所期望的值呢? 看一下result的活动对象里有一个arguments,arguments对象是一个参数的集合,是用来保存对象的。 那么我们就可以把i当成参数传进去,这样一调用函数生成的活动对象内的arguments就有当前i的副本。 改进之后:

<script> function outer(){ var result = new Array(); for(var i = 0; i < 2; i++){ //定义一个带参函数 function arg(num){ return num; } //把i当成参数传进去 result[i] = arg(i); } return result; } var fn = outer(); console.log(fn[0]);//result:0 console.log(fn[1]);//result:1 </script>

基本闭包由此介绍已完,深入详细介绍移步:闭包


5、arguments 对象

一、在函数调用的时候,浏览器每次都会传递进两个隐式参数 1、函数的上下文对象this 2、封装实参的对象arguments

二、arguments 对象

arguments 对象实际上是所在函数的一个内置类数组对象每个函数都有一个arguments属性,表示函数的实参集合,这里的实参是重点,就是执行函数时实际传入的参数的集合。arguments不是数组而是一个对象,但它和数组很相似,所以通常称为类数组对象,以后看到类数组其实就表示arguments。arguments对象不能显式的创建,它只有在函数开始时才可用。arguments还有属性callee,length和迭代器Symbol。arguments同样具有length属性,arguments.length 为函数实参个数,可以用arguments[length]显示调用参数arguments对象可以检测参数个数,模拟函数重载

三、理解点

第一点:arguments对象:可以在函数内访问所有的参数,实参

function f1(){ console.log(arguments[0]); console.log(arguments[1]); console.log(arguments[2]); } f1(12,23,45); //12 34 45

第二点:在正常的模式下,arguments对象可以在运行的时候进行修改

function f2(a,b){ arguments[0] = 10; arguments[1] = 20; return a + b; } console.log(f2(4,6)); //30

第三点:在严格的模式下,arguments对象在运行的时候不可以修改,修改arguments对象不会影响到实际的函数参数

function f3(a,b){ 'use strict'; //开启严格模式 arguments[0] = 10; arguments[1] = 20; return a + b; } console.log(f3(3,6)); //9

第四点:通过arguments对象的length属性,可以判断实参的个数

function f4(){ console.log(arguments.length); } f4(2,3,4,5); //4 f4(1); //1 f4(); //0

第五点:arguments是一个对象,不是数组,转换为数组可以采用 slice 和 逐一填入新数组

var arr = Array.prototype.slice.call(arguments); console.log(typeof arr); var arr2 = []; for(var i = 0; i<arguments.length;i++){ arr2.push(arguments[i]); } console.log(typeof arr2);

第六点:arguments的callee属性可以返回对应的原函数,达到调用自身函数的效果,但是在严格模式中是不适用的

var f5 = function(){ console.log(arguments.callee===f5); //true console.log(arguments.callee===f6); //false } var f6; f5(); //返回原函数,调用自身

四、arguments的应用

第一点:arguments.callee为引用函数自身。我们可以借用arguments.length可以来查看实参和形参的个数是否一致 function add(a, b) { var realLen = arguments.length; console.log("realLen:", arguments.length); var len = add.length; console.log("len:", add.length); if (realLen == len) { console.log('实参和形参个数一致'); } else { console.log('实参和形参个数不一致'); } }; add(11,22,33,44,66); 第二点:我们可以借用arguments.callee来让匿名函数实现递归 var sum = function(n) { if(n == 1) { return 1; } else { return n + arguments.callee(n-1);   } } console.log("sum =", sum(5)); 第三点:编写一个函数求传入的若干个数字(不能用数组显示传入)的和 function add() { var len = arguments.length; var sum = 0; for (var i = 0; i < len; ++i) { sum += arguments[i]; } return sum; }; add(11,22,33,44,66);

6、Ajax的原生写法

原生ajax写法-原生ajax请求-原生ajax实例

理解:Browser和Server之间通信的一种方式,发送以及请求数据的一种方式

在AJAX出现之前: 用户每次与server进行一次交互都需要进入一个新的页面。例如用户点击下一页按钮,会直接跳转页面,用户明明只需要一部分的数据,确需要重新向server请求整个页面的数据,那么很多数据是重复的相同的,造成了不必要的带宽浪费,对server压力也大。

AJAX 出现之后: 用户点击下一页按钮,发起AJAX请求,只需要获取第二页的数据,然后修改页面局部的视图,OVER。其最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容。

AJAX优点:

1.无刷新更新数据。 无需重载整个页面,按需请求部分数据,节约带宽,减少服务器压力。异步与服务器通信。 不会打断用户操作,有些用户只需要首屏的搜索功能,就不必等到页面全部加载完成。 提升浏览器渲染体验,用户会在server响应数据之前看到整个页面的大概框架以及结构。 其实目的也是为了体验好!体验好!体验好!

AJAX缺点:

1.浏览器的收藏功能在某些情况使用不便(用户想收藏第二页数据时)。浏览器的后退功能在某些情况使用不便(用户退回第一页时)。2.AJAX的安全问题 AJAX相当于Browser和Server之间的一条通道,通过观察server的响应数据结构,在某些情况下回暴露出一些server的逻辑。黑客也可以模拟用户向Server发起请求,出现了诸如跨站点脚步攻击、SQL注入攻击等商业转载请联系作者获得授权,非商业转载请注明出处。

Ajax原生js实现

最简单的实现方式:

var xhr = new XMLHttpRequest(); xhr.open('请求方式GET或者POST或者其他', 请求地址url, 是否开启异步async); xhr.onreadystatechange = function() { // readyState == 4说明请求已完成 if (xhr.readyState == 4 && xhr.status == 200) { console.log(xhr.responseText); } } if (method == 'POST') { //给指定的HTTP请求头赋值 xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); } xhr.send()

目前已经有很多的工具(类库实现了AJAX的封装),只会用当然不行,我们需要详细的理解它的属性内容

XMLHttpRequest的属性

onreadystatechange:值为一个function,当readyState属性改变时会调用它readyState:Http请求过程中的状态值,具体情况如下 状态值描述0初始化状态。XMLHttpRequest 对象已创建或已被 abort() 方法重置。1open() 方法已调用,但是 send() 方法未调用。请求还没有被发送。2Send() 方法已调用,HTTP 请求已发送到 Web 服务器。未接收到响应。3所有响应头部都已经接收到。响应体开始接收但未完成。4HTTP 响应已经完全接收。

responseText:目前为止收到的响应体(不包括头部),或者如果还没有接收到数据的话,就是空字符串。 如果 readyState 小于 3,这个属性就是一个空字符串。当 readyState 为 3,这个属性返回目前已经接收的响应部分。如果 readyState 为 4,这个属性保存了完整的响应体。 如果响应包含了为响应体指定字符编码的头部,就使用该编码。否则,假定使用 Unicode UTF-8。

responseXML 对请求的响应,解析为 XML 并作为 Document 对象返回。

status 由服务器返回的 HTTP 状态代码,HTTP状态码整理。当 readyState 小于 3 的时候读取这一属性会导致一个异常。

statusText 这个属性用名称而不是数字指定了请求的 HTTP 的状态代码。也就是说,当状态为 200 的时候它是 “OK”,当状态为 404 的时候它是 “Not Found”。和 status 属性一样,当 readyState 小于 3 的时候读取这一属性会导致一个异常。


XMLHttpRequest的方法

open(method,url,async) 初始化一个请求。 注意: 在一个已经激活的request下(已经调用open()方法的request)再次调用这个方法相当于调用了abort()方法。 参数描述methodHTTP请求方式:“GET”, “POST”, “PUT”, "DELETE"等url请求路径async是否异步请求。值为布尔值,默认为true,如果值为false,则send()方法不会返回任何东西,直到接受到了服务器的返回数据。

abort() 取消当前响应,关闭连接并且结束任何未决的网络活动。 这个方法把 XMLHttpRequest 对象重置为 readyState 为 0 的状态,并且取消所有未决的网络活动。例如,如果请求用了太长时间,而且响应不再必要的时候,可以调用这个方法。

send(string) 发送 HTTP 请求。只有POST方式才传参,参数类型为字符串。GET方式参数跟在url上

setRequestHeader(header,value) 向一个打开但未发送的请求设置或添加一个 HTTP 请求(设置请求头)。 注意:POST请求一般情况下需要设置请求头

xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded') 参数描述header设置的请求头名称value对应的请求头的值 getAllResponseHeaders() 把 HTTP 响应头部作为未解析的字符串返回。 如果 readyState 小于 3,这个方法返回 null。否则,它返回服务器发送的所有 HTTP 响应的头部。头部作为单个的字符串返回,一行一个头部。每行用换行符 “\r\n” 隔开。getResponseHeader(name) 返回指定的 HTTP 响应头部的值。其参数是要返回的 HTTP 响应头部的名称。可以使用任何大小写来制定这个头部名字,和响应头部的比较是不区分大小写的。 该方法的返回值是指定的 HTTP 响应头部的值,如果没有接收到这个头部或者 readyState 小于 3 则为空字符串。如果接收到多个有指定名称的头部,这个头部的值被连接起来并返回,使用逗号和空格分隔开各个头部的值。

试着封装一下

以下是简化版,仅概述原理 使用闭包来防止变量污染

const $ = (function() { var name = 'jquery'; return { ajax: function({ type, url, data, isAsync, success }) { if (!url) { console.error('请输入请求地址') return; } var xhr = new XMLHttpRequest(); // 处理data对象 var query = [], queryData; for (var key in data) { // 默认encodeURIComponent一下 query.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key])); } queryData = query.join('&'); if (type == 'GET') { // get方式参数要跟在url上 url = url + '?' + queryData } // 默认使用GET,默认异步 xhr.open(type || 'GET', url, isAsync || true); xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) { // 有传入success回调就执行 success && success(xhr.responseText); } } if (type == 'POST') { //给指定的HTTP请求头赋值 xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); // 数组转成字符串 xhr.send(queryData) } else { xhr.send() } } } })(); //类似jquery的使用方式 $.ajax({ type: 'POST', url: 'https://web-api.juejin.im/gptzllpbev', data: { name: '嘻嘻' }, success: function(res) { console.log(res); } })

7、对象深拷贝、浅拷贝

前言:

基本类型值 var str = 'a'; var num = 1;

在JavaScript中基本数据类型有String,Number,Undefined,Null,Boolean,在ES6中,又定义了一种新的基本数据类型Symbol,所以一共有6种 基本类型是按值访问的,从一个变量复制基本类型的值到另一个变量后这2个变量的值是完全独立的,即使一个变量改变了也不会影响到第二个变量

var str1 = 'a'; var str2 = str1; str2 = 'b'; console.log(str2); //'b' console.log(str1); //'a' 引用类型值 引用类型值是引用类型的实例,它是保存在堆内存中的一个对象,引用类型是一种数据结构,最常用的是Object,Array,Function类型,另外还有Date,RegExp,Error等,ES6同样也提供了Set,Map2种新的数据结构

JavaScript是如何复制引用类型的

JavaScript对于基本类型和引用类型的赋值是不一样的

var obj1 = {a:1}; var ob2 = obj1; obj2.a = 2; console.log(obj1); //{a:2} console.log(obj2); //{a:2}

在这里只修改了obj1中的a属性,却同时改变了ob1和obj2中的a属性

当变量复制引用类型值的时候,同样和基本类型值一样会将变量的值复制到新变量上,不同的是对于变量的值,它是一个指针,指向存储在堆内存中的对象(JS规定放在堆内存中的对象无法直接访问,必须要访问这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值,所以引用类型的值是按引用访问)

变量的值也就是这个指针是存储在栈上的,当变量obj1复制变量的值给变量obj2时,obj1,obj2只是一个保存在栈中的指针,指向同一个存储在堆内存中的对象,所以当通过变量obj1操作堆内存的对象时,obj2也会一起改变

浅拷贝

对于浅拷贝的定义可以理解为 创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

以下是一些JavaScript提供的浅拷贝方法

Object.assign() ES6中拷贝对象的方法,接受的第一个参数是拷贝的目标,剩下的参数是拷贝的源对象(可以是多个)

语法:Object.assign(target, ...sources) var target = {}; var source = {a:1}; Object.assign(target ,source); console.log(target); //{a:1} source.a = 2; console.log(source); //{a:2} console.log(target); //{a:1}

Object.assign是一个浅拷贝,它只是在根属性(对象的第一层级)创建了一个新的对象,但是对于属性的值是仍是对象的话依然是浅拷贝,

Object.assign还有一些注意的点是:

不会拷贝对象继承的属性不可枚举的属性属性的数据属性/访问器属性可以拷贝Symbol类型

可以理解为Object.assign就是使用简单的=来赋值,遍历从右往左遍历源对象(sources)的所有属性用 = 赋值到目标对象(target)上

var obj1 = { a:{ b:1 }, sym:Symbol(1) }; Object.defineProperty(obj1,'innumerable',{ value:'不可枚举属性', enumerable:false }); var obj2 = {}; Object.assign(obj2,obj1) obj1.a.b = 2; console.log('obj1',obj1); console.log('obj2',obj2);

可以看到Symbol类型可以正确拷贝,但是不可枚举的属性被忽略了并且改变了obj1.a.b的值,obj2.a.b的值也会跟着改变,说明依旧存在访问的是堆内存中同一个对象的问题

扩展运算符

利用扩展运算符可以在构造字面量对象时,进行克隆或者属性拷贝

语法:var cloneObj = { ...obj }; 语法:var cloneObj = { ...obj }; var obj = {a:1,b:{c:1}} var obj2 = {...obj}; obj.a=2; console.log(obj); //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}} obj.b.c = 2; console.log(obj); //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}

扩展运算符 Object.assign()有同样的缺陷,对于值是对象的属性无法完全拷贝成2个不同对象,但是如果属性都是基本类型的值的话,使用扩展运算符更加方便

Array.prototype.slice() slice() 方法返回一个新的数组对象,这一对象是一个由 begin和 end(不包括end)决定的原数组的浅拷贝。原始数组不会被改变。

语法: arr.slice(begin, end);

在ES6以前,没有剩余运算符,Array.from的时候可以用 Array.prototype.slice将arguments类数组转为真正的数组,它返回一个浅拷贝后的的新数组

Array.prototype.slice.call({0: "aaa", length: 1}) //["aaa"] let arr = [1,2,3,4] console.log(arr.slice() === arr); //false

深拷贝

深拷贝则是对于复杂数据类型在堆内存中开辟了一块内存地址用于存放复制的对象并且把原有的对象复制过来,这2个对象是相互独立的,也就是2个不同的地址

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

一个简单的深拷贝

var obj1 = { a: { b: 1 }, c: 1 }; var obj2 = {}; obj2.a = {} obj2.c = obj1.c obj2.a.b = obj1.a.b; console.log(obj1); //{a:{b:1},c:1}; console.log(obj2); //{a:{b:1},c:1}; obj1.a.b = 2; console.log(obj1); //{a:{b:2},c:1}; console.log(obj2); //{a:{b:1},c:1};

JSON.stringify()

JSON.stringify()是目前前端开发过程中最常用的深拷贝方式,原理是把一个对象序列化成为一个JSON字符串,将对象的内容转换成字符串的形式再保存在磁盘上,再用JSON.parse()反序列化将JSON字符串变成一个新的对象

var obj1 = { a:1, b:[1,2,3] } var str = JSON.stringify(obj1) var obj2 = JSON.parse(str) console.log(obj2); //{a:1,b:[1,2,3]} obj1.a=2 obj1.b.push(4); console.log(obj1); //{a:2,b:[1,2,3,4]} console.log(obj2); //{a:1,b:[1,2,3]}

通过JSON.stringify实现深拷贝有几点要注意:

拷贝的对象的值中如果有函数,undefined,symbol则经过JSON.stringify()序列化后的JSON字符串中这个键值对会消失无法拷贝不可枚举的属性,无法拷贝对象的原型链拷贝Date引用类型会变成字符串拷贝RegExp引用类型会变成空对象

虽说通过JSON.stringify()方法深拷贝对象也有很多无法实现的功能,但是对于日常的开发需求(对象和数组),使用这种方法是最简单和快捷的

使用第三方库实现对象的深拷贝

1.lodash

2.jQuery

以上2个第三方的库都很好的封装的深拷贝的方法,有兴趣的同学可以去深入研究一下

自己封装一个深拷贝函数


8、图片懒加载和预加载

图片预加载: 就是在网页全部加载之前,提前加载图片,当用户需要查看时可直接从本地缓存中渲染,以提供给用户更好的体验,减少等待的时间。

图片懒加载(缓载):延迟加载图片或符合某些条件时才加载某些图片。

懒加载的实现:

HTML部分:

<div class="box"> <h1>js懒加载</h1> <div class="box"> <img src="" class="img" lazyload="true" data-original="http://pic.58pic.com/58pic/17/18/97/01U58PIC4Xr_1024.jpg"> <img src="" class="img" lazyload="true" data-original="http://pic.58pic.com/58pic/17/18/97/01U58PIC4Xr_1024.jpg"> <img src="" class="img" lazyload="true" data-original="http://pic.58pic.com/58pic/17/18/97/01U58PIC4Xr_1024.jpg"> <img src="" class="img" lazyload="true" data-original="http://pic.58pic.com/58pic/17/18/97/01U58PIC4Xr_1024.jpg"> <img src="" class="img" lazyload="true" data-original="http://pic.58pic.com/58pic/17/18/97/01U58PIC4Xr_1024.jpg"> <img src="" class="img" lazyload="true" data-original="http://pic.58pic.com/58pic/17/18/97/01U58PIC4Xr_1024.jpg"> <img src="" class="img" lazyload="true" data-original="http://pic.58pic.com/58pic/17/18/97/01U58PIC4Xr_1024.jpg"> <img src="" class="img" lazyload="true" data-original="http://pic.58pic.com/58pic/17/18/97/01U58PIC4Xr_1024.jpg"> <img src="" class="img" lazyload="true" data-original="http://pic.58pic.com/58pic/17/18/97/01U58PIC4Xr_1024.jpg"> </div> </div>

JS部分:

window.onload = function () { //获取当前浏览器的视口高度 var viewHeight = document.documentElement.clientHeight; //鼠标滚动回调 function lazyload() { var img = document.getElementsByClassName('img'); //获取所有图片集合 //遍历图片集合 for (let item of img) { //获取图片距视口顶部的距离 var imgHeight = item.getBoundingClientRect(); //判断当图片出现在视口160px时把地址放到src中,显示出图片 if (imgHeight.top < (viewHeight - 200)) { item.src = item.getAttribute("data-original") } } } lazyload(); //页面加载时把当前视口中的图片加载进来 document.addEventListener('scroll', lazyload); }

预加载:

简单理解:就是在使用该图片资源前,先加载到本地来,真正到使用时,直接从本地请求数据就行了。

var arr = [ '../picture/1.jpg', '../picture/2.jpg', '../picture/3.jpg', ]; var imgs =[] preLoadImg(arr); //图片预加载方法 function preLoadImg(pars){ for(let i=0;i<arr.length;i++){ imgs[i] = new Image(); imgs[i].src = arr[i]; } }

注意:关于new Image()应用

可以看到图片已经加载完毕了,到本地了

现在在页面放一个img标签

<body> <img src="" alt="" id="pic"/> //存放图片路径的地址 <script> var arr = [ '../picture/1.jpg', '../picture/2.jpg', '../picture/3.jpg', ]; var imgs =[] preLoadImg(arr); //图片预加载方法 function preLoadImg(pars){ for(let i=0;i<arr.length;i++){ imgs[i] = new Image(); imgs[i].src = arr[i]; } } // 5s之后显示出该图片 setInterval(function(){ var pic = document.querySelector('#pic'); pic.src = '../picture/1.jpg' },5000); </script>

5s之后并没有重复请求1.jpg这个图片,因为之前已经请求下来了,这就是预加载

懒加载与预加载区别及其他理解

懒加载和预加载都是相对用户需要看到图片的时间点而言。如果在用户需要看到图片之前就加载完图片,就称之为“预加载”;如果,在用户需要看到图片的时候才即时加载,那么我们就称之“懒加载”。预加载会导致服务器承受的并发量增加,也就是说会给服务器增加压力。而懒加载一般是发生在首屏加载渲染完成之后的页面浏览,所以从结果来看,是缓解了服务器压力。


9、实现页面加载进度条

建议直接使用进度条插件jQuery nprogress.js页面加载进度条

原生实现进度条建议集思广益,检索其他大佬博客(毕竟大佬做的更精致)


10、this关键字

本文主要解释在JS里面this关键字的指向问题(在浏览器环境下)。

首先,必须搞清楚在JS里面,函数的几种调用方式:

普通函数调用作为方法来调用作为构造函数来调用使用apply/call方法来调用Function.prototype.bind方法es6箭头函数

但是不管函数是按哪种方法来调用的,请记住一点:谁调用这个函数或方法,this关键字就指向谁。

详细可以查阅这篇this指向问题

定义: this关键字的定义 this是Javascript语言的一个关键字。

普通定义:当前发生事件的对象。

通俗定义:当前的方法属于谁。

全局作用域中的this

console.log(this); // Window全局对象

在浏览器中执行,将会得到一个全局的Window对象。

1、纯粹的函数直接调用

var x = 1; function test(){ this.x = 0; } test(); alert(x); //0

this作为全局对象Global调用,属于全局通用性

2、作为对象的方法调用

function test() { alert(this.x); } var o = {}; //声明一个对象o.x = 1; //给对象添加一个属性o.m = test; //给对象添加一个方法o.m(); // 1 调用方法,结果为1

当函数作为对象的方法被调用时,this指向当前调用该方法的对象。

3、作为构造函数调用

var x = 2; function test() { this.x = 1; } var o = new test(); alert(o.x); //1alert(x); //2

全局对象中的属性x并没有被改变,此时this指向该构造函数创建的对象。

4、apply、call、bind调用

var x = 0; function test() { alert(this.x); } var o = {}; //声明一个对象o.x = 1; //给对象添加一个属性o.m = test; //给对象添加一个方法o.m.apply(); //0 o.m.apply(o); //1 o.m.call(); //0 o.m.call(o); //1

call和apply都是Function对象的方法,都可以用来动态改变this的指向,达成函数复用的目的。

两个方法的第一个参数就是this,不传参数默认为全局对象,传入参数表示当前传入的对象。

注意:两种调用方式产生的结果完全相同。如果你已经有一个数组,使用apply方法,只有一个单独的变量,则用call方法。

嵌套函数作用域中的this

var a = 1; function test(){ console.log(this.a); // 2 var self = this; function test2(){ console.log(self.a); // 2 } test2();}var o = {}; //声明一个对象o.a = 2; //给对象添加一个属性o.m = test; //给对象添加一个方法o.m();

嵌套函数中,为了防止this作用域的混乱使用,通常自定义一个变量用于存储this,然后在嵌套函数内部使用这个变量,如代码中的self。

11、函数式编程

浅谈函数式编程及其理解

什么是函数式编程

我们常见的编程范式有两种:命令式和声明式,比如我们熟悉的面向对象思想就属于命令式,而函数式编程属于声明式。而且顺带说一句,函数式编程里面提到的“函数”不是我们理解的编程中的“function”概念,而是数学中的函数,即变量之间的映射。

12、手动实现parseInt

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)

13、为什么会有同源策略

同源策略是为了保护网站的安全,防止用户信息泄露,防止身份伪造等(读取Cookie)

具体可访问该博客:同源策略详解及跨域方式

14、怎么判断两个对象是否相等

两个Object类型对象,即使拥有相同属性、相同值,当使用 == 或 === 进行比较时,也不认为他们相等。这就是因为他们是通过引用(内存里的位置)比较的,不像基本类型是通过值比较的。

var obj1 = { name: "xiaoming", sex : "male" } var obj2 = { name: "xiaoming", sex : "male" } console.log(obj1 === obj2); // false 复制代码

但是如果浅拷贝指向同一内存的时候,此时两个对象相等。

var obj1 = { name: "xiaoming", sex : "male" }; var obj2 = { name: "xiaoming", sex : "male" }; var obj3 = obj1; console.log(obj1 === obj3); // true console.log(obj2 === obj3); // false 复制代码

正如你所看想的,检查对象的“值相等”我们基本上是要遍历的对象的每个属性,看看它们是否相等

/* * @param x {Object} 对象1 * @param y {Object} 对象2 * @return {Boolean} true 为相等,false 为不等 */ var deepEqual = function (x, y) { // 指向同一内存时 if (x === y) { return true; } else if ((typeof x == "object" && x != null) && (typeof y == "object" && y != null)) { if (Object.keys(x).length != Object.keys(y).length) return false; for (var prop in x) { if (y.hasOwnProperty(prop)) { if (! deepEqual(x[prop], y[prop])) return false; } else return false; } return true; } else return false; } 复制代码

虽然这个简单的实现适用于我们的例子中,有很多情况下,它是不能处理。例如:

如果该属性值之一本身就是一个对象吗? 如果属性值中的一个是NaN(在JavaScript中,是不是等于自己唯一的价值? 如果一个属性的值为undefined,而另一个对象没有这个属性(因而计算结果为不确定?)

检查对象的“值相等”的一个强大的方法,最好是依靠完善的测试库,涵盖了各种边界情况。Underscore和Lo-Dash有一个名为_.isEqual()方法,用来比较好的处理深度对象的比较。

最后附上Underscore里的_.isEqual()源码地址: github.com/hanzichi/un…

15、事件模型(事件委托、代理,如何让事件先冒泡后捕获)

1、事件捕获和事件冒泡与事件委托三者的关系

事件冒泡和事件捕获分别由网景公司和微软公司提出,这两个概念都是为了解决页面中事件流(事件发生顺序)的问题。事件捕获和冒泡是现在浏览器的执行事件的不同阶段,事件委托是利用冒泡阶段的运行机制来实现的

捕获、冒泡、事件委托关系图


事件冒泡和捕获的运行图


运行条件:当一个事件发生在具有父元素的元素上时,现代浏览器根据事件添加时的设置来执行(冒泡或者捕获) 通过addEventerListener()的第三个参数来设置事件是通过捕获阶段注册的(true),还是冒泡阶段注册的(false),默认情况下是false。

2、事件冒泡

从实际操作的元素(事件)向父元素一级一级执行下去,直到达到 有的时候父元素和子元素都定义了click事件,但是不希望点击子元素的时候执行父元素的click事件,可以通过阻止冒泡(stopPropagation())在子元素上阻止冒泡。

3、事件捕获

浏览器检查元素的最外层祖先,是否在捕获阶段注册了一个click事件处理程序,如果是,则运行它。 然后,它移动到的下一个元素(点击元素的父元素),并执行相同的操作,然后是下一个元素(点击的元 素的父元素),以此类推,直到达到实际点击的元素。

4、事件捕获和冒泡的区别

执行顺序不同 事件冒泡:事件会从最内层的元素开始发生,一直向上传播,直到document对象。 事件捕获:事件从最外层开始发生,直到最具体的元素。

5、事件委托使用场景

如果你想要在大量子元素中单击任何一个就可以执行一段代码,这个时候可以把事件监听器设置在父节点上。 当事件捕获和事件冒泡同时存在的情况下,事件又是如何触发的呢?

<!--部分HTML代码--> <div id="s1">father <div id='s2'>children</div> </div> <!--部分JS代码--> s1.addEventListener("click",function(e){ console.log('father的捕获事件') },true); s2.addEventListener("click",function(e){ console.log('children的捕获事件') },true); s1.addEventListener("click",function(e){ console.log('father的冒泡事件') },false); s2.addEventListener("click",function(e){ console.log('children的冒泡事件') },false);

打印结果:

father的捕获事件 children的捕获事件 children的冒泡事件 father的冒泡事件

结论: 对于非target节点,则先执行捕获再执行冒泡,对于target节点则先执行先注册的事件,无论冒泡还是捕获。 先执行非target节点的捕获,然后根据注册顺序执行target节点的事件,然后再执行非target节点的冒泡。

addEventerListener,IE8及以下不支持,属于DOM2级的方法,可添加多个方法不被覆盖 解绑事件,参数和绑定相同

removeEventListener(event.type, handle, boolean);

绑定事件兼容IE8及以下

attachEvent(event.type,handle);写事件名时要加上on前缀 ,IE特有,兼容IE8及以下,可添加多个事件处理程序,只支持冒泡阶段

由于事件捕获阶段没有可以阻止事件的函数,所以一般都是设置为事件冒泡

6、阻止冒泡

e.stopPropagation() stopPropagation是事件对象(Event)的一个方法,作用是阻止目标元素的冒泡事件,但是不会阻止默认行为。

兼容IE

e.cancelBubble = true

阻止冒泡兼容IE写法

window.event?window.event.cancelBubble=true:e.stopPropagation(); e && e.stopPropagation ? e.stopPropagation() : window.event.cancelBubble = true;

7、取消默认事件

e.preventDefault() preventDefault它也是事件对象的一个方法,作用是取消一个目标元素的默认行为,既然说是默认行为,当然只有它有默认行为才能被取消,如果元素本身无默认行为,调用当然无效啦,比如链接a,提交按钮input type="submit"等,当event对象的cancelable为false时,表示没有默认行为,这时即使有默认行为,调用preventDefault也是不会起作用的。 兼容IE: return false; js的return false只会阻止默认行为,而是用jquery的话则既可以阻止默认行为,有防止对象冒泡。

8、总结使用方法 :

停止冒泡

function stopBubble(e){ <!--如果提供了事件对象,则这是个非IE浏览器--> if(e&&e.stopPropagation){ e.stopPropagation(); }else{ <!--我们需要使用IE的方式来取消事件冒泡--> window.event.cancelBubble = true; } }

阻止默认行为

function stopDefault(e){ <!--阻止默认行为W3C--> if(e&&e.preventDefault()){ e.preventDefault(); }else{ <!--IE中阻止默认行为--> return false; } } }

9、事件注意点:

1、event代表事件的状态,例如触发event对象的元素,鼠标的位置及状态、按下的键等。 2、event对象只在事件发生的过程中才有效 firefox里的event和IE中的不同,IE里的是全局变量,随时可用,firefox里的要用参数引导才能用,是运行时的临时变量。 在IE/Opera中是window.event,在Firefox中是event;而事件的对象,在IE中是window.event.srcElement,在Firefox中是event.target,Opera中两者都可用。


16、window 的 onload 事件和 domcontentloaded

window.onload 和 DOMContentLoaded 的区别

在js中DOMContentLoaded方法是在HTML文档被完全的加载和解析之后才会触发的事件,他并不需要等到(样式表/图像/子框架)加载完成之后再进行。在看load事件(onload事件),用于检测一个加载完全的页面。

DOM完整的解析过程:

1、解析HTML结构。 2、加载外部脚本和样式表文件。 3、解析并执行脚本代码。//js之类的 4、DOM树构建完成。//DOMContentLoaded 5、加载图片等外部文件。 6、页面加载完毕。//load 7、在第4步的时候DOMContentLoaded事件会被触发。 8、在第6步的时候load事件会被触发。

触发 1、当 onload事件触发时,页面上所有的DOM,样式表,脚本,图片,flash都已经加载完成了。 2、当 DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片,flash。

示例: window.onload

<style> #bg { width: 100px; height: 100px; border: 2px solid; } </style> <script> document.getElementById("bg").style.background="yellow" </script> </head> <body> <div id="bg"></div> </body>

运行效果:

添加window.onload事件:

window.onload = function() { document.getElementById("bg").style.background="yellow" }

运行效果: 代码完成将div背景颜色设置为yellow,将设置背景颜色的代码放置在window.onload的事件处理函数中,当文档加载完成后,才会执行事件处理函数,保证文档已经加载完成。

DOMContentLoaded事件

示例:

<body> <p>测试</p> <script> console.log('观察脚本加载的顺序') window.addEventListener("load", function() { console.log('load事件回调') }, false); document.addEventListener("DOMContentLoaded", function() { console.log('DOMContentLoaded回调') // 不兼容老的浏览器,兼容写法见[jQuery中ready与load事件] ,原理看下文 }, false); </script> </body>

运行效果: 1

<script> document.addEventListener("DOMContentLoaded", function(event) { console.log('DOMContentLoaded回调') }); for(var i=0; i<1000000000; i++) {} //循环 1000000000 次,为了使这个同步脚本将延迟DOM的解析。 //所以DOMContentLoaded事件等待了一段时间(解析完所有js)才会被执行。 </script>

二、为什么要区分? 开发中我们经常需要给一些元素的事件绑定处理函数。但问题是,如果那个元素还没有加载到页面上,但是绑定事件已经执行完了,是没有效果的。这两个事件大致就是用来避免这样一种情况,将绑定的函数放在这两个事件的回调中,保证能在页面的某些元素加载完毕之后再绑定事件的函数。 当然DOMContentLoaded机制更加合理,因为我们可以容忍图片,flash延迟加载,却不可以容忍看见内容后页面不可交互。


17、for…in 迭代和 for…of 有什么区别

for...in循环的是key。for...of循环的是value

结论:

推荐在循环对象属性的时候,使用for…in,在遍历数组的时候的时候使用for…of。for…in循环出的是key,for…of循环出的是value注意,for…of是ES6新引入的特性。修复了ES5引入的for…in的不足for…of不能循环普通的对象,需要通过和Object.keys()搭配使用

示例: 假设我们要遍历一个数组的value let aArray = [‘a’,123,{a:‘1’,b:‘2’}]: 使用for…in循环:

for(let index in aArray){ console.log(`${aArray[index]}`); } // index为索引 // a // 123 // [object Object]

使用for…of循环:

for(var value of aArray){ console.log(value); } // a // 123 // {a: "1", b: "2"}

作用于数组的for-in循环除了遍历数组元素以外,还会遍历自定义属性

for…of循环不会循环对象的key,只会循环出数组的value,因此for…of不能循环遍历普通对象,对普通对象的属性遍历推荐使用for…in

如果实在想用for…of来遍历普通对象的属性的话,可以通过和Object.keys()搭配使用,先获取对象的所有key的数组 然后遍历:

var student={ name:'wujunchuan', age:22, locate:{ country:'china', city:'xiamen', school:'XMUT' } } for(var key of Object.keys(student)){ //使用Object.keys()方法获取对象key的数组 console.log(key+": "+student[key]); } // name: wujunchuan // 25 age: 22 // 25 locate: [object Object]

18、函数柯里化

什么是柯里化

维基百科上说道:柯里化,英语:Currying(果然是满满的英译中的既视感),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

看这个解释有一点抽象,我们就拿被做了无数次示例的add函数,来做一个简单的实现。

// 普通的add函数 function add(x, y) { return x + y } // Currying后 function curryingAdd(x) { return function (y) { return x + y } } add(1, 2) // 3 curryingAdd(1)(2) // 3

Currying有哪些好处呢?

参数复用 // 正常正则验证字符串 reg.test(txt) // 函数封装后 function check(reg, txt) { return reg.test(txt) } check(/\d+/g, 'test') //false check(/[a-z]+/g, 'test') //true // Currying后 function curryingCheck(reg) { return function(txt) { return reg.test(txt) } } var hasNumber = curryingCheck(/\d+/g) var hasLetter = curryingCheck(/[a-z]+/g) hasNumber('test1') // true hasNumber('testtest') // false hasLetter('21212') // false

上面的示例是一个正则的校验,正常来说直接调用check函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数reg进行复用,这样别的地方就能够直接调用hasNumber,hasLetter等函数,让参数能够复用,调用起来也更方便。

提前确认 var on = function(element, event, handler) { if (document.addEventListener) { if (element && event && handler) { element.addEventListener(event, handler, false); } } else { if (element && event && handler) { element.attachEvent('on' + event, handler); } } } var on = (function() { if (document.addEventListener) { return function(element, event, handler) { if (element && event && handler) { element.addEventListener(event, handler, false); } }; } else { return function(element, event, handler) { if (element && event && handler) { element.attachEvent('on' + event, handler); } }; } })(); //换一种写法可能比较好理解一点,上面就是把isSupport这个参数给先确定下来了 var on = function(isSupport, element, event, handler) { isSupport = isSupport || document.addEventListener; if (isSupport) { return element.addEventListener(event, handler, false); } else { return element.attachEvent('on' + event, handler); } }

我们在做项目的过程中,封装一些dom操作可以说再常见不过,上面第一种写法也是比较常见,但是我们看看第二种写法,它相对一第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了会走哪一个方法,避免每次都进行判断。

延迟运行 Function.prototype.bind = function (context) { var _this = this var args = Array.prototype.slice.call(arguments, 1) return function() { return _this.apply(context, args) } }

像我们js中经常使用的bind,实现的机制就是Currying. 说了这几点好处之后,发现还有个问题,难道每次使用Currying都要对底层函数去做修改,

有没有什么通用的封装方法?

// 初步封装 var currying = function(fn) { // args 获取第一个方法内的全部参数 var args = Array.prototype.slice.call(arguments, 1) return function() { // 将后面方法里的全部参数和args进行合并 var newArgs = args.concat(Array.prototype.slice.call(arguments)) // 把合并后的参数通过apply作为fn的参数并执行 return fn.apply(this, newArgs) } }

这边首先是初步封装,通过闭包把初步参数给保存下来,然后通过获取剩下的arguments进行拼接,最后执行需要currying的函数。

但是好像还有些什么缺陷,这样返回的话其实只能多扩展一个参数,currying(a)(b)©这样的话,貌似就不支持了(不支持多参数调用),一般这种情况都会想到使用递归再进行封装一层。

// 支持多参数传递 function progressCurrying(fn, args) { var _this = this var len = fn.length; var args = args || []; return function() { var _args = Array.prototype.slice.call(arguments); Array.prototype.push.apply(args, _args); // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数 if (_args.length < len) { return progressCurrying.call(_this, fn, _args); } // 参数收集完毕,则执行fn return fn.apply(this, _args); } }

最后再扩展一道经典面试题

// 实现一个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; } add(1)(2)(3) // 6 add(1, 2, 3)(4) // 10 add(1)(2)(3)(4)(5) // 15 add(2, 6)(1) // 9

其他博客理解函数柯里化


19、call apply区别,原生实现bind

可参考call、apply区别及原生实现方式

call、apply、bind 原理、区别及原生js模拟。

Function的prototype原型上存放着 Function实例 的一些共有方法。 A.Function的原型不像其他类(Array、Object…)的原型一样是个对象,Function的原型是一个空函数,是可以执行的,只不过返回undefined,Function.prototype();但是这并不影响它作为一个对象拥有自己的属性方法 B.Function这个类,同时也是Function的一个实例,所以它也具备__proto__属性,这个属性指向它自己的原型

1.call方法。每个函数都可以调用call方法,来改变当前这个函数执行的this关键字,并且支持传入参数;我们用原生js模拟call方法,来更加深刻了解它。 a.第一个参数为调用call方法的函数中的this指向 b.第二个及以后的参数为给调用call方法的函数传入的参数 c.执行这个函数,call方法返回的结果就是 调用他的函数返回的结果 d.将this指向销毁。

Function.prototype.mycall = function(context){ context = context || window; context.fn = this; var arr = []; for(var i = 1;i<arguments.length;i++){ arr.push('arguments['+i+']'); } var result = eval('context.fn('+arr.toString()+')'); delete context.fn; return result; } var obj = {name:'qiaoshi'}; var a = {sex:1} function say(n){ console.log(this,n); } say.mycall(obj,a)

2.apply和call方法类似,作用都是改变当前函数执行的this指向,并且将函数执行。 唯一不同就是 call方法给当前函数传参是一个一个传。而apply是以数组方式传入参数

Function.prototype.myApply =function(context,arr){ context = Object(context) || window; context.fn = this; var result; if(!arr){ result= context.fn(); }else{ var args = []; for(var i=0;i<arr.length;i++){ args.push('arr['+i+']'); } result = eval('context.fn('+args.toString()+')') } delete context.fn; return result; } var q = {name:'chuchu'}; var arg1 = 1; var arg2= [123] function eat(n,m){ console.log(this,n,m); } eat.myApply(q,[arg1,arg2])

3.bind方法,是改变当前调用bind方法的函数this指向,但是不会立即执行当前函数,而是返回一个新的函数。并且支持给新的函数传入参数执行,从而出发之前调用bind方法的函数执行,并且参数透传进去。bind方法是高阶函数的一种。

Function.prototype.myBind = function(){ var context = arguments[0]; var self = this; return function (){ self.myApply(context,arguments) } }; var j = {name:1}; var k = [123] function drink (k){ console.log(this.name,k); } var fn = drink.myBind(j); fn(k);

实现原生 call、apply、bind方法的重点: 1.改变this指向:函数执行,点.前面是谁,this就是谁的原理改变this指向 2.参数透传:通过eval将字符串转变成js语法 去执行。 3.bind方法返回一个函数,返回的函数执行,会进行作用域查找context对象;并且通过原型链查找调用apply方法

call、apply、bind相同和区别 相同:都能改变函数执行的this指向 不同:call apply 是立即执行 bind是不执行

call传参是一个一个传入,apply是数组形式传入

其他关于此类博客 this的绑定规则

21、async/await

一、async/await的优点

1)方便级联调用:即调用依次发生的场景;

2)同步代码编写方式: Promise使用then函数进行链式调用,一直点点点,是一种从左向右的横向写法;async/await从上到下,顺序执行,就像写同步代码一样,更符合代码编写习惯;

3)多个参数传递: Promise的then函数只能传递一个参数,虽然可以通过包装成对象来传递多个参数,但是会导致传递冗余信息,频繁的解析又重新组合参数,比较麻烦;async/await没有这个限制,可以当做普通的局部变量来处理,用let或者const定义的块级变量想怎么用就怎么用,想定义几个就定义几个,完全没有限制,也没有冗余工作;

4)同步代码和异步代码可以一起编写: 使用Promise的时候最好将同步代码和异步代码放在不同的then节点中,这样结构更加清晰;async/await整个书写习惯都是同步的,不需要纠结同步和异步的区别,当然,异步过程需要包装成一个Promise对象放在await关键字后面;

5)基于协程: Promise是根据函数式编程的范式,对异步过程进行了一层封装,async/await基于协程的机制,是真正的“保存上下文,控制权切换……控制权恢复,取回上下文”这种机制,是对异步过程更精确的一种描述;

6)async/await是对Promise的优化: async/await是基于Promise的,是进一步的一种优化,不过在写代码时,Promise本身的API出现得很少,很接近同步代码的写法;

二、协程

(暂时可能忽略吧)

进程>线程>协程协程的第一大优势是具有极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;协程的第二大优势是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多;协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行,需要注意的是:在一个子程序中中断,去执行其他子程序,这并不是函数调用,有点类似于CPU的中断;汽车和公路举个例子:js公路只是单行道(主线程),但是有很多车道(辅助线程)都可以汇入车流(异步任务完成后回调进入主线程的任务队列);generator把js公路变成了多车道(协程实现),但是同一时间只有一个车道上的车能开(依然单线程),不过可以自由变道(移交控制权);协程意思是多个线程互相协作,完成异步任务,运行流程大致如下: 1)协程A开始执行; 2)协程A执行到一半,进入暂停,执行权转移到协程B; 3)一段时间后,协程B交还执行权; 4)协程A恢复执行;协程是一个无优先级的子程序调度组件,允许子程序在特定的地点挂起恢复;线程包含于进程,协程包含于线程,只要内存足够,一个线程中可以有任意多个协程,但某一个时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源;就实际使用理解来说,协程允许我们写同步代码的逻辑,却做着异步的事,避免了回调嵌套,使得代码逻辑清晰;何时挂起,唤醒协程:协程是为了使用异步的优势,异步操作是为了避免IO操作阻塞线程,那么协程挂起的时刻应该是当前协程发起异步操作的时候,而唤醒应该在其他协程退出,并且他的异步操作完成时;单线程内开启协程,一旦遇到io,从应用程序级别(而非操作系统)控制切换对比操作系统控制线程的切换,用户在单线程内控制协程的切换,优点如下:

1)协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级; 2)单线程内就可以实现并发的效果,最大限度地利用cpu;

// 传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。 // 如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高: import time def consumer(): r = '' while True: n = yield r if not n: return print('[CONSUMER] Consuming %s...' % n) time.sleep(1) r = '200 OK' def produce(c): c.next() n = 0 while n < 5: n = n + 1 print('[PRODUCER] Producing %s...' % n) r = c.send(n) print('[PRODUCER] Consumer return: %s' % r) c.close() if __name__=='__main__': c = consumer() produce(c) [PRODUCER] Producing 1... [CONSUMER] Consuming 1... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 2... [CONSUMER] Consuming 2... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 3... [CONSUMER] Consuming 3... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 4... [CONSUMER] Consuming 4... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 5... [CONSUMER] Consuming 5... [PRODUCER] Consumer return: 200 OK

注意到consumer函数是一个generator(生成器),把一个consumer传入produce后:

首先调用c.next()启动生成器;

然后,一旦生产了东西,通过c.send(n)切换到consumer执行;

consumer通过yield拿到消息,处理,又通过yield把结果传回;

produce拿到consumer处理的结果,继续生产下一条消息;

produce决定不生产了,通过c.close()关闭consumer,整个过程结束。

整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。

三、async关键字

1)表明程序里面可能有异步过程: async关键字表明程序里面可能有异步过程,里面可以有await关键字;当然全部是同步代码也没关系,但是这样async关键字就显得多余了;

2)非阻塞: async函数里面如果有异步过程会等待,但是async函数本身会马上返回,不会阻塞当前线程,可以简单认为,async函数工作在主线程,同步执行,不会阻塞界面渲染,async函数内部由await关键字修饰的异步过程,工作在相应的协程上,会阻塞等待异步任务的完成再返回;

3)async函数返回类型为Promise对象: 这是和普通函数本质上不同的地方,也是使用时重点注意的地方; (1)return new Promise();这个符合async函数本意; (2)return data;这个是同步函数的写法,这里是要特别注意的,这个时候,其实就相当于Promise.resolve(data);还是一个Promise对象,但是在调用async函数的地方通过简单的=是拿不到这个data的,因为返回值是一个Promise对象,所以需要用.then(data => { })函数才可以拿到这个data; (3)如果没有返回值,相当于返回了Promise.resolve(undefined);

4)无等待 联想到Promise的特点,在没有await的情况下执行async函数,它会立即执行,返回一个Promise对象,并且绝对不会阻塞后面的语句,这和普通返回Promise对象的函数并无二致;

5)await不处理异步error: await是不管异步过程的reject(error)消息的,async函数返回的这个Promise对象的catch函数负责统一抓取内部所有异步过程的错误;async函数内部只要有一个异步过程发生错误,整个执行过程就中断,这个返回的Promise对象的catch就能抓取到这个错误;

async function testAsync() { return "hello async"; } const result = testAsync(); // 返回一个Promise对象 console.log(result); // async函数返回的是一个Promise对象,async函数(包括函数语句、函数表达式、Lambda表达式)会返回一个Promise对象,如果在函数中return一个直接量,async会把这个直接量通过Promise.resolve() 封装成 Promise 对象; // async函数返回的是一个Promise对象,所以在最外层不能用await获取其返回值的情况,应该使用原始的方式:then()链来处理这个Promise对象 testAsync().then(v => { console.log(v); // 输出 hello async });

四、await关键字

1)await只能在async函数内部使用:不能放在普通函数里面,否则会报错;

2)await关键字后面跟Promise对象:在Pending状态时,相应的协程会交出控制权,进入等待状态,这是协程的本质;

3)await是async wait的意思: wait的是resolve(data)的消息,并把数据data返回,比如下面代码中,当Promise对象由Pending变为Resolved的时候,变量a就等于data,然后再顺序执行下面的语句console.log(a),这真的是等待,真的是顺序执行,表现和同步代码几乎一模一样;

const a = await new Promise((resolve, reject) => { // async process ... return resolve(data); }); console.log(a);

4)await后面也可以跟同步代码: 不过系统会自动将其转化成一个Promsie对象,比如:

const a = await 'hello world' // 相当于 const a = await Promise.resolve('hello world'); // 跟同步代码是一样的,还不如省事点,直接去掉await关键字 const a = 'hello world';

5)await对于失败消息的处理: await只关心异步过程成功的消息resolve(data),拿到相应的数据data,至于失败消息reject(error),不关心不处理;对于错误的处理有以下几种方法供选择: (1)让await后面的Promise对象自己catch; (2)也可以让外面的async函数返回的Promise对象统一catch; (3)像同步代码一样,放在一个try…catch结构中;

async componentDidMount() { // 这是React Native的回调函数,加个async关键字,没有任何影响,但是可以用await关键字 // 将异步和同步的代码放在一个try..catch中,异常都能抓到 try { let array = null; let data = await asyncFunction(); // 这里用await关键字,就能拿到结果值;否则,没有await的话,只能拿到Promise对象 if (array.length > 0) { // 这里会抛出异常,下面的catch也能抓到 array.push(data); } } catch (error) { alert(JSON.stringify(error)) } }

6)await对于结果的处理: await是个运算符,用于组成表达式,await表达式的运算结果取决于它等的东西,如果它等到的不是一个Promise对象,那么await表达式的运算结果就是它等到的东西;如果它等到的是一个Promise对象,await就忙起来了,它会阻塞其后面的代码,等着Promise对象resolve,然后得到resolve的值,作为await表达式的运算结果;虽然是阻塞,但async函数调用并不会造成阻塞,它内部所有的阻塞都被封装在一个Promise对象中异步执行,这也正是await必须用在async函数中的原因;

五、套路分析一

// 异步过程封装 function sleep(ms) { return new Promise((resolve) => { setTimeout(() => { resolve('sleep for ' + ms + ' ms'); }, ms); }); } // 定义异步流程,可以将按照需要定制,就像写同步代码那样 async function asyncFunction() { console.time('asyncFunction total executing:'); const sleep1 = await sleep(2000); console.log('sleep1: ' + sleep1); const [sleep2, sleep3, sleep4]= await Promise.all([sleep(2000), sleep(1000), sleep(1500)]); console.log('sleep2: ' + sleep2); console.log('sleep3: ' + sleep3); console.log('sleep4: ' + sleep4); const sleepRace = await Promise.race([sleep(3000), sleep(1000), sleep(1000)]); console.log('sleep race: ' + sleepRace); console.timeEnd('asyncFunction total executing:'); return 'asyncFunction done.' // 这个可以不返回,这里只是做个标记,为了显示流程 } // 像普通函数调用async函数,在then函数中获取整个流程的返回信息,在catch函数统一处理出错信息 asyncFunction().then(data => { console.log(data); // asyncFunction return 的内容在这里获取 }).catch(error => { console.log(error); // asyncFunction 的错误统一在这里抓取 }); console.log('after asyncFunction code executing....'); // 这个代表asyncFunction函数后的代码, // 显示asyncFunction本身会立即返回,不会阻塞主线程 // 执行结果 after asyncFunction code executing.... sleep1: sleep for 2000 ms sleep2: sleep for 2000 ms sleep3: sleep for 1000 ms sleep4: sleep for 1500 ms sleep race: sleep for 1000 ms asyncFunction total executing:: 5006.276123046875ms asyncFunction done.

代码分析

after asyncFunction code executing…代码位置在async函数asyncFunction()调用之后,反而先输出,这说明async函数asyncFunction()调用之后会马上返回,不会阻塞主线程;

sleep1: sleep for 2000 ms这是第一个await之后的第一个异步过程,最先执行,也最先完成,说明后面的代码,不论是同步和异步,都在等他执行完毕;

sleep2 ~ sleep4这是第二个await之后的Promise.all()异步过程,这是“比慢模式”,三个sleep都完成后,再运行下面的代码,耗时最长的是2000ms;

sleep race: sleep for 1000 ms这是第三个await之后的Promise.race()异步过程,这是“比快模式”,耗时最短sleep都完成后,就运行下面的代码,耗时最短的是1000ms;

asyncFunction total executing:: 5006.276123046875ms这是最后的统计总共运行时间代码,三个await之后的异步过程之和: 1000(独立的) + 2000(Promise.all) + 1000(Promise.race) = 5000ms 这个和统计出来的5006.276123046875ms非常接近,说明上面的异步过程,和同步代码执行过程一致,协程真的是在等待异步过程执行完毕;

asyncFunction done.这个是async函数返回的信息,在执行时的then函数中获得,说明整个流程完毕之后参数传递的过程;

六、套路分析二

余下套路分析代码链接


22、立即执行函数和使用场景

一、什么是立即执行函数?

声明一个函数,并马上调用这个匿名函数就叫做立即执行函数;也可以说立即执行函数是一种语法,让你的函数在定义以后立即执行;

立即执行函数的创建步骤,看下图: 二、立即执行函数的写法:

有时,我们定义函数之后,立即调用该函数,这时不能在函数的定义后面直接加圆括号,这会产生语法错误。产生语法错误的原因是,function 这个关键字,既可以当做语句,也可以当做表达式,比如下边:

//语句 function fn() {}; //表达式 var fn = function (){};

为了避免解析上的歧义,JS引擎规定,如果function出现在行首,一律解析成语句。因此JS引擎看到行首是function关键字以后,认为这一段都是函数定义,不应该以原括号结尾,所以就报错了。

解决方法就是不要让function出现在行首,让JS引擎将其理解为一个表达式,最简单的处理就是将其放在一个圆括号里,比如下边:

(function(){ //code }()) (function (){ //code })()

上边的两种写法,都是以圆括号开头,引擎会意味后面跟的是表达式,而不是一个函数定义语句,所以就避免了错误,这就叫做"立即调用的函数表达式"。 立即执行函数,还有一些其他的写法(加一些小东西,不让解析成语句就可以),比如下边:

(function () {alert("我是匿名函数")}()) //用括号把整个表达式包起来 (function () {alert("我是匿名函数")})() //用括号把函数包起来 !function () {alert("我是匿名函数")}() //求反,我们不在意值是多少,只想通过语法检查 +function () {alert("我是匿名函数")}() -function () {alert("我是匿名函数")}() ~function () {alert("我是匿名函数")}() void function () {alert("我是匿名函数")}() new function () {alert("我是匿名函数")}()

三、立即执行函数的作用:

不必为函数命名,避免了污染全局变量立即执行函数内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量封装变量

总而言之:立即执行函数会形成一个单独的作用域,我们可以封装一些临时变量或者局部变量,避免污染全局变量

四、使用场景

1.怎样使以下alert的结果为0,1,2:

<body> <ul id="list"> <li>公司简介</li> <li>联系我们</li> <li>营销网络</li> </ul> <script> var list = document.getElementById("list"); var li = list.children; for(var i = 0 ;i<li.length;i++){ li[i].onclick=function(){ alert(i); // 结果总是3.而不是0,1,2 } } </script> </body>

为什么alert总是3? 因为i是贯穿整个作用域的,而不是给每一个li分配一个i,点击事件使异步,用户一定是在for运行完了以后,才点击,此时i已经变成3了。 那么怎么解决这个问题呢,可以用立即执行函数,给每个li创建一个独立的作用域,在立即执行函数执行的时候,i的值从0到2,对应三个立即执行函数,这3个立即执行函数里边的j分别是0,1,2所以就能正常输出了,看下边例子

<body> <ul id="list"> <li>公司简介</li> <li>联系我们</li> <li>营销网络</li> </ul> <script> var list = document.getElementById("list"); var li = list.children; for(var i = 0 ;i<li.length;i++){ ( function(j){ li[j].onclick = function(){ alert(j); })(i); //把实参i赋值给形参j } } </script> </body>

当然,也可以使用ES6的块级作用域解决整个问题:

<body> <ul id="list"> <li>公司简介</li> <li>联系我们</li> <li>营销网络</li> </ul> <script> var list = document.getElementById("list"); var li = list.children; for(let i = 0 ;i<li.length;i++){ li[i].onclick=function(){ alert(i); // 结果是0,1,2 } } </script> </body>

2.如何避免了污染全局变量

某些代码只需要执行一次,比如只需要显示一个时间,但是这些代码也需要一些临时的变量,但是初始化过程结束之后,就再也不会被用到,如果将这些变量作为全局变量,不是一个好的主意,我们可以用立即执行函数——去将我们所有的代码包裹在它的局部作用域中,不会让任何变量泄露成全局变量,看如下代码: 比如上面的代码,如果没有被包裹在立即执行函数中,而是直接以非函数的形式直接写在


23、设计模式(要求说出如何实现,应用,优缺点)/单例模式实现

设计模式

工厂模式单体模式模块模式代理模式职责链模式命令模式模板方法模式策略模式发布–订阅模式中介者模式

单例模式实现

// 单例模式 var Singleton = function(name){ this.name = name; this.instance = null; }; Singleton.prototype.getName = function(){ return this.name; } // 获取实例对象 function getInstance(name) { if(!this.instance) { this.instance = new Singleton(name); } return this.instance; } // 测试单例模式的实例 var a = getInstance("aa"); var b = getInstance("bb");

Javascript常用的设计模式详解

一:理解工厂模式

工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。

简单的工厂模式可以理解为解决多个相似的问题;这也是她的优点;比如如下代码:

function CreatePerson(name,age,sex) { var obj = new Object(); obj.name = name; obj.age = age; obj.sex = sex; obj.sayName = function(){ return this.name; } return obj; } var p1 = new CreatePerson("longen",'28','男'); var p2 = new CreatePerson("tugenhua",'27','女'); console.log(p1.name); // longen console.log(p1.age); // 28 console.log(p1.sex); // 男 console.log(p1.sayName()); // longen console.log(p2.name); // tugenhua console.log(p2.age); // 27 console.log(p2.sex); // 女 console.log(p2.sayName()); // tugenhua // 返回都是object 无法识别对象的类型 不知道他们是哪个对象的实列 console.log(typeof p1); // object console.log(typeof p2); // object console.log(p1 instanceof Object); // true

如上代码:函数CreatePerson能接受三个参数name,age,sex等参数,可以无数次调用这个函数,每次返回都会包含三个属性和一个方法的对象。

工厂模式是为了解决多个类似对象声明的问题;也就是为了解决实列化对象产生重复的问题。

优点:能解决多个相似的问题。

缺点:不能知道对象识别的问题(对象的类型不知道)。

24、iframe的缺点有哪些

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已经渐渐的退出了前端开发。

25、数组问题

1、数组去重(12种方法,史上最全)

利用ES6 Set去重(ES6中最常用):

function unique (arr) { return Array.from(new Set(arr)) } var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}]; console.log(unique(arr)) //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]

2、数组常用方法

3、js查找数组重复元素

方法一:利用sort方法,先使用sort方法将数组排序,再来判断找出重复元素

function res(arr){ var temp=[]; arr.sort().sort(function(a,b){ if(a===b&&temp.indexOf(a)===-1){ temp.push(a) } }) return temp; } var arr=[1,2,4,4,3,3,1,5,3]; console.log(res(arr))

方法二:对数组每一项循环,判断它的第一个位置和最后一个位置只要不一样就是重复值,然后在判断放置重复值的数组有没有这个值

function res(arr){ var temp=[]; arr.forEach(function(item){ if(arr.indexOf(item)!==arr.lastIndexOf(item)&&temp.indexOf(item)===-1){ temp.push(item) } }) return temp; }

4、扁平化数组的几种方法

一、扁平化的概念

扁平化管理是企业为解决层级结构的组织形式在现代环境下面临的难题而实施的一种管理模式。当企业规模扩大时,原来的有效办法是增加管理层次,而现在的有效办法是增加管理幅度。当管理层次减少而管理幅度增加时,金字塔状的组织形式就被“压缩”成扁平状的组织形式。

二、数组扁平化

用于将嵌套多层的数组“拉平”,变成一维的数组 1、[1, [2]] => [1,2] 2、[[1, 2], [3, 4]] => [1, 2, 3, 4] 3、[1, 2, [3, 4, [5, 6]]] => [1, 2, 3, 4, 5, 6]

三、数组扁平化的几种方法

【1】Array​.prototype​.flat() flat()方法会按照一个可指定的深度depth递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。该方法不会改变原数组

语法: let newArray = arr.flat(depth) 描述: depth为指定要提取嵌套数组的结构深度,默认值为 1 flat() 方法会移除数组中的空项

例子:

let arr1 = [1, 2, [3, 4]]; arr1.flat(); // [1, 2, 3, 4] // 指定要提取嵌套数组的结构深度为1层 let arr2 = [1, 2, [3, 4, [5, 6]]]; arr2.flat(1); // [1, 2, 3, 4, [5, 6]] // 指定要提取嵌套数组的结构深度为2层 let arr3 = [1, 2, [3, 4, [5, 6]]]; arr3.flat(2); // [1, 2, 3, 4, 5, 6] // 使用 Infinity 作为深度,展开任意深度的嵌套数组 let arr4 = [1, 2, [3, 4, [5, 6]]] arr4.flat(Infinity); // [1, 2, 3, 4, 5, 6] // 移除数组中的空项 let arr5 = [1, 2, , 4, 5]; arr5.flat(); // [1, 2, 4, 5]

【2】归并方法 reduce()

我们用reduce函数进行遍历,把prev的初值赋值为[],如果当前的值是数组的话,那么我们就递归遍历它的孩子,如果当前的值不是数组,那么我们就把它拼接进数组里。

let arr = [[1, 2, [3, 4], 5], [6, 7, 8], [[9, 10], 11]]; function flat(arr) { return arr.reduce(function (prev, cur) { return prev.concat(Array.isArray(cur) ? flat(cur) : cur); }, []) } console.log(flat(arr)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

注:reduce函数用法详解

【3】toString()

toString()方法有很大局限性,只适用于数组元素全部为数字的情况下

// toString && Json.parase let arr = [[1, 2, [3, 4], 5], [6, 7, 8], [[9, 10], 11]]; function flat(arr) { var str = arr.toString(); return JSON.parse('[' + str + ']'); } console.log(flat(arr)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] // toString && split let arr = [[1, 2, [3, 4], 5], [6, 7, 8], [[9, 10], 11]]; function flat(arr) { return arr.toString().split(',').map(item => { return Number(item) }) } console.log(flat(arr)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] // join && split let arr = [[1, 2, [3, 4], 5], [6, 7, 8], [[9, 10], 11]]; function flat(arr) { return arr.join(',').split(',').map(item => { return Number(item); }) } console.log(flat(arr)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

【4】循环+递归 递归的遍历每一项,若为数组则继续遍历,否则concat

let arr = [[1, 2, [3, 4], 5], [6, 7, 8], [[9, 10], 11]]; function flat(arr) { let result = []; arr.map(item => { if (Array.isArray(item)) { result = result.concat(flat(item)); } else { result.push(item); } }); return result; } console.log(flat(arr)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

【5】扩展运算符. . .

… 用于取出参数对象的所有可遍历属性,拷贝到当前对象中

let arr = [[1, 2, [3, 4], 5], [6, 7, 8], [[9, 10], 11]] function flat(arr) { while (arr.some(item => Array.isArray(item))) { arr = [].concat(...arr); } return arr; } console.log(flat(arr)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

5、按数组中各项和特定值差值排序

1、js sort方法根据数组中对象的某一个属性值进行排序

var arr = [ {name:'zopp',age:0}, {name:'gpp',age:18}, {name:'yjj',age:8} ]; function compare(property){ return function(a,b){ var value1 = a[property]; var value2 = b[property]; return value1 - value2; } } console.log(arr.sort(compare('age')))

26、BOM属性对象方法

参考文章

参考手册注意:BOM,即JavaScript可以进行操作的浏览器的各个功能部件的接口

Window对象

1、window方法

confirm

console.log(window.confirm('我是小仙女')); //点确认,返回true

open:

window.open("http://www.baidu.com","baidu") //可以打开新的窗口

close

window.close() //把窗口关了

2、window属性:

closed

我们有的时候需要代开我们的子窗体,域名下面还有一个新的域名,也就是子域名,子网页 var myWindow = window.open("xxx.html"); 我们打开一个子网页, 会返回给我们一个值的,这个值代表另一个页面的window属性 可以通过myWindow.closed来查看另一个页面的是否关闭的

opener: 比如说我现在一个空页面创建一个百度,那么百度的opener就是我

注意: window和document没什么明确的所属关系,window只是存了一个document的引用而已,它俩没有关系,因为window对象除了充当控制浏览器的对象以外,还充当全局作用域,所以必须存有document的引用

history同理,没有关系,只存了一个引用而已

Navigator对象

1、历史: javascript在1996年首次在Navigator上实现,所以这也是最先驱的浏览器的名字 在2003年的时候网景公司将Navigator浏览器源码开源了,Navigator源码是masic,火狐公司拿了Navigator的源码建立了firefox

2、Navigator对象属性:

appName (现在一般浏览器除了ie(Internet explore)以外差不多打出来的appName几乎都是"Netscape")

navigator.appName 返回:"Netscape"

online 指明系统是否处于脱机状态(就是检查现在是否有网)要是脱机状态也就是没网返回false,有网返回true,

userAgent 返回客户端的完整信息,可以判断机型,浏览器,手机端还是PC端。 调用这个会返回很多信息,其中有一个就是Windows NT 10.0; WOW64 代表我现在是window系统 Nt,但是要是切换到手机版的话,就返回Linux; Android 6.0; 还可以调到iPhone; CPU iPhone OS;

Screen对象

Screen对象属性 deviceXDPI:返回显示屏幕的每英寸水平点(像素)数。返回的其实就是分辨率,而那个水平点数中的一点就是一像素,DPI是显示分辨率的一个比例值,DPI越高说明分辨率越高,如果安卓DPI比率分辨值为1的话,苹果必然是2,苹果的像素高

History对象

History对象属性: length在同一个标签页,跳转了多少次,length就是多少 例如在一个标签页内,先打开自己,length就为1,再打开百度,length就为2,再打开新浪,length就为3.

History对象方法: back()和forward()

history.back(1):就回到了百度 history.forward(1)就又回到了新浪 然后再history还为3 通过回退和向前操作而产生的历史不记录到历史记录里 还有history.back(2)也还是回到百度,而不是自己 所以填不填参数没什么区别,这个方法是不需要填参数的。

go()

history.go(-2)从新浪回到自己, 再history.go(2)从自己又回到新浪, history.go(-1)从新浪回到百度,这个需要填参数

Location对象

Location对象属性

host:设置或返回主机名和当前 URL 的端口号。 拿百度来说:location.host 返回"www.baidu.com" 端口号:我们正常一个网页是http://www.baidu.com,但是这叫域名,不是IP地址, 正常要转化为IP地址的,在转化为IP地址的时候,我们势必要给它加一个端口号 如果域名后面没加端口号,那默认加一个80端口,也就是这样的http://www.baidu.com:80 那么加端口有什么用呢:一个IP地址代表一个服务器,一个服务器的功能有网页下载,FTP下载(文件下载)什么的很多功能而这些功能一个浏览器就能实现, 那么怎么区别这些功能呢:我们就用IP地址来区分,域名,IP地址是来找到服务器的,具体让服务器实现什么功能,用端口号来提供,改变端口号是访问页面的hostname:设置或返回当前 URL 的主机名protocol:设置或返回当前 URL 的协议。(http或者https)search:设置或返回从问号 (?) 开始的 URL(查询部分)。 例: <form action="" method="get"> <input type="text" value="abc" name="bcd"/> <input type="submit"/> </form> 点击提交,会出现问号部分,然后location.search就返回"?bcd=abc", hash:设置或返回从井号 (#) 开始的 URL(锚)。其实就相当于锚点 利用hash能找到div,在控制台打印location.hash = "only"就能找到div,还有通过改变location.hash能改变定位锚点 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>lottery</title> <style> #only{ width:100px; height:100px; background-color:red; position:absolute; top:1000px; } </style> </head> <body> <div id="only"></div> <script type=text/javascript></script> </body> </html>

锚点,利用a标签,点击find 就能找到div

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <tile>lottery</title> <style> #only{ width:100px; height:100px; background-color:red; position:absolute; top:1000px; } </style> </head> <body> <div id="only"></div> <a href="#only">find you</a> <script type=text/javascript></script> </body> </html>

一般location.hash都配合CSS3来使用,还有a标签href也可以改变location.hash,所以它有锚点的功能。改变的动态的其实也是location.hash 利用location.hash来定位div然后background-color就会改变

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>lottery</title> <style> #only{ width:100px; height:100px; background-color:red; position:absolute; top:1000px; } #only:target{ background-color:pink; } </style> </head> <body> <div id="only"></div> <a href="#only">find you</a> <script type=text/javascript></script> </body> </html>

27、服务端渲染

为什么现在又流行服务端渲染html?

浅谈服务端渲染(推荐)

28、垃圾回收机制

js中垃圾回收机制

29、eventloop,进程和线程,任务队列

30、如何快速让字符串变成已千为精度的数字

最新回复(0)