前端(二)JS篇

JS篇

js数据类型

1、js有几种数据类型,其中基本数据类型有哪些

七种数据类型
复制代码
  • Boolean
  • Null
  • Undefined
  • Number
  • String
  • Symbol (ECMAScript 6 新定义)
  • Object

(ES6之前)其中5种为基本类型:string,number,boolean,null,undefined

ES6出来的Symbol也是原始数据类型 ,表示独一无二的值

Object 为引用类型(范围挺大),也包括数组、函数

2、null和undefined的差异

大体说一下,想要知其所以然请引擎搜索

相同点:

  • 在 if判断语句中,值都默认为 false
  • 大体上两者都是代表无,具体看差异

差异:

  • null转为数字类型值为0,而undefined转为数字类型为NaN(Not a Number)
  • undefined是代表调用一个值而该值却没有赋值,这时候默认则为undefined
  • null是一个很特殊的对象,最为常见的一个用法就是作为参数传入(说明该参数不是对象)
  • 设置为null的变量或者对象会被内存收集器回收

3、== 和 ===区别,什么情况用 ==

JS对象

1、原生对象及方法

1.1、改变原数组的方法

  • 改变原数组的方法:
pop、push、reverse、shift、sort、splice、unshift,以及两个ES6新增的方法copyWithin 和 fill;

复制代码
  • 不改变原数组(复制):
concat、join、slice、toString、toLocaleString、indexOf、lastIndexOf、
未标准的toSource以及ES7新增的方法includes;
复制代码
  • 循环遍历:
forEach、every、some、filter、map、reduce、reduceRight
以及ES6新增的方法entries、find、findIndex、keys、values。
复制代码

1.2、splice和slice、map和forEach、 filter()、reduce()的区别

  • 1.slice(start,end):方法可以从已有数组中返回选定的元素,返回一个新数组,包含从startend(不包含该元素)的数组方法 注意:该方法不会更新原数组,而是返回一个子数组
  • 2.splice():该方法想或者从数组中添加或删除项目,返回被删除的项目。(该方法会改变原数组) splice(index, howmany,item1,...itemx) ·index参数:必须,整数规定添加或删除的位置,使用负数,从数组尾部规定位置 ·howmany参数:必须,要删除的数量, ·item1..itemx:可选,向数组添加新项目
  • 3.map():会返回一个全新的数组。使用于改变数据值的时候。会分配内存存储空间数组并返回,forEach()不会返回数据
  • 4.forEach(): 不会返回任何有价值的东西,并且不打算改变数据,单纯的只是想用数据做一些事情,他允许callback更改原始数组的元素
  • 5.reduce(): 方法接收一个函数作为累加器,数组中的每一个值(从左到右)开始缩减,最终计算一个值,不会改变原数组的值
  • 6.filter(): 方法创建一个新数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。它里面通过function去做处理
  • String
方法 描述
charAt() 返回在指定位置的字符。
charCodeAt() 返回在指定的位置的字符的 Unicode 编码。
concat() 连接字符串。
indexOf() 检索字符串。
match() 找到一个或多个正则表达式的匹配。
replace() 替换与正则表达式匹配的子串。
search() 检索与正则表达式相匹配的值。
slice() 提取字符串的片断,并在新的字符串中返回被提取的部分。
split() 把字符串分割为字符串数组。
toLocaleLowerCase() 把字符串转换为小写。
toLocaleUpperCase() 把字符串转换为大写。
toLowerCase() 把字符串转换为小写。
toUpperCase() 把字符串转换为大写。
substr() 从起始索引号提取字符串中指定数目的字符。
substring() 提取字符串中两个指定的索引号之间的字符。

-** Array**

方法 描述
slice[start,end) 返回从原数组中指定开始下标到结束下标之间的项组成的新数组(不影响原数组)
: . 1个参数:n.即:n到末尾的所有
: . 2个参数:[start,end]
splice(): . 删除:2个参数,起始位置,删除的项数
. 插入:3个参数,起始位置,删除的项数,插入的项
. 替换:任意参数,起始位置,删除的项数,插入任意数量的项
pop() 删除数组的最后一个元素,减少数组的长度,返回删除的值。(无参)
push() 将参数加载到数组的最后,返回新数组的长度。 (参数不限)
shift() 删除数组的第一个元素,数组长度减1,返回删除的值。 (无参)
unshift() 向数组的开头添加一个或更多元素,并返回新的长度。(参数不限)
sort() 按指定的参数对数组进行排序 ,返回的值是经过排序之后的数组(无参/函数)
concat(3,4) 把两个数组拼接起来。 返回的值是一个副本 (参数不限)
join() 将数组的元素组起一个字符串,以separator为分隔符,省略的话则用默认用逗号为分隔符
indexOf() 从数组开头向后查找,接受两个参数,要查找的项(可选)和查找起点位置的索引
lastIndexOf() 从数组末尾开始向前查找,接受两个参数,要查找的项(可选)和查找起点位置的索引
every() 对数组中的每一项运行给定函数,如果该函数对每一项都返回true,则返回true。
filter() 对数组中的每一项运行给定函数,返回该函数会返回true的项组成数组。
forEach() 对数组的每一项运行给定函数,这个方法没有返回值。
map() 对数组的每一项运行给定函数,返回每次函数调用的结果组成的数组。
some() 对数组的每一项运行给定参数,如果该函数对任一项返回true,则返回true。以上方法都不会修改数组中的包含的值。
reduce()和reduceRight() 缩小数组的方法,这两个方法都会迭代数组的所有项,然后构建一个最终返回的值。
  • 正则
方法 描述
compile 编译正则表达式。
exec 检索字符串中指定的值。返回找到的值,并确定其位置。
test 检索字符串中指定的值。返回 true 或 false。
search 检索与正则表达式相匹配的值。
match 找到一个或多个正则表达式的匹配。
replace 替换与正则表达式匹配的子串。
split 把字符串分割为字符串数组。

推荐:知道这20个正则表达式,能让你少写1,000行代码

面试的一些 JavaScript 算法

2、创建对象的几种方法以及优缺点

原型 / 构造函数 / 实例

  • 原型(prototype): 一个简单的对象,用于实现对象的 属性继承。可以简单的理解成对象的爹。在 Firefox 和 Chrome 中,每个JavaScript对象中都包含一个__proto__ (非标准)的属性指向它爹(该对象的原型),可obj.__proto__进行访问。

  • 构造函数: 可以通过new来 新建一个对象 的函数。

  • 实例: 通过构造函数和new创建出来的对象,便是实例。 实例通过__proto__指向原型,通过constructor指向构造函数。

2.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.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)

2.3、原型模式

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法

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

复制代码

原型中所有属性是被实例共享的, 引用类型的属性会出问题

function Person(){
}

Person.prototype = {
    constructor: Person, // 重写原型一定要将constructor属性赋值为原构造函数,否则原型丢失
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    friends : ["Shelby", "Court"],
    sayName : function () {
        alert(this.name);
    }
};

var person1 = new Person();
var person2 = new Person();

person1.friends.push("Van");

console.log(person1.friends);    //"Shelby,Court,Van"
console.log(person2.friends);    //"Shelby,Court,Van"
console.log(person1.friends === person2.friends);  //true

复制代码

2.4、组合使用构造函数模式和原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。

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中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。

2.5、动态原型模式

把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。

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操作符确定它的类型。

2.6、寄生构造函数模式

这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数

function Person(name, age, job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        alert(this.name);
    };    
    return o;
}

var p1 = new Person("Nicholas", 29, "Software Engineer");
p1.sayName();  //"Nicholas"

复制代码

Person函数创建了一个新对象,并以相应的属性和方法初始化该对象,然后又返回了这个对象。除了使用new操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的,构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。

这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用这个模式。

function SpecialArray(){

    //创建数组
    var values = new Array();

    //添加值
    values.push.apply(values, arguments);

    //添加方法
    values.toPipedString = function(){
        return this.join("|");
    };

    //返回数组
    return values;
}

var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); //"red|blue|green"

复制代码

关于寄生构造函数模式,有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖instanceof操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式

2.7、稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this和new),或者在防止数据被其他应用程序改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this;二是不使用new操作符调用构造函数。按照稳妥构造函数的要求,可以将前面的Person构造函数重写如下。

function Person(name, age, job){

    //创建要返回的对象
    var o = new Object();

    //可以在这里定义私有变量和函数

    //添加方法
    o.sayName = function(){
        alert(name);
    };    

    //返回对象
    return o;
}

复制代码

注意,在以这种模式创建的对象中,除了使用sayName()方法之外,没有其他办法访问name的值。可以像下面使用稳妥的Person构造函数。

var person = Person("Nicholas", 29, "Software Engineer");
person.sayName();  //"Nicholas"

复制代码

这样,变量person中保存的是一个稳妥对象,而除了调用sayName()方法外,没有别的方式可以访问其数据成员。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境下使用。

3、this对象及指向

改变函数内部this指针的指向函数(bind,apply,call的区别)

  • 通过applycall改变函数的this指向,他们两个函数的第一个参数都是一样的表示要改变指向的那个对象,第二个参数,apply是数组,而call则是arg1,arg2...这种形式。

  • 通过bind改变this作用域会返回一个新的函数,这个函数不会马上执行。

手动实现 call 方法:

Function.prototype.myCall = function(context = window, ...rest) {
    context.fn = this; //此处this是指调用myCall的function
    let result = context.fn(...rest);
    //将this指向销毁
    delete context.fn;
    return result;
};
复制代码

手动实现 apply 方法:

Function.prototype.myCall = 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;
};
复制代码

手动实现 bind 方法:

Function.prototype.myBind = function(oThis, ...rest) {
    let _this = this;
    let F = function() {}
    // 根据 bind 规定,如果使用 new 运算符构造 bind 的返回函数时,第一个参数绑定的 this 失效
    let resFn = function(...parmas) {
        return _this.apply(this instanceof resFn ? this : oThis, [...rest,...parmas]);
    };
    // 继承原型
    if (this.prototype) {
        F.prototype = this.prototype;
        resFn.prototype = new F;
    }
    return resFn;
};

复制代码

京东小姐姐的文章 嗨,你真的懂this吗?

4、浅拷贝和深拷贝

浅拷贝就是把属于源对象的值都复制一遍到新的对象,不会开辟两者独立的内存区域;

深度拷贝则是完完全全两个独立的内存区域,互不干扰

  • 浅拷贝

// 这个 ES5的

function shallowClone(sourceObj) {
  // 先判断传入的是否为对象类型
  if (!sourceObj || typeof sourceObj !== 'object') {
    console.log('您传入的不是对象!!')
  }
  // 判断传入的 Obj是类型,然后给予对应的赋值
  var targetObj = sourceObj.constructor === Array ? [] : {};
  
  // 遍历所有 key
  for (var keys in sourceObj) {
    // 判断所有属于自身原型链上的 key,而非继承(上游 )那些
    if (sourceObj.hasOwnProperty(keys)) {
      // 一一复制过来
      targetObj[keys] = sourceObj[keys];
    }
  }
  return targetObj;
}

 // ES6 可以用 Object.assign(targeObj, source1,source2,source3) 来实现对象浅拷贝
 


复制代码
  • 深度拷贝

// 就是把需要赋值的类型转为基本类型(字符串这些)而非引用类型来实现
// JOSN对象中的stringify可以把一个js对象序列化为一个JSON字符串,parse可以把JSON字符串反序列化为一个js对象

var deepClone = function(sourceObj) {
  if (!sourceObj || typeof sourceObj !== 'object') {
    console.log('您传入的不是对象!!');
    return;
  }
  // 转->解析->返回一步到位
  return window.JSON
    ? JSON.parse(JSON.stringify(sourceObj))
    : console.log('您的浏览器不支持 JSON API');
};




复制代码
  • 深拷贝的考虑点实际上要复杂的多,详情看看知乎怎么说

原型链和继承

js万物皆对象,用var a={}var a = new Object()或者用构造函数的形式:var a = new A()创建一个对象时,该对象不仅可以访问它自身的属性,还会根据__proto__属性找到它原型链上的属性,直到找到Object上面的null

每个函数都有prototype 属性,除了 Function.prototype.bind(),该属性指向原型。

每个对象都有__proto__ 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 _proto_来访问。

对象可以通过 __proto__来寻找不属于该对象的属性,__proto__将对象连接起来组成了原型链。

  • 原型链继承: function Cat(){ } Cat.prototype = new Animal(); Cat.prototype.name = 'cat'; 无法实现多继承
  • 构造继承:使用父类的构造函数来增强子类实例。function Cat(name){Animal.call(this);this.name = name || 'Tom';}无法继承父类原型链上的属性跟方法 installof去检验
  • 实例继承:为父类实例添加新特性,作为子类实例的返回
  • 拷贝继承:拷贝父类元素上的属性跟方法
  • 组合继承:构造继承 和原型继承的组合体
  • 寄生组合继承:通过寄生方式,在构造继承上加一个·Super·函数(没有实例和方法)让他的原型链指向父类的原型链砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性

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、组合继承

相当于构造继承和原型链继承的组合体。通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

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 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);
复制代码

详细请看 路易斯的文章 JS原型链与继承别再被问倒了

函数作用域

1、JS 的作用域是什么?有什么特别之处么?

变量作用域的概念:就是一个变量可以使用的范围

JS中首先有一个最外层的作用域:称之为全局作用域

JS中还可以通过函数创建出一个独立的作用域,其中函数可以嵌套,所以作用域也可以嵌套

2、作用域链

作用域链是由当前作用域与上层一系列父级作用域组成,作用域的头部永远是当前作用域,尾部永远是全局作用域。作用域链保证了当前上下文对其有权访问的变量的有序访问。

作用域链的意义:查找变量(确定变量来自于哪里,变量是否可以访问)

简单来说,作用域链可以用以下几句话来概括:(或者说:确定一个变量来自于哪个作用域)

查看当前作用域,如果当前作用域声明了这个变量,就确定结果

查找当前作用域的上级作用域,也就是当前函数的上级函数,看看上级函数中有没有声明

再查找上级函数的上级函数,直到全局作用域为止

如果全局作用域中也没有,我们就认为这个变量未声明(xxx is not defined)

推荐文章 深入到不能再深入之JS大法系列 -- 作用域链

3、函数声明 VS 函数表达式

函数声明和函数表达式判别的依据是:函数的声明是否以function关键词开始。 以关键词function 开始的声明是函数声明,其余的函数声明全部是函数表达式。

//函数声明
function foo() {

}

//函数表达式
var foo = function () {

};

(function() {

})();

复制代码

4、具名函数 VS 匿名函数

  • 具名函数 拥有名字的函数
function foo() {

}

var foo = function bar() {

}

setTimeout( function foo() {

} )

+function foo() {

}();

复制代码

需要注意:函数声明一定要是具名函数。

  • 匿名函数 没有名字的函数
var foo = function () {

}

setTimeout( function foo() {

} )

-function foo() {

}();

复制代码
  • 立即执行函数(IIFE)
vara=2;

(function foo() { 
    var a=3;
    console.log( a ); // 3
})();

console.log( a ); // 2

复制代码
  • 另一种表达形式
(function() {

}())

复制代码

5、闭包

5.1、定义

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这就产生了闭包。 ----《你不知道的Javascript上卷》
复制代码

闭包就是一个函数,一个可以访问并操作其他函数内部变量的函数。也可以说是一个定义在函数内部的函数。因为JavaScript没有动态作用域,而闭包的本质是静态作用域(静态作用域规则查找一个变量声明时依赖的是源程序中块之间的静态关系),所以函数访问的都是我们定义时候的作用域,也就是词法作用域。所以闭包才会得以实现。

我们常见的闭包形式就是a 函数套 b 函数,然后 a 函数返回 b 函数,这样 b 函数在 a 函数以外的地方执行时,依然能访问 a 函数的作用域。其中“b 函数在 a 函数以外的地方执行时”这一点,才体现了闭包的真正的强大之处。

function fn1() {
	var name = 'iceman';
	function fn2() {
		console.log(name);
	}
	return fn2;
}
var fn3 = fn1();
fn3();

复制代码

这样就清晰地展示了闭包:

  • fn2的词法作用域能访问fn1的作用域

  • fn2当做一个值返回

  • fn1执行后,将fn2的引用赋值给fn3

  • 执行fn3,输出了变量name

我们知道通过引用的关系,fn3就是fn2函数本身。执行fn3能正常输出name,这不就是fn2能记住并访问它所在的词法作用域,而且fn2函数的运行还是在当前词法作用域之外了。

正常来说,当fn1函数执行完毕之后,其作用域是会被销毁的,然后垃圾回收器会释放那段内存空间。而闭包却很神奇的将fn1的作用域存活了下来,fn2依然持有该作用域的引用,这个引用就是闭包。

总结:某个函数在定义时的词法作用域之外的地方被调用,闭包可以使该函数极限访问定义时的词法作用域。
复制代码

5.2、闭包形成的条件

  • 函数嵌套
  • 内部函数引用外部函数的局部变量

5.3、闭包的特性

每个函数都是闭包,每个函数天生都能够记忆自己定义时所处的作用域环境。把一个函数从它定义的那个作用域,挪走,运行。这个函数居然能够记忆住定义时的那个作用域。不管函数走到哪里,定义时的作用域就带到了哪里。接下来我们用两个例子来说明这个问题:

//例题1
var inner;
function outer(){
var a=250;
inner=function(){
alert(a);//这个函数虽然在外面执行,但能够记忆住定义时的那个作用域,a是250
  }
}
outer();
var a=300;
inner();//一个函数在执行的时候,找闭包里面的变量,不会理会当前作用域。

复制代码
//例题2
function outer(x){
  function inner(y){
  console.log(x+y);
  }
return inner;
}
var inn=outer(3);//数字3传入outer函数后,inner函数中x便会记住这个值
inn(5);//当inner函数再传入5的时候,只会对y赋值,所以最后弹出8

复制代码

5.4、闭包的内存泄漏

栈内存提供一个执行环境,即作用域,包括全局作用域和私有作用域,那他们什么时候释放内存的?

  • 全局作用域----只有当页面关闭的时候全局作用域才会销毁
  • 私有的作用域----只有函数执行才会产生

一般情况下,函数执行会形成一个新的私有的作用域,当私有作用域中的代码执行完成后,我们当前作用域都会主动的进行释放和销毁。但当遇到函数执行返回了一个引用数据类型的值,并且在函数的外面被一个其他的东西给接收了,这种情况下一般形成的私有作用域都不会销毁。

如下面这种情况:

function fn(){
var num=100;
return function(){
  }
}
var f=fn();//fn执行形成的这个私有的作用域就不能再销毁了

复制代码

也就是像上面这段代码,fn函数内部的私有作用域会被一直占用的,发生了内存泄漏。

所谓内存泄漏指任何对象在您不再拥有或需要它之后仍然存在。闭包不能滥用,否则会导致内存泄露,影响网页的性能。闭包使用完了后,要立即释放资源,将引用变量指向null。

接下来我们看下有关于内存泄漏的一道经典面试题:

  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  

复制代码

5.5、闭包的作用

  • 可以读取函数内部的变量。
  • 可以使变量的值长期保存在内存中,生命周期比较长。因此不能滥用闭包,否则会造成网页的性能问题
  • 可以用来实现JS模块。

JS模块:具有特定功能的js文件,将所有的数据和功能都封装在一个函数内部(私有的),只向外暴露一个包信n个方法的对象或函数,模块的使用者,只需要通过模块暴露的对象调用方法来实现对应的功能。

5.6、闭包的运用

应用闭包的主要场合是:设计私有的方法和变量。

推荐文章:

JavaScript 闭包

高效使用 JavaScript 闭包

6、new操作符

推荐 yck 的文章 重学 JS 系列:聊聊 new 操作符

7、 执行上下文(EC)

执行上下文可以简单理解为一个对象:

  • 它包含三个部分:

    • 变量对象(VO)
    • 作用域链(词法作用域)
    • this指向
  • 它的类型:

    • 全局执行上下文
    • 函数执行上下文
    • eval执行上下文
  • 代码执行过程:

    • 创建 全局上下文 (global EC)
    • 全局执行上下文(caller)逐行 自上而下 执行。遇到函数时,函数执行上下文 (callee)push到执行栈顶层
    • 函数执行上下文被激活,成为 active EC, 开始执行函数中的代码,caller 被挂起
    • 函数执行完后,calleepop移除出执行栈,控制权交还全局上下文(caller),继续执行

DOM

1、dom 操作

创建:

createDocumentFragment()    //创建一个DOM片段

createElement()   //创建一个具体的元素

createTextNode()   //创建一个文本节点
复制代码
  • 添加:appendChild()

  • 移出:removeChild()

  • 替换:replaceChild()

  • 插入:insertBefore()

  • 复制:cloneNode(true)

查找:

getElementsByTagName()    //通过标签名称

getElementsByClassName()    //通过标签名称

getElementsByName()    //通过元素的Name属性的值

getElementById()    //通过元素Id,唯一性
复制代码

子节点

  • Node.childNodes //获取子节点列表NodeList; 注意换行在浏览器中被算作了text节点,如果用这种方式获取节点列表,需要进行过滤
  • Node.firstChild //返回第一个子节点
  • Node.lastChild //返回最后一个子节点

父节点

  • Node.parentNode // 返回父节点
  • Node.ownerDocument //返回祖先节点(整个document)

同胞节点

  • Node.previousSibling // 返回前一个节点,如果没有则返回null
  • Node.nextSibling // 返回后一个节点

2、dom事件

3、dom事件模型

DOM事件模型分为捕获和冒泡。一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。

  • (1)捕获阶段:事件从window对象自上而下目标节点传播的阶段;
  • (2)目标阶段:真正的目标节点正在处理事件的阶段;
  • (3)冒泡阶段:事件从目标节点自下而上window对象传播的阶段。

上文中讲到了addEventListener的第三个参数为指定事件是否在捕获或冒泡阶段执行,设置为true表示事件在捕获阶段执行,而设置为false表示事件在冒泡阶段执行。那么什么是事件冒泡和事件捕获呢?可以用下图来解释:

3.1.事件捕获

捕获是从上到下,事件先从window对象,然后再到document(对象),然后是html标签(通过document.documentElement获取html标签),然后是body标签(通过document.body获取body标签),然后按照普通的html结构一层一层往下传,最后到达目标元素。我们只需要将addEventListener第三个参数改为true就可以实现事件捕获。

代码如下:

//摘自xyyojl的《深入理解DOM事件机制》




"grandfather1"> 爷爷
"parent1"> 父亲
"child1">儿子
复制代码

3.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  

复制代码

4、事件代理(事件委托)

由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)

4.1.优点

  • 减少内存消耗,提高性能

如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的,效率上需要消耗很多性能。借助事件代理,我们只需要给父容器ul绑定方法即可,这样不管点击的是哪一个后代元素,都会根据冒泡传播的传递机制,把容器的click行为触发,然后把对应的方法执行,根据事件源,我们可以知道点击的是谁,从而完成不同的事。

  • 动态绑定事件

在很多时候,我们需要通过用户操作动态的增删列表项元素,如果一开始给每个子元素绑定事件,那么在列表发生变化时,就需要重新给新增的元素绑定事件,给即将删去的元素解绑事件,如果用事件代理就会省去很多这样麻烦。

4.2、跨浏览器处理事件程序

标准事件对象:

  • (1)type:事件类型
  • (2)target:事件目标
  • (3)stopPropagation()方法:阻止事件冒泡
  • (4)preventDefault()方法:阻止事件的默认行为

IE中的事件对象:

  • (1)type:事件类型

  • (2)srcElement:事件目标

  • (3)cancelBubble属性:阻止事件冒泡 true表示阻止冒泡,false表示不阻止

  • (4)returnValue属性:阻止事件的默认行为

/*
* @Author: 陈陈陈
*/
var  eventUtil{

    /*添加事件处理程序    */
    addHandler:function(element,type,handler){//参数表元素、事件类型、函数
        if(element.addEventListener){//DOM 2级事件处理程序
            element.addEventListener(type,handler,false);//false表冒泡事件调用;true表捕获事件调用
        }else if(element.attachEvent){//IE事件处理程序
            element.attachEvent('on'+type,handler);//没有第三个参数的原因是:IE8及更早的浏览器版本只支持事件冒泡
        }else{//DOM 0级事件处理程序
            element['on'+type]=handler;
        }
    },

    /*删除事件处理程序    */
    removeHandler:function(element,type,handler){//删除事件处理程序,参数表元素、事件类型、函数
        if(element.removeEventListener){//DOM 2级事件处理程序
            element.removeEventListener(type,handler,false);//false表冒泡事件调用;true表捕获事件调用
        }else if(element.detachEvent){//IE事件处理程序
            element.detachEvent('on'+type,handler);//没有第三个参数的原因是:IE8及更早的浏览器版本只支持事件冒泡
        }else{//DOM 0级事件处理程序
            element['on'+type]=null;
        }
    },

 
    
    getEvent:function(event){
        return event?event:window.event;
    },

    getTarget:function(event){
        return target || srcElement;
    },

    stopPro:function(event){
        if(event.stopPropagation){
            event.stopPropagation();
        }else{
            event.cancelBubble=true;
        }
    },

    preventDef:function(event){
        if(event.preventDefault){
            event.preventDefault();
        }else{
            event.returnValue=false;
        }
    },
}
复制代码

JS中级篇

1、图片的懒加载和预加载

  • 预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染。 为什么要使用预加载:在网页加载之前,对一些主要内容进行加载,以提供用户更好的体验,减少等待时间。否则,如果一个页面的内容过于庞大,会出现留白。

解决页面留白的方案:

  • 1).预加载

  • 2).使用svg站位图片,将一些结构快速搭建起来,等待请求的数据来了之后,替换当前的占位符

实现预加载的方法:
		1.使用html标签
		2.使用Image对象
		3.使用XMLHTTPRequest对像,但会精细控制预加载过程

复制代码
  • 懒加载:懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。

两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。 懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。

2、懒加载怎么实现

  • 场景:一个页面中很多图片,但是首屏只出现几张,这时如果一次性把图片都加载出来会影响性能。这时可以使用懒加载,页面滚动到可视区在加载。优化首屏加载。
  • 实现:img标签src属性为空,给一个data-xx属性,里面存放图片真实地址,当页面滚动直至此图片出现在可视区域时,用js取到该图片的data-xx的值赋给src
  • 优点:页面加载速度快,减轻服务器压力、节约流量,用户体验好。

3、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
复制代码

4、 js拖拽功能的实现

首先是三个事件,分别是mousedown,mousemove,mouseup 当鼠标点击按下的时候,需要一个tag标识此时已经按下,可以执行mousemove里面的具体方法。

clientX,clientY标识的是鼠标的坐标,分别标识横坐标和纵坐标,并且我们用offsetX和offsetY来表示元素的元素的初始坐标,移动的举例应该是:鼠标移动时候的坐标-鼠标按下去时候的坐标。

也就是说定位信息为:

  • 鼠标移动时候的坐标-鼠标按下去时候的坐标+元素初始情况下的offetLeft.

  • 还有一点也是原理性的东西,也就是拖拽的同时是绝对定位,我们改变的是绝对定位条件下的left 以及top等等值。

5、异步加载js的方法

  • defer:只支持IE如果您的脚本不会改变文档的内容,可将 defer属性加入到script标签中, 以便加快处理文档的速度。因为浏览器知道它将能够安全地读取文档的剩余部分而不用执行脚本,它将推迟对脚本的解释,直到文档已经显示给用户为止。

  • async,HTML5属性仅适用于外部脚本,并且如果在IE中,同时存在defer和async,那么defer的优先级比较高,脚本将在页面完成时执行。

  • 创建script标签,插入到DOM

6、js中的垃圾回收机制

  • JS具有自动垃圾收集的机制

  • JS的内存生命周期(变量的生命)

    • 1).分配你所需要的空间 var a = 20
    • 2).使用分配带的内存(读写) alert(a + 10)
    • 3).不适用的时候,释放内存空间 a = null
  • JS的垃圾收集器每隔固定的时间就执行一次释放操作,通用的是通过标记清除的算法

标记清除算法:js最常见的垃圾回收方式,当变量进入执行环境的时候,比如函数中声明一个变量,垃圾回收器将他标
记为'进入环境',当变量离开(函数执行完后),就其标记为'离开环境'。垃圾回收器会在运行的时候给存储在内存中
的所有变量加上标记,然后去掉环境中的变量以及被环境中该变量所引用的变量(闭包)。在这些完成之后仍存在标记
的就是要删除的变量了
复制代码

7、内存泄露

  • 意外的全局变量: 无法被回收
  • 定时器: 未被正确关闭,导致所引用的外部变量无法被释放
  • 事件监听: 没有正确销毁 (低版本浏览器可能出现)
  • 闭包: 会导致父级中的变量无法被释放
  • dom引用: dom 元素被删除时,内存中的引用未被正确清空

8、防抖与节流

防抖与节流函数是一种最常用的 高频触发优化方式,能对性能有较大的帮助。

8.1、防抖

  • 定义: 合并事件且不会去触发事件,当一定时间内没有触发这个事件时,才真正去触发事件。

  • 原理:对处理函数进行延时操作,若设定的延时到来之前,再次触发事件,则清除上一次的延时操作定时器,重新定时。

  • 场景: keydown事件上验证用户名,用户输入,只需再输入完成后做一次输入校验即可。

function debounce(fn, wait, immediate) {
    let timer = null

    return function() {
        let args = arguments
        let context = this

        if (immediate && !timer) {
            fn.apply(context, args)
        }

        if (timer) clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(context, args)
        }, wait)
    }
}
复制代码

8.2、节流

  • 定义: 持续触发事件时,合并一定时间内的事件,在间隔一定时间之后再真正触发事件。每间隔一段时间触发一次。

  • 原理:对处理函数进行延时操作,若设定的延时到来之前,再次触发事件,则清除上一次的延时操作定时器,重新定时。

  • 场景: resize改变布局时,onscroll滚动加载下面的图片时。

  • 实现:

方法一:使用时间戳。

当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为0),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。

缺陷:第一次事件会立即执行,停止触发后没办法再激活事件。

function throttle(fn, interval) {
var previousTime = +new Date()

    return function () {
        var that = this
        var args = arguments
        var now = +new Date()
        if (now - previousTime >= interval) {
            previousTime = now
            fn.apply(that, args)
        }
   }
}
复制代码

方法二:使用定时器

当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。

缺陷:第一次事件会在n秒后执行,停止触发后依然会再执行一次事件。

function throttle(fn, interval) {
    var timer
    return function (){
        var that = this
        var args = arguments

   if(!timer){
        timer = setTimeout(function () {
            fn.apply(that, args)
            timer = null
         }, interval)
        }
    }
}
复制代码

方法三:优化

鼠标移入能立刻执行,停止触发的时候还能再执行一次。

var throttle = function(func,delay){
    var timer = null;
    var startTime = Date.now();

    return function(){
        var curTime = Date.now();
        var remaining = delay-(curTime-startTime);
        var context = this;
        var args = arguments;

        clearTimeout(timer);
        if(remaining<=0){
            func.apply(context,args);
            startTime = Date.now();
        }else{
            timer = setTimeout(func,remaining);
        }
    }
}

复制代码

9、函数柯里化

在一个函数中,首先填充几个参数,然后再返回一个新的函数的技术,称为函数的柯里化

通常可用于在不侵入函数的前提下,为函数 预置通用参数,供多次重复调用。

const add = function add(x) {
	return function (y) {
		return x + y
	}
}

const add1 = add(1)

add1(2) === 3
add1(20) === 21

复制代码

10、高阶函数是什么,怎么去写一个高阶函数

高阶函数英文叫 Higher-order function,它的定义很简单,就是至少满足下列一个条件的函数:

  • 接受一个或多个函数作为输入
  • 输出一个函数

也就是说高阶函数是对其他函数进行操作的函数,可以将它们作为参数传递,或者是返回它们。

简单来说,高阶函数是一个接收函数作为参数传递或者将函数作为返回值输出的函数。

高阶函数:参数值为函数或者返回值为函数。例如mapreducefiltersort方法就是高阶函数。

编写高阶函数,就是让函数的参数能够接收别的函数。

10.1 map()方法

map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果,原始数组不会改变。传递给 map 的回调函数(callback)接受三个参数,分别是

currentValue、index(可选)、array(可选),除了 callback 之外还可以接受 this 值(可选),用于执行 callback 函数时使用的this 值。
复制代码

来个简单的例子方便理解,现在有一个数组 [1, 2, 3, 4],我们想要生成一个新数组,其每个元素皆是之前数组的两倍,那么我们有下面两种使用高阶和不使用高阶函数的方式来实现。

// 木易杨
const arr1 = [1, 2, 3, 4];
const arr2 = arr1.map(item => item * 2);

console.log( arr2 );
// [2, 4, 6, 8]
console.log( arr1 );
// [1, 2, 3, 4]

复制代码

10.2、filter() 方法

filter() 方法创建一个新数组, 其包含通过提供函数实现的测试的所有元素,原始数组不会改变。接收的参数和 map 是一样的,其返回值是一个新数组、由通过测试的所有元素组成,如果没有任何数组元素通过测试,则返回空数组。

来个例子介绍下,现在有一个数组 [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4],我们想要生成一个新数组,这个数组要求没有重复的内容,即为去重。

const arr1 = [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4];
const arr2 = arr1.filter( (element, index, self) => {
    return self.indexOf( element ) === index;
});

console.log( arr2 );
// [1, 2, 3, 5, 4]
console.log( arr1 );
// [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4]

复制代码

10.3、reduce() 方法

reduce() 方法对数组中的每个元素执行一个提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

reduce 的回调函数(callback)接受四个参数,分别是累加器 accumulator、currentValue、currentIndex(可选)、array(可选),除了 callback 之外还可以接受初始值 initialValue 值(可选)。
复制代码
  • 如果没有提供 initialValue,那么第一次调用callback函数时,accumulator 使用原数组中的第一个元素,currentValue 即是数组中的第二个元素。 在没有初始值的空数组上调用 reduce 将报错。

  • 如果提供了 initialValue,那么将作为第一次调用 callback 函数时的第一个参数的值,即 accumulatorcurrentValue使用原数组中的第一个元素。

来个简单的例子介绍下,现在有一个数组 [0, 1, 2, 3, 4],需要计算数组元素的和,需求比较简单,来看下代码实现。

无 initialValue 值

const arr = [0, 1, 2, 3, 4];
let sum = arr.reduce((accumulator, currentValue, currentIndex, array) => {
  return accumulator + currentValue;
});

console.log( sum );
// 10
console.log( arr );
// [0, 1, 2, 3, 4]

上面是没有 initialValue 的情况,代码的执行过程如下,callback 总共调用四次。
复制代码

有 initialValue 值

我们再来看下有 initialValue 的情况,假设 initialValue 值为 10,我们看下代码。

const arr = [0, 1, 2, 3, 4];
let sum = arr.reduce((accumulator, currentValue, currentIndex, array) => {
  return accumulator + currentValue;
}, 10);

console.log( sum );
// 20
console.log( arr );
// [0, 1, 2, 3, 4]
复制代码代码的执行过程如下所示,callback 总共调用五次。
复制代码

例题

已知如下数组:var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10]; 编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组

答案:

var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10]
// 扁平化
let flatArr = arr.flat(4)
// 去重
let disArr = Array.from(new Set(flatArr))
// 排序
let result = disArr.sort(function(a, b) {
    return a-b
})
console.log(result)
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

复制代码

11、Web Worker

现代浏览器为JavaScript创造的多线程环境。可以新建并将部分任务分配到worker线程并行运行,两个线程可 独立运行,互不干扰,可通过自带的 消息机制 相互通信。

基本用法:
// 创建 worker
const worker = new Worker('work.js');

// 向主进程推送消息
worker.postMessage('Hello World');

// 监听主进程来的消息
worker.onmessage = function (event) {
  console.log('Received message ' + event.data);
}
复制代码
  • 限制:

    • 同源限制
    • 无法使用 document / window / alert / confirm
    • 无法加载本地资源

12、怎么把es6转成es5,babel怎么工作的

  • 解析:将代码字符串解析成抽象语法树
  • 变换:对抽象语法树进行变换操作
  • 再建:根据变换后的抽象语法树再生成代码字符串

13、用过哪些设计模式

(1)单例模式

  • 定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

  • 实现方法:先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。

  • 适用场景:一个单一对象。比如:弹窗,无论点击多少次,弹窗只应该被创建一次。

(2)发布/订阅模式

  • 定义:又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

  • 场景:订阅感兴趣的专栏和公众号。

(3)策略模式

  • 定义:将一个个算法(解决方案)封装在一个个策略类中。

  • 优点:

    • 策略模式可以避免代码中的多重判断条件。
    • 策略模式很好的体现了开放-封闭原则,将一个个算法(解决方案)封装在一个个策略类中。便于切换,理解,扩展。
    • 策略中的各种算法可以重复利用在系统的各个地方,避免复制粘贴。
    • 策略模式在程序中或多或少的增加了策略类。但比堆砌在业务逻辑中要清晰明了。
    • 违反最少知识原则,必须要了解各种策略类,才能更好的在业务中应用。
  • 应用场景:根据不同的员工绩效计算不同的奖金;表单验证中的多种校验规则。

(4)代理模式

  • 定义:为一个对象提供一个代用品或占位符,以便控制对它的访问。

  • 应用场景:图片懒加载(先通过一张loading图占位,然后通过异步的方式加载图片,等图片加载好了再把完成的图片加载到img标签里面。)

(5)中介者模式

  • 定义:通过一个中介者对象,其他所有相关对象都通过该中介者对象来通信,而不是互相引用,当其中的一个对象发生改变时,只要通知中介者对象就可以。可以解除对象与对象之间的紧耦合关系。

  • 应用场景: 例如购物车需求,存在商品选择表单、颜色选择表单、购买数量表单等等,都会触发change事件,那么可以通过中介者来转发处理这些事件,实现各个事件间的解耦,仅仅维护中介者对象即可。

(6)装饰者模式

  • 定义:在不改变对象自身的基础上,在程序运行期间给对象动态的添加方法。

  • 应用场景: 有方法维持不变,在原有方法上再挂载其他方法来满足现有需求;函数的解耦,将函数拆分成多个可复用的函数,再将拆分出来的函数挂载到某个函数上,实现相同的效果但增强了复用性。

14、事件队列(宏任务、微任务)

参考::这一次,彻底弄懂 JavaScript 执行机制

15、怎么用原生js实现一个轮播图,以及滚动滑动

思路,用定时器去实现,以及如何实现平滑的滚动效果。详情请看: 原生js实现轮播图

前端模块化

前端模块化就是复杂的文件编程一个一个独立的模块,比如js文件等等,分成独立的模块有利于重用(复用性)和维护(版本迭代),这样会引来模块之间相互依赖的问题,所以有了commonJS规范AMDCMD规范等等,以及用于js打包(编译等处理)的工具webpack`。

1、什么是模块?

将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

2、模块化的进化过程

1)全局function模式: 将不同的功能封装成不同的全局函数

  • 编码: 将不同的功能封装成不同的全局函数

  • 问题: 污染全局命名空间, 容易引起命名冲突或数据不安全,而且模块成员之间看不出直接关系

2)namespace模式 : 简单对象封装

  • 作用: 减少了全局变量,解决命名冲突

  • 问题: 数据不安全(外部可以直接修改模块内部的数据)

3)IIFE模式:匿名函数自调用(闭包)

  • 作用: 数据是私有的, 外部只能通过暴露的方法操作

  • 编码: 将数据和行为封装到一个函数内部, 通过给window添加属性来向外暴露接口

  • 问题: 如果当前这个模块依赖另一个模块怎么办?

  • 解决:IIFE模式增强 : 引入依赖

3、模块化的好处

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

4、引入多个script标签后出现出现问题

请求过多

首先我们要依赖多个模块,那样就会发送多个请求,导致请求过多
复制代码

依赖模糊

我们不知道他们的具体依赖关系是什么,也就是说很容易因为不了解他们之间的依赖关系导致加载先后顺序出错。
复制代码

难以维护

以上两种原因就导致了很难维护,很可能出现牵一发而动全身的情况导致项目出现严重的问题。
复制代码

模块化固然有多个好处,然而一个页面需要引入多个js文件,就会出现以上这些问题。而这些问题可以通过模块化规范来解决,下面介绍开发中最流行的commonjs, AMD, ES6, CMD规范。

5、模块化规范

  • CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。
  • AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
  • CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重
  • ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJSAMD 规范,成为浏览器和服务器通用的模块解决方案。
  • require与import的区别

    • require支持 动态导入,import不支持,正在提案 (babel 下可支持)
    • require是 同步 导入,import属于 异步 导入
    • require是 值拷贝,导出值变化不会影响导入值;import指向 内存地址,导入值会随导出值而变化

6、MVC和MVVM的区别

  • Model用于封装和应用程序的业务逻辑相关的数据以及对数据的处理方法;
  • View作为视图层,主要负责数据的展示;
  • Controller定义用户界面对用户输入的响应方式,它连接模型和视图,用于控制应用程序的流程,处理用户的行为和数据上的改变。

MVC将响应机制封装在controller对象中,当用户和你的应用产生交互时,控制器中的事件触发器就开始工作了。

MVVMViewModel的同步逻辑自动化了。以前Controller负责的View和Model同步不再手动地进行操作,而是交给框架所提供的数据绑定功能进行负责,只需要告诉它View显示的数据对应的是Model哪一部分即可。也就是双向数据绑定,就是View的变化能实时让Model发生变化,而Model的变化也能实时更新到View

参考: 浅析前端开发中的 MVC/MVP/MVVM 模式

7、async、await 优缺点

asyncawait相比直接使用 Promise来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码。

缺点在于滥用await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。

下面来看一个使用 await 的代码。

var a = 0
var b = async () => {
  a = a + await 10
  console.log('2', a) // -> '2' 10
  a = (await 10) + a
  console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1

复制代码

首先函数b先执行,在执行到await 10之前变量 a 还是0,因为在 await内部实现了 generatorsgenerators 会保留堆栈中东西,所以这时候 a = 0被保存了下来

因为 await是异步操作,遇到await就会立即返回一个pending状态的Promise对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,所以会先执行 console.log('1', a)

这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候a = 10 然后后面就是常规执行代码了

推荐:

浪里行舟的 前端模块化详解(完整版)

subwaydown的 前端模块化:CommonJS,AMD,CMD,ES6

6、前端打包工具

1)基于任务运行的工具:

Grunt、Gulp
复制代码

它们会自动执行指定的任务,就像流水线,把资源放上去然后通过不同插件进行加工,它们包含活跃的社区,丰富的插件,能方便的打造各种工作流。

2)基于模块化打包的工具:

Browserify、Webpack、rollup.js
复制代码

有过 Node.js 开发经历的应该对模块很熟悉,需要引用组件直接一个 require 就 OK,这类工具就是这个模式,还可以实现按需加载、异步加载模块。

3)整合型工具:

Yeoman、FIS、jdf、Athena、cooking、weflow
复制代码

使用了多种技术栈实现的脚手架工具,好处是即开即用,缺点就是它们约束了技术选型,并且学习成本相对较高。

7、webpack

WebPack 是一个模块打包工具,你可以使用WebPack管理你的模块依赖,并编绎输出模块们所需的静态文件。它能够很好地管理、打包Web开发中所用到的HTMLJavaScriptCSS以及各种静态文件(图片、字体等),让开发过程更加高效。对于不同类型的资源,webpack有对应的模块加载器。webpack模块打包器会分析模块间的依赖关系,最后 生成了优化且合并后的静态资源。

webpack的两大特色:

  • 1.code splitting(可以自动完成)

  • 2.loader 可以处理各种类型的静态文件,并且支持串联操作

webpack 是以commonJS的形式来书写脚本滴,但对AMD/CMD 的支持也很全面,方便旧项目进行代码迁移。

webpack具有requireJsbrowserify的功能,但仍有很多自己的新特性

    1. CommonJSAMDES6的语法做了兼容
    1. jscss、图片等资源文件都支持打包
    1. 串联式模块加载器以及插件机制,让其具有更好的灵活性和扩展性,例如提供对CoffeeScriptES6的支持
    1. 有独立的配置文件webpack.config.js
    1. 可以将代码切割成不同的chunk,实现按需加载,降低了初始化时间
    1. 支持 SourceUrlsSourceMaps,易于调试
    1. 具有强大的Plugin接口,大多是内部插件,使用起来比较灵活
    1. webpack 使用异步 IO 并具有多级缓存。这使得 webpack 很快且在增量编译上更加快

8、webpack 如何进行打包优化

从提取公共模块,区分开发环境,移除重复不必要的 css 和 js 文件等方面说。

推荐arlendp2012的文章 Webpack打包优化

9、 webpack 打包原理

  • 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler

  • 编译:从 Entry 发出,针对每个Module串行调用对应的Loader去翻译文件内容,再找到该Module 依赖的 Module,递归地进行编译处理。

  • 输出:对编译后的Module组合成 Chunk,把 Chunk转换成文件,输出到文件系统。

推荐whjin的文章 webpack原理

10、webpack的作用

  • 依赖管理:方便引用第三方模块、让模块更容易复用,避免全局注入导致的冲突、避免重复加载或者加载不需要的模块。会一层一层的读取依赖的模块,添加不同的入口;同时,不会重复打包依赖的模块。
  • 合并代码:把各个分散的模块集中打包成大文件,减少HTTP的请求链接数,配合UglifyJS(压缩代码)可以减少、优化代码的体积。
  • 各路插件:统一处理引入的插件,babel编译ES6文件,TypeScript,eslint可以检查编译期的错误。

一句话总结:webpack的作用就是处理依赖,模块化,打包压缩文件,管理插件。

参考文章:

刷前端面试题的话,收藏这一篇就够了!

2017下半年掘金日报优质文章合集:前端篇

2018春招前端面试: 闯关记(精排精校) | 掘金技术征文

「中高级前端面试」JavaScript手写代码无敌秘籍

转载于:https://juejin.im/post/5cde77c151882526015c3d11

你可能感兴趣的:(javascript,前端,webpack)