前端

142 阅读26分钟

javascript

1、说说JavaScript中的数据类型?以及区别?

JS 中有六种简单数据类型:undefined、null、boolean、string、number、symbol,以及引用类型:object
常见的类型转换有:

1.强制转换(显示转换)

  • Number()
    将任意类型的值转化为数值
    Number转换的时候是很严格的,只要有一个字符无法转成数值,整个字符串就会被转为NaN
Number(324) // 324

// 字符串:如果可以被解析为数值,则转换为相应的数值
Number('324') // 324

// 字符串:如果不可以被解析为数值,返回 NaN
Number('324abc') // NaN

// 空字符串转为0
Number('') // 0

// 布尔值:true 转成 1,false 转成 0
Number(true) // 1
Number(false) // 0

// undefined:转成 NaN
Number(undefined) // NaN

// null:转成0
Number(null) // 0

// 对象:通常转换成NaN(除了只包含单个数值的数组)
Number({a: 1}) // NaN
Number([1, 2, 3]) // NaN
Number([5]) // 5
  • parseInt() parseInt相比Number,就没有那么严格了,parseInt函数逐个解析字符,遇到不能转换的字符就停下来
parseInt('32a3') //32
  • String() 可以将任意类型的值转化成字符串

68747470733a2f2f7374617469632e7675652d6a732e636f6d2f34386464386562302d363639322d313165622d383566362d3666616337376330633962332e706e67.png

  • Boolean() 可以将任意类型的值转为布尔值

68747470733a2f2f7374617469632e7675652d6a732e636f6d2f35336264616431302d363639322d313165622d616239302d6439616538313462323430642e706e67.png

Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false
Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true

2.自动转换(隐式转换)

  • 自动转换为布尔值 undefined、null、false、+0、-0、NaN、""、- 自动转换成字符串,除了以上几种会被转化成false,其他都会被转化成true
  • 自动转换成字符串
'5' + 1 // '51'
'5' + true // "5true"
'5' + false // "5false"
'5' + {} // "5[object Object]"
'5' + [] // "5"
'5' + function (){} // "5function (){}"
'5' + undefined // "5undefined"
'5' + null // "5null"
  • 自动转换成数值
'5' - '2' // 3
'5' * '2' // 10
true - 1  // 0
false - 1 // -1
'1' - 1   // 0
'5' * []    // 0
false / '5' // 0
'abc' - 1   // NaN
null + 1 // 1
undefined + 1 // NaN

null转为数值时,值为0 。undefined转为数值时,值为NaN

2、Javscript数组的常用方法有哪些

  • 操作方法

增:

  • push() push() 方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度
let colors = []; // 创建一个数组
let count = colors.push("red", "green"); // 推入两项
console.log(count) // 2
  • unshift() unshift()在数组开头添加任意多个值,然后返回新的数组长度
let colors = new Array(); // 创建一个数组
let count = colors.unshift("red", "green"); // 从数组开头推入两项
alert(count); // 2
  • splice() 传入三个参数,分别是开始位置、0(要删除的元素数量)、插入的元素,返回空数组
let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 0, "yellow", "orange")
console.log(colors) // red,yellow,orange,green,blue
console.log(removed) // []
  • concat() 首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组
let colors = ["red", "green", "blue"];
let colors2 = colors.concat("yellow", ["black", "brown"]);
console.log(colors); // ["red", "green","blue"]
console.log(colors2); // ["red", "green", "blue", "yellow", "black", "brown"]

删:

  • pop() pop() 方法用于删除数组的最后一项,同时减少数组的 length 值,返回被删除的项
let colors = ["red", "green"]
let item = colors.pop(); // 取得最后一项
console.log(item) // green
console.log(colors.length) // 1
  • shift() shift()方法用于删除数组的第一项,同时减少数组的 length 值,返回被删除的项
let colors = ["red", "green"]
let item = colors.shift(); // 取得第一项
console.log(item) // red
console.log(colors.length) // 1
  • splice() 传入两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数组
let colors = ["red", "green", "blue"];
let removed = colors.splice(0,1); // 删除第一项
console.log(colors); // green,blue
console.log(removed); // red,只有一个元素的数组

- slice() slice() 用于创建一个包含原有数组中一个或多个元素的新数组,不会影响原始数组

let colors = ["red", "green", "blue", "yellow", "purple"];
let colors2 = colors.slice(1);
let colors3 = colors.slice(1, 4);
console.log(colors)   // red,green,blue,yellow,purple
console.log(colors2); // green,blue,yellow,purple
console.log(colors3); // green,blue,yellow

改:

  • splice() 传入三个参数,分别是开始位置,要删除元素的数量,要插入的任意多个元素,返回删除元素的数组,对原数组产生影响
let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 1, "red", "purple"); // 插入两个值,删除一个元素
console.log(colors); // red,red,purple,blue
console.log(removed); // green,只有一个元素的数组

查:

  • indexOf() 返回要查找的元素在数组中的位置,如果没找到则返回-1
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.indexOf(4) // 3
  • includes() 返回要查找的元素在数组中的位置,找到返回true,否则false
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.includes(4) // true
  • find() 返回第一个匹配的元素
const people = [
    {
        name: "Matt",
        age: 27
    },
    {
        name: "Nicholas",
        age: 29
    }
];
people.find((element, index, array) => element.age < 28) // // {name: "Matt", age: 27}
  • 排序方法

  • reverse() 顾名思义,将数组元素方向排列
let values = [1, 2, 3, 4, 5];
values.reverse();
alert(values); // 5,4,3,2,1
  • sort() sort()方法接受一个比较函数,用于判断哪个值应该排在前面
function compare(value1, value2) {
    if (value1 < value2) {
        return -1;
    } else if (value1 > value2) {
        return 1;
    } else {
        return 0;
    }
}
let values = [0, 1, 5, 10, 15];
values.sort(compare);
alert(values); // 0,1,5,10,15
  • 转换方法

  • join() join() 方法接收一个参数,即字符串分隔符,返回包含所有项的字符串
let colors = ["red", "green", "blue"];
alert(colors.join(",")); // red,green,blue
alert(colors.join("||")); // red||green||blue
  • 迭代方法

  • some() 对数组每一项都运行传入的函数,如果有一项函数返回 true ,则这个方法返回 true
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let someResult = numbers.every((item, index, array) => item > 2);
console.log(someResult) // true
  • every() 对数组每一项都运行传入的函数,如果对每一项函数都返回 true ,则这个方法返回 true
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let everyResult = numbers.every((item, index, array) => item > 2);
console.log(everyResult) // false
  • forEach() 对数组每一项都运行传入的函数,没有返回值
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.forEach((item, index, array) => {
    // 执行某些操作
});
  • filter() 对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let filterResult = numbers.filter((item, index, array) => item > 2);
console.log(filterResult); // 3,4,5,4,3
  • map() 对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let mapResult = numbers.map((item, index, array) => item * 2);
console.log(mapResult) // 2,4,6,8,10,8,6,4,2

3、Javascript字符串的常用方法有哪些?

1.操作方法

这里增的意思并不是说直接增添内容,而是创建字符串的一个副本,再进行操作。除了常用+以及${}进行字符串拼接之外,还可通过concat

  • concat 用于将一个或多个字符串拼接成一个新字符串
let stringValue = "hello ";
let result = stringValue.concat("world");
console.log(result); // "hello world"
console.log(stringValue); // "hello

这里的删的意思并不是说删除原字符串的内容,而是创建字符串的一个副本,再进行操作

  • slice()
  • substr()
  • substring() 这三个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数。
let stringValue = "hello world";
console.log(stringValue.slice(3)); // "lo world"
console.log(stringValue.substring(3)); // "lo world"
console.log(stringValue.substr(3)); // "lo world"
console.log(stringValue.slice(3, 7)); // "lo w"
console.log(stringValue.substring(3,7)); // "lo w"
console.log(stringValue.substr(3, 7)); // "lo worl"

这里改的意思也不是改变原字符串,而是创建字符串的一个副本,再进行操作

  • trim()trimLeft()trimRight() 删除前、后或前后所有空格符,再返回新的字符串
let stringValue = " hello world ";
let trimmedStringValue = stringValue.trim();
console.log(stringValue); // " hello world "
console.log(trimmedStringValue); // "hello world"
  • repeat() 接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果
let stringValue = "na ";
let copyResult = stringValue.repeat(2) // na na 
  • padEnd() 复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件
let stringValue = "foo";
console.log(stringValue.padStart(6)); // " foo"
console.log(stringValue.padStart(9, ".")); // "......foo"
  • toLowerCase()toUpperCase() 大小写转化
let stringValue = "hello world";
console.log(stringValue.toUpperCase()); // "HELLO WORLD"
console.log(stringValue.toLowerCase()); // "hello world"
  • chatAt() 返回给定索引位置的字符,由传给方法的整数参数指定
let message = "abcde";
console.log(message.charAt(2)); // "c"
  • indexOf() 从字符串开头去搜索传入的字符串,并返回位置(如果没找到,则返回 -1 )
let stringValue = "hello world";
console.log(stringValue.indexOf("o")); // 4
  • startWith()includes() 从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值
let message = "foobarbaz";
console.log(message.startsWith("foo")); // true
console.log(message.startsWith("bar")); // false
console.log(message.includes("bar")); // true
console.log(message.includes("qux")); // false

2.转换方法

  • split() 把字符串按照指定的分割符,拆分成数组中的每一项
let str = "12+23+34"
let arr = str.split("+") // [12,23,34]

3.模板匹配方法

  • match() 接收一个参数,可以是一个正则表达式字符串,也可以是一个RegExp对象,返回数组
let text = "cat, bat, sat, fat";
let pattern = /.at/;
let matches = text.match(pattern);
console.log(matches[0]); // "cat"
  • search() 接收一个参数,可以是一个正则表达式字符串,也可以是一个RegExp对象,找到则返回匹配索引,否则返回 -1
let text = "cat, bat, sat, fat";
let pos = text.search(/at/);
console.log(pos); // 1
  • replace() 接收两个参数,第一个参数为匹配的内容,第二个参数为替换的元素(可用函数)
let text = "cat, bat, sat, fat";
let result = text.replace("at", "ond");
console.log(result); // "cond, bat, sat, fat"

4、说说你对闭包的理解?闭包使用场景

1.理解

闭包让你可以在一个内层函数中访问到其外层函数的作用域
在JavaScript中,函数即闭包,只有函数才会产生作用域。

2.使用场景

  • 创建私有变量
  • 延长变量的生命周期 一般函数的词法环境在函数返回后就会被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但是创建时所在词法环境依然存在,以达到延长变量的生命周期的目的

3.优缺点

  • 优点:
    可以避免全局变量的污染
  • 缺点: 闭包会常驻内存,增加内存的使用量,使用不当很容易造成内存泄漏。

4.特性

  • 函数嵌套函数
  • 在函数内部可以引用外部的参数和变量
  • 参数和变量不会以垃圾回收机制回收

5、说说你对作用域链的理解

  • 作用域

作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合
换句话说,作用域决定了代码区块中变量和其他资源的可见性

1.全局作用域

任何不在函数中或者大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问

// 全局变量
var greeting = 'Hello World!';
function greet() {
  console.log(greeting);
}
// 打印 'Hello World!'
greet();

2.函数作用域

函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就是在一个函数作用域下面。这些变量只能在函数背部访问,不能在函数以为去访问

function greet() {
  var greeting = 'Hello World!';
  console.log(greeting);
}
// 打印 'Hello World!'
greet();
// 报错: Uncaught ReferenceError: greeting is not defined
console.log(greeting);

3.块级作用域

ES6引入了let和const关键字,和var关键字不同,在大括号中使用let和const声明的变量存在于块级作用域中。在大括号之外不能访问这些变量

{
  // 块级作用域中的变量
  let greeting = 'Hello World!';
  var lang = 'English';
  console.log(greeting); // Prints 'Hello World!'
}
// 变量 'English'
console.log(lang);
// 报错:Uncaught ReferenceError: greeting is not defined
console.log(greeting);
  • 词法作用域

词法作用域,又叫静态作用域,变量被创建时就确定好了,而非执行阶段确定的。也就是说我们写好代码时它的作用域就确定了,JavaScript 遵循的就是词法作用域

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

由于JavaScript遵循词法作用域,相同层级的 foo 和 bar 就没有办法访问到彼此块作用域中的变量,所以输出2

  • 作用域链

当在Javascript中使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域

var sex = '男';
function person() {
    var name = '张三';
    function student() {
        var age = 18;
        console.log(name); // 张三
        console.log(sex); // 男 
    }
    student();
    console.log(age); // Uncaught ReferenceError: age is not defined
}
person();

上述代码主要主要做了以下工作:

  • student函数内部属于最内层作用域,找不到name,向上一层作用域person函数内部找,找到了输出“张三”
  • student内部输出cat时找不到,向上一层作用域person函数找,还找不到继续向上一层找,即全局作用域,找到了输出“男”
  • 在person函数内部输出age时找不到,向上一层作用域找,即全局作用域,还是找不到则报错

6、JavaScript原型、原型链 ? 有什么特点?

1.原型

每个函数都有一个特殊的属性叫作原型prototype
原型对象有一个自有属性constructor,这个属性指向该函数,如下图关系展示

68747470733a2f2f7374617469632e7675652d6a732e636f6d2f35366438373235302d373235652d313165622d616239302d6439616538313462323430642e706e67.png

2.原型链

原型对象可可能拥有原型,并从中继承方法和属性,一层一层,以此类推,这种关系常被称为原型链,他解释了为何一个对象会拥有定义在其他对象中的属性和方法
在对象实例和它的构造器之间建立一个链接(它是__protot__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法

function Person(name) {
    this.name = name;
    this.age = 18;
    this.sayName = function() {
        console.log(this.name);
    }
}
// 第二步 创建实例
var person = new Person('person')

68747470733a2f2f7374617469632e7675652d6a732e636f6d2f36303832356161302d373235652d313165622d383566362d3666616337376330633962332e706e67.png

分析:

  • 构造函数Person存在原型对象Person.prototype
  • 构造函数生成实例对象personperson__proto__指向构造函数Person原型对象
  • Person.prototype.__proto__ 指向内置对象,因为 Person.prototype 是个对象,默认是由 Object 函数作为类创建的,而 Object.prototype 为内置对象
  • Person.__proto__ 指向内置匿名函数 anonymous,因为 Person 是个函数对象,默认由 Function 作为类创建
  • Function.prototypeFunction.__proto__ 同时指向内置匿名函数 anonymous,这样原型链的终点就是 null

3.总结

__proto__作为不同对象之间的桥梁,用来指向创建它的构造函数的原型对象的

68747470733a2f2f7374617469632e7675652d6a732e636f6d2f36613734323136302d373235652d313165622d616239302d6439616538313462323430642e706e67.png

每个对象的__proto__都是指向它的构造函数的原型对象prototype的

person1.__proto__ === Person.prototype

构造函数是一个函数对象,是通过 Function 构造器产生的

Person.__proto__ === Function.prototype

原型对象本身是一个普通对象,而普通对象的构造函数都是Object

Person.prototype.__proto__ === Object.prototype

刚刚上面说了,所有的构造器都是函数对象,函数对象都是 Function 构造产生的

Object.__proto__ === Function.prototype

Object 的原型对象也有__proto__属性指向null,null是原型链的顶端

Object.prototype.__proto__ === null

结论:

  • 一切对象都是继承自Object对象,Object 对象直接继承根源对象 null
  • 一切的函数对象(包括 Object 对象),都是继承自 Function 对象
  • Object 对象直接继承自 Function 对象
  • Function对象的__proto__会指向自己的原型对象,最终还是继承自Object对象

7、Javascript如何实现继承?

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

特点

  • 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
  • 父类新增原型方法/原型属性,子类都能访问到
  • 简单,易于实现
    缺点
  • 要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放在构造器中
  • 无法实现多继承
  • 来自原型对象的所有属性被所有实例共享
  • 创建子类实例时,无法向父类构造函数传参
  • 创建 Child 实例的时候,不能传参

2.构造函数继承(借助call)

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

function Parent(){
    this.name = 'parent1';
}

Parent.prototype.getName = function () {
    return this.name;
}

function Child(){
    Parent1.call(this);
    this.type = 'child'
}

let child = new Child();
console.log(child);  // 没问题
console.log(child.getName());  // 会报错

优点:

  • 解决了原型链继承中,子类实例共享父类引用属性的问题
  • 创建子类实例时,可以像父类传递参数
  • 可以实现多继承(call多个父类对象) 缺点:
  • 实例并不是父类的实例,只是子类的实例
  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

3.组合继承

function Parent3 () {
    this.name = 'parent3';
    this.play = [1, 2, 3];
}

Parent3.prototype.getName = function () {
    return this.name;
}
function Child3() {
    // 第二次调用 Parent3()
    Parent3.call(this);
    this.type = 'child3';
}

// 第一次调用 Parent3()
Child3.prototype = new Parent3();
// 手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play);  // 不互相影响
console.log(s3.getName()); // 正常输出'parent3'

这种方式看起来就没什么问题,方式一和方式二的问题都解决了,但是从上面代码我们也可以看到 Parent3 执行了两次,造成了多构造一次的性能开销

4.原型式继承

这里主要借助Object.create方法实现普通对象的继承
这种继承方式的缺点也很明显,因为Object.create 方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能

let parent4 = {
    name: "parent4",
    friends: ["p1", "p2", "p3"],
    getName: function() {
      return this.name;
    }
  };

  let person4 = Object.create(parent4);
  person4.name = "tom";
  person4.friends.push("jerry");

  let person5 = Object.create(parent4);
  person5.friends.push("lucy");

  console.log(person4.name); // tom
  console.log(person4.name === person4.getName()); // true
  console.log(person5.name); // parent4
  console.log(person4.friends); // ["p1", "p2", "p3","jerry","lucy"]
  console.log(person5.friends); // ["p1", "p2", "p3","jerry","lucy"]

5.寄生继承

寄生式继承在上面继承基础上进行优化,利用这个浅拷贝的能力再进行增强,添加一些方法

let parent5 = {
    name: "parent5",
    friends: ["p1", "p2", "p3"],
    getName: function() {
        return this.name;
    }
};

function clone(original) {
    let clone = Object.create(original);
    clone.getFriends = function() {
        return this.friends;
    };
    return clone;
}

let person5 = clone(parent5);

console.log(person5.getName()); // parent5
console.log(person5.getFriends()); // ["p1", "p2", "p3"]

6.寄生组合式继承

function Parent(name) {
  this.name = name
}
Parent.prototype.getName = function () {
  console.log(this.name + '被调用')
}
function Child() {
  Parent.apply(this, arguments)
}
// Child.prototype = Parent.prototype  不可用(一旦更改Child.prototype ,Parent.prototype也会被修改)
Child.prototype = Object.create(Parent.prototype)
// Object.create(Parent.prototype)实现过程
// let temFunc = function () {}
// temFunc.prototype = Parent.prototype
// Child.prototype = new temFunc()

Child.prototype.constructor = Child
const c1 = new Child('c1')
console.log(c1)

7.总结

68747470733a2f2f7374617469632e7675652d6a732e636f6d2f30646637343730302d373331632d313165622d616239302d6439616538313462323430642e706e67.png

通过Object.create 来划分不同的继承方式,最后的寄生式组合继承方式是通过组合继承改造之后的最优继承方式,而 extends 的语法糖和寄生组合继承的方式基本类似

8、new 关键字到底做了什么?

image.png

  1. ⼀个继承⾃ Player.prototype 的新对象 whitePlayer 被创建
  2. whitePlayer.proto 指向 Player.prototype,即 whitePlayer.proto = Player.prototype
  3. 将 this 指向新创建的对象 whitePlayer
  4. 返回新对象
  • 如果构造函数没有显式返回值,则返回 this
  • 如果构造函数有显式返回值,是基本类型,⽐如 number,string,boolean, 那么还是返回 this
  • 如果构造函数有显式返回值,是对象类型,⽐如{ a: 1 }, 则返回这个对象{ a: 1 }

手写实现new函数

// 1. ⽤new Object() 的⽅式新建了⼀个对象 obj 
// 2. 取出第⼀个参数,就是我们要传⼊的构造函数。此外因为 shift 会修改原数组,所以 arguments 会被去除第⼀个参数 
// 3. 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性 
// 4. 使⽤ apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的 属性 
// 5. 返回 obj 
function objectFactory() { 
    let obj = new Object();
    let Constructor = [].shift.call(arguments);
    obj.__proto__ = Constructor.prototype; 
    let ret = Constructor.apply(obj, arguments); 
    return typeof ret === "object" ? ret : obj;
}

9、谈谈this对象的理解

1.定义

this关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象

function baz() {
    // 当前调用栈是:baz
    // 因此,当前调用位置是全局作用域
    
    console.log( "baz" );
    bar(); // <-- bar的调用位置
}

function bar() {
    // 当前调用栈是:baz --> bar
    // 因此,当前调用位置在baz中
    
    console.log( "bar" );
    foo(); // <-- foo的调用位置
}

function foo() {
    // 当前调用栈是:baz --> bar --> foo
    // 因此,当前调用位置在bar中
    
    console.log( "foo" );
}

baz(); // <-- baz的调用位置

2.绑定规则

1.默认绑定

全局环境中定义person函数,内部使用this关键字

var name = 'Jenny';
function person() {
    return this.name;
}
console.log(person());  //Jenny

上述输出Jenny,原因是调用函数的对象在浏览器中为window,因此this指向window,所以输出Jenny
注意:
严格模式下,不能将全局对象用于默认绑定,this会绑定到undefined,只有函数运行在非严格模式下,默认绑定才能绑定到全局对象

2.隐式绑定

函数还可以作为某个对象的方法调用,这时this就指这个上级对象

var o = {
    a:10,
    b:{
        fn:function(){
            console.log(this.a); //undefined
        }
    }
}
o.b.fn();
// this的上一级对象为b,b内部并没有a变量的定义,所以输出undefined
var o = {
    a:10,
    b:{
        a:12,
        fn:function(){
            console.log(this.a); //undefined
            console.log(this); //window
        }
    }
}
var j = o.b.fn;
j();

this永远指向的是最后调用它的对象,虽然fn是对象b的方法,但是fn赋值给j时候并没有执行,所以最终指向window

3.new绑定

通过构建函数new关键字生成一个实例对象,此时this指向这个实例对象

function test() {
 this.x = 1;
}

var obj = new test();
obj.x // 1
// 之所以能过输出1,是因为new关键字改变了this的指向

4.显示绑定

apply()、call()、bind()是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时this指的就是这第一个参数

var x = 0;
function test() {
 console.log(this.x);
}

var obj = {};
obj.x = 1;
obj.m = test;
obj.m.apply(obj) // 1

3.箭头函数

在 ES6 的语法中还提供了箭头函语法,让我们在代码书写时就能确定 this 的指向(编译时绑定)

const obj = {
  sayThis: () => {
    console.log(this);
  }
};

obj.sayThis(); // window 因为 JavaScript 没有块作用域,所以在定义 sayThis 的时候,里面的 this 就绑到 window 上去了
const globalSay = obj.sayThis;
globalSay(); // window 浏览器中的 global 对象

4.优先级

  • 隐式绑定 VS 显式绑定 (显示绑定>隐式绑定)
  • new绑定 VS 隐式绑定 (new绑定的优先级>隐式绑定)
  • new绑定 VS 显式绑定 (new绑定优先级>显式绑定)

综上,new绑定优先级 > 显示绑定优先级 > 隐式绑定优先级 > 默认绑定优先级

10、bind、call、apply 区别?如何实现一个bind?

call 、apply 、bind 作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向

1.apply

apply接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传入
改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次

function fn(...args){
    console.log(this,args);
}
let obj = {
    myname:"张三"
}

fn.apply(obj,[1,2]); // this会变成传入的obj,传入的参数必须是一个数组;
fn(1,2) // this指向window

当第一个参数为null、undefined的时候,默认指向window(在浏览器中)

fn.apply(null,[1,2]); // this指向window
fn.apply(undefined,[1,2]); // this指向window

2.call

call方法的第一个参数也是this的指向,后面传入的是一个参数列表
跟apply一样,改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次

function fn(...args){
    console.log(this,args);
}
let obj = {
    myname:"张三"
}

fn.call(obj,1,2); // this会变成传入的obj,传入的参数必须是一个数组;
fn(1,2) // this指向window

同样的,当第一个参数为null、undefined的时候,默认指向window(在浏览器中)

fn.call(null,[1,2]); // this指向window
fn.call(undefined,[1,2]); // this指向window

3.bind

bind方法和call很相似,第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)
改变this指向后不会立即执行,而是返回一个永久改变this指向的函数

function fn(...args){
    console.log(this,args);
}
let obj = {
    myname:"张三"
}

const bindFn = fn.bind(obj); // this 也会变成传入的obj ,bind不是立即执行需要执行一次
bindFn(1,2) // this指向obj
fn(1,2) // this指向window

4.小结

区别:

  • 三者都可以改变函数的this对象指向
  • 三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window
  • 三者都可以传参,但是apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入
  • bind 是返回绑定this之后的函数,apply 、call 则是立即执行

5.实现

1.修改this指向
2.动态传递参数

// 方式一:只在bind中传递函数参数
fn.bind(obj,1,2)()

// 方式二:在bind中传递函数参数,也在返回函数中传递参数
fn.bind(obj,1)(2)

3.兼容new关键字

整体实现代码如下:
Function.prototype.myBind = function (context) {
    // 判断调用对象是否为函数
    if (typeof this !== "function") {
        throw new TypeError("Error");
    }

    // 获取参数
    const args = [...arguments].slice(1),
          fn = this;

    return function Fn() {

        // 根据调用方式,传入不同绑定值
        return fn.apply(this instanceof Fn ? new fn(...arguments) : context, args.concat(...arguments)); 
    }
}

11、JavaScript中执行上下文和执行栈是什么

12、说说JavaScript中的事件模型

13、typeof 与 instanceof 区别

14、解释下什么是事件代理?应用场景?

15、ajax原理是什么?如何实现?

16、说说你对正则表达式的理解?应用场景?

17、说说你对事件循环的理解

18、DOM常见的操作有哪些?

19、说说你对BOM的理解,常见的BOM对象你了解哪些?

20、举例说明你对尾递归的理解,有哪些应用场景

21、说说 JavaScript 中内存泄漏的几种情况?

22、Javascript本地存储的方式有哪些?区别及应用场景?

23、说说你对函数式编程的理解?优缺点?

24、Javascript中如何实现函数缓存?函数缓存有哪些应用场景?

25、说说 Javascript 数字精度丢失的问题,如何解决?

26、什么是防抖和节流?有什么区别?如何实现?

27、如何判断一个元素是否在可视区域中?

28、大文件上传如何做断点续传?

29、如何实现上拉加载,下拉刷新?

30、什么是单点登录?如何实现?

31、web常见的攻击方式有哪些?如何防御?

ES6

1、说说var、let、const之间的区别

2、ES6中数组新增了哪些扩展?

3、ES6中对象新增了哪些扩展?

4、ES6中函数新增了哪些扩展?

5、ES6中新增的Set、Map两种数据结构怎么理解?

6、你是怎么理解ES6中 Promise的?使用场景?

7、怎么理解ES6中 Generator的?使用场景?

8、你是怎么理解ES6中Proxy的?使用场景?

9、你是怎么理解ES6中Module的?使用场景?

10、你是怎么理解ES6中 Decorator 的?使用场景?

vue

1、哈希路由(hash模式)和历史路由(history模式)的区别

实现:

1.改变url且不让浏览器向服务器发出请求
2.监测url的变化
3.截获url地址,并解析出需要的信息来匹配路由规则

hash能兼容到IE8,history只能兼容到IE10

区别:

hash本来是拿来做页面定位的,如果拿来做路由的话,原来的锚点功能就不能用了;
hash的传参是基于url的,如果要传递复杂的数据,会有体积的限制
而history模式不仅可以在url里放参数,还可以将数据存放在一个特定的对象中。

2、如何给SPA做SEO

下面给出基于vue的SPA如何实现SEO的三种方式:

  1. SSR服务器端渲染

讲组件或者页面通过服务器生成html,再返回给浏览器,入nuxt.js

  1. 静态化

a.一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中
b.另一种是通过web服务器的URL Rewrite的方式,他的原理是通过web服务器内部模块按一定规则将外部的URL请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的

这两种方法都达到了实现URl静态化的效果

  1. 使用Phantomjs针对爬虫处理

原理是通过Nginx配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server,再通过Phantomjs来解析完整的HTML,返回给爬虫。

image.png

3、什么是SPA?

单页面应用,即一个web项目就只有一个页面(即一个HTML文件)
就是指一个系统只加载一次资源,之后的操作交互、数据交互是通过路由、ajax来进行,页面并没有刷新
特点是加载次数少,加载以后性能较高,不利于seo,如果页面支持h5可以用h5模式+服务器路由rewrite+h5 history api去掉路由的锚点,和搜索软件优化lib进行seo优化。

4、实现一个SPA

原理:

1.监听地址栏中hash变化驱动界面变化
2.用pushsate记录浏览器的历史,驱动界面发送变化

68747470733a2f2f7374617469632e7675652d6a732e636f6d2f66633935626636302d336163362d313165622d616239302d6439616538313462323430642e706e67.png

实现:

hash模式:核心通过监听url中的hash来进行路由跳转
history模式:history模式核心借用HTML5 history api,api提供了丰富的router相关属性先了解一个几个相关的api

  • history.pushState 浏览器历史纪录添加记录
  • history.replaceState修改浏览器历史纪录中当前纪录
  • history.popState 当 history 发生变化时触发

5、单页面和多页面的区别

单页面应用:只有一张web页面的应用,是一种从web服务器加载的富客户端,单页面跳转仅刷新局部资源,公共资源(js、css等)仅需加载一次,常用于pc端官网、购物等网站。
多页面应用:多页面跳转刷新所有资源,每个公共资源(js、css等)需要选择性重新加载,常用于app或者客户端等

单页面应用多页面应用
组成一个外壳页面和多个页面片段组成多个完整页面构成
资源共用(css,js共用,只需在外壳部分加载不共用,每个页面都需要加载
刷新方式页面局部刷新或更改整个页面刷新
url模式a.com/#/pageone
a.com/#/pagetwo
a.com/#/pageone.com
a.com/#/pagetwo.com
用户体验页面片段间的切换快,用户体验良好页面切换加载缓慢,流畅度不够,用户体验比较差
转场动画容易实现无法实现
数据传递容易依赖url传参、或者cookie、localStorage等
搜索引擎优化(seo)需要单独方案,实现较为困难,不利于seo检索。可利用服务器渲染(ssr)优化实现方法简易
试用范围高要求的体验度、追求界面流畅的应用适用于追求高度支持搜索引擎的应用
开发成本较高,常需借助专业的框架较低,但页面重复代码多
维护成本相对容易相对复杂

单页面的优缺点

优点

  • 具有桌面应用的即时性、网站的可移植性和可访问性
  • 用户体验好、快,内容的改变不需要重新加载整个页面
  • 良好的前后端分离,分工更明确
    缺点
  • 不利于搜索引擎的抓取
  • 首次渲染速度相对较慢

6、v-show和v-if的区别

v-show与v-if的作用效果是相同的(不含v-else),都能控制元素在页面是否显示

<Model v-show="isShow" />  
<Model v-if="isShow" />
  • 当表达式为true的时候,都会占据页面的位置
  • 当表达式都为false时,都不会占据页面位置
    区别
  1. 控制手段不同:v-show隐藏则是为该元素添加css--display:none,dom元素依旧还在;v-if显示隐藏是将dom元素整个添加或者删除
  2. 编译过程不同:v-if切换有一个局部编译/卸载的过程,切换过程中合适的销毁和重建内部的时间监听和子组件;v-show只是简单的基于css的切换
  3. 编译条件不同:v-if是真正的田间渲染,它会确保在切换过程中条件块内的时间监听器和子组件适当的被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染
  • v-show由false变为true的时候不会触发组件的生命周期。
  • v-if由false变为true的时候,触发组件的beforeCreate、create、beforeMount、mounted钩子,由true变为false的时候触发组件的beforeDestory、destoryed方法
  1. 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗

使用场景

  • v-if与v-show都能控制dom元素在页面的显示
  • v-if相比v-show开销更大的(直接操作dom节点增加与删除)
  • 如果需要非常频繁地切换,则使用v-show较好
  • 如果在运行时条件很少改变,则使用v-if较好

7、 Vue实例挂载的过程中发生了什么

  • new vue的时候调用会调用_init方法
    • 定义$set、$get、$delete、$watch等方法
    • 定义$on、$off、$emit、$off等事件
    • 定义_update、$forceUpdate、$destory生命周期
  • 调用$mount进行页面的挂载
  • 挂载的时候主要通过mountComponent方法
  • 定义updateComponent更新函数
  • 执行render生成虚拟DOM
  • _update将虚拟DOM生成真实DOM结构,并且渲染到页面中

8、 对Vue生命周期的理解?

在vue中实例从创建到销毁的过程就是生命周期,即指的是从创建、初始化数据、编译模板、挂载DOM->渲染、更新->渲染、写在等一系列过程

vue的生命周期总共可以分为8个阶段:创建前后、载入前后、更新前后、销毁前后,以及一些特殊场景的生命周期

生命周期描述使用场景分析
beforeCreate组件实例被创建之初执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务
created组件实例已经完全创建组件初始化完毕,各种数据可以使用,常用于异步数据获取
beforeMount组件挂载之前未执行渲染、更新、dom未创建
mounted组件挂载到实例上去之后初始化结束,dom已创建,可用于获取访问数据和dom元素
beforeUpdate组件数据发生变化,更新之前更新前,用于获取更新前各种状态
updated数据更新之后更新后,所有状态已经是最新
beforeDestroy组件实例销毁之前销毁前,可用于一些定时器或者订阅的取消
destroyed组件实例销毁之后组件已经销毁,作用同上
activatedkeep-alive缓存的组件激活时
deactivatedkeep-alive缓存的组件停用时调用
errorCaptured捕获一个来自子孙组件的错误时被调用

生命周期整体流程

68747470733a2f2f7374617469632e7675652d6a732e636f6d2f34343131343738302d336163612d313165622d383566362d3666616337376330633962332e706e67.png

**具体分析 **

  1. beforeCreate -> created
  • 初始化vue实例,进行数据观测
  1. created
  • 完成数据观测,属性与方法的运算,watch、event时间回调的配置
  • 可调用methods中的方法,访问和修改data数据出发星影视渲染dom,可通过computed和watch完成数据计算
  • 此时vm.$el并没有被创建
  1. created -> beforeMount
  • 判断是否存在el选型,若不存在则停止编译,直到调用vm.$mount(el)才会继续编译
  • vm.el获取到的事挂载DOM的
  1. beforeMount
  • 在此阶段可获取到vm.el
  • 此阶段vm.el虽已完成DOM初始化,但并未挂载在el选项上
  1. beforeMount -> mounted
  • 此阶段vm.el完成挂载,vm.$el生成的DOM替换了el选项所对应的DOM
  1. mounted
  • vm.el已完成DOM的挂载与渲染,此刻打印vm.$el,发现之前的挂载点及内容已被替换成新的DOM
  1. beforeUpdate
  • 更新的数据必须是被渲染在模板上的(el、template、render之一)
  • 此时view层还未更新
  • 若在beforeUpdate中再次修改数据,不会再次触发更新方法
  1. updated
  • 完成view层的更新
  • 若在update中再次修改数据,会再次触发更新方法(beforeUpdate、updated)
  1. beforeDestroy
  • 实例被销毁前调用,此时实例属性与方法仍可访问
  1. destroyed
  • 完全销毁一个实例,可清理它与其它实例的连接,解绑它的全部指令以及事件监听器
  • 并不能清除DOM,仅仅销毁实例

9、数据请求在created和mouted的区别

  • created是在组件实例一旦创建完成的时候立刻调用的,这时候页面dom节点并未生成
  • mounted是在页面dom节点渲染完毕之后就立刻执行的触发
  • 时机上created是比mounted要更早的
    两者的相同点:都能拿到实例对象的属性和方法讨论这个问题。本质就是触发的时机,放在mounted请求有可能导致页面闪动(页面dom结构已经生成),但如果在页面加载前完成则不会出现此情况

建议:放在create生命周期当中

10、双向绑定

双向绑定由三个重要部分构成:

  • 数据层(Model):应用的数据及业务逻辑
  • 视图层(View):应用的展示效果,各类UI组件
  • 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来
    理解ViewModel

它的主要职责就是:

  • 数据变化后更新视图
  • 视图变化后更新数据
    当然,它还有两个主要部分组成:
  • 监听器(Observer):对所有数据的属性进行监听
  • 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

实现双向绑定

  1. new Vue()首先执行初始化,对data执行响应化处理,这个过程发生observe中
  2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile中
  3. 同时定义一个更新函数和Watcher,将来对应数据变化是Watcher会调用更新函数
  4. 由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个Watcher
  5. 将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

68747470733a2f2f7374617469632e7675652d6a732e636f6d2f65353336393835302d336163392d313165622d383566362d3666616337376330633962332e706e67.png

11、为什么Vue中的v-if和v-for不建议一起用?

1.作用

  • v-if指令用于条件性地渲染一块内容,这块内容只会在指令的表达式返回true值的时候被渲染
  • v-for指令基于一个数组来渲染一个列表。v-for指令需要使用item in items形式的特殊语法,其中items是源数据数组或者对象,而item则是被迭代的数组元素的别名
  • 在v-for的时候,建议设置key值,并且保证每个key值是独一无二的,这便于diff算法优化
<Modal v-if="isShow" />
<li v-for="item in items" :key="item.id">
    {{ item.label }}
</li>

2.优先级

v-if与v-for都是vue模板系统中的指令
v-for优先级比v-if高
3.x 版本中 v-if 总是优先于 v-for 生效。

3.注意事项

  • 永远不要把v-if和v-for同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)
  • 如果避免出现这种情况,则在外层嵌套template(页面渲染不生成dom节点),在这一层进行v-if判断,然后在内部进行v-for循环
  • 如果条件出现在循环内部,可通过计算属性computed提前过滤掉那些不需要显示的项

12、SPA(单页应用)首屏加载速度慢怎么解决

首屏时间指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容

在页面渲染的过程中,导致加载速度慢的因素可能如下:

  • 网络延时问题
  • 资源文件体积是否过大
  • 资源是否重复发送请求去加载了
  • 加载脚本的时候,渲染内容堵塞了

spa首屏优化方式

  • 减小入口文件体积
    常用的手段就是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加
    在vue-router配置路由的时候,采用动态加载路由的形式
routes:[ 
    path: 'Blogs',
    name: 'ShowBlogs',
    component: () => import('./components/ShowBlogs.vue')
]

以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件

  • 静态资源本地缓存
    后端返回资源问题:
  1. 采用HTTP缓存,设置设置Cache-Control,Last-Modified,Etag等响应头
  2. 采用Service Worker离线缓存
    前端合理利用localStorage
  • UI框架按需加载
  • 图片资源的压缩
    图片资源虽然不在编码过程中,但它确是对页面性能影响最大的因素
    对于所有的图片资源,我们可以进行适当的压缩
    对页面上使用到的icon,可以使用在线字体图标,或者雪碧图,讲众多小图标合并到同一张图上,用以减轻http请求压力
  • 组件重复打包
  • 开启GZip压缩
  • 使用SSR
    SSR(Server side ),也就是服务端渲染,组件或页面通过服务器生成html字符串,再发送到浏览器
    从头搭建一个服务端渲染是很复杂的,vue应用建议使用Nuxt.js实现服务端渲染

68747470733a2f2f7374617469632e7675652d6a732e636f6d2f34666166653930302d336163632d313165622d383566362d3666616337376330633962332e706e67.png

13、Vue中给对象添加新属性界面不刷新?

vue不允许在已经创建的实例上动态添加新的响应式属性:
弱项实现数据与视图同步更新,可以采用下面三种解决方案:

1. Vue.set()

Vue.set( target, propertyName/index, value )
参数

  • {Object | Array} target
  • {string | number} propertyName/index
  • {any} value
    返回值:设置的值
    通过Vue.set向响应式对象中添加一个property,并确保这个新 property 同样是响应式的,且触发视图更新

2. Object.assgin()

直接使用Object.assign()添加到对象的新属性不会触发更新
应创建一个新的对象,合并原对象和混入对象的属性
this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...})

3. $forcecUpdated()

如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事
$forceUpdate迫使 Vue 实例重新渲染
PS:仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。

总结

  • 如果为对象添加少量的新属性,可以直接采用Vue.set()
  • 如果需要为新对象添加大量的新属性,则通过Object.assign()创建新对象
  • 如果你实在不知道怎么操作时,可采取$forceUpdate()进行强制刷新 (不建议)
  • vue3是用过proxy实现数据响应式的,直接动态添加新属性仍可以实现数据响应式

15、 Vue组件间通信方式都有哪些?

68747470733a2f2f7374617469632e7675652d6a732e636f6d2f38356239323430302d336163612d313165622d616239302d6439616538313462323430642e706e67.png

组件间通信的分类可以分成以下

  • 父子组件之间的通信
  • 兄弟组件之间的通信
  • 祖孙与后代组件之间的通信
  • 非关系组件间之间的通信

组件间通信的方案

1. 通过 props 传递

  • 适用场景:父组件传递数据给子组件
  • 子组件设置props属性,定义接收父组件传递过来的参数
  • 父组件在使用子组件标签中通过字面量来传递值

2. 通过 $emit 触发自定义事件

  • 适用场景:子组件传递数据给父组件
  • 子组件通过$emit触发自定义事件,$emit第二个参数为传递的数值
  • 父组件绑定监听器获取到子组件传递过来的参数
    Chilfen.vue
    this.$emit('add', good)
    Father.vue
    <Children @add="cartAdd($event)" />

3. 使用 ref

  • 父组件在使用子组件的时候设置ref
  • 父组件通过设置子组件ref来获取数据
    Father.vue
    <Children ref="foo" />
    this.$refs.foo // 获取子组件实例,通过子组件实例我们就能拿到对应的数据

4. EventBus

  • 使用场景:兄弟组件传值
  • 创建一个中央时间总线EventBus
  • 兄弟组件通过$emit触发自定义事件,$emit第二个参数为传递的数值
  • 另一个兄弟组件通过$on监听自定义事件
    Bus.js
// 创建一个中央时间总线类  
class Bus {  
  constructor() {  
    this.callbacks = {};   // 存放事件的名字  
  }  
  $on(name, fn) {  
    this.callbacks[name] = this.callbacks[name] || [];  
    this.callbacks[name].push(fn);  
  }  
  $emit(name, args) {  
    if (this.callbacks[name]) {  
      this.callbacks[name].forEach((cb) => cb(args));  
    }  
  }  
}  
  
// main.js  
Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上  
// 另一种方式  
Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能 

Children1.vue

this.$bus.$emit('foo')

Children2.vue

this.$bus.$on('foo', this.handle)

5. $parent$root

  • 通过共同祖辈$parent或者$root搭建通信侨联
    兄弟组件
this.$parent.on('add',this.add)

另一个兄弟组件

this.$parent.emit('add')

6. attrslisteners

  • 适用场景:祖先传递数据给子孙
  • 设置批量向下传属性$attrs$listeners
  • 包含了父级作用域中不作为 prop 被识别 (且获取) 的特性绑定 ( class 和 style 除外)。
  • 可以通过 v-bind="$attrs" 传⼊内部组件

7. ProvideInject

  • 在祖先组件定义provide属性,返回传递的值
  • 在后代组件通过inject接收组件传递过来的值
    祖先组件
provide(){  
    return {  
        foo:'foo'  
    }  
}  

后代组件

inject:['foo'] // 获取到祖先组件传递过来的值  

8. Vuex

  • 适用场景: 复杂关系的组件数据传递
  • Vuex作用相当于一个用来存储共享变量的容器
  • state用来存放共享变量的地方
  • getter,可以增加一个getter派生状态,(相当于store中的计算属性),用来获得共享变量的值
  • mutations用来存放修改state的方法。
  • actions也是用来存放修改state的方法,不过action是在mutations的基础上进行。常用来做一些异步操作

68747470733a2f2f7374617469632e7675652d6a732e636f6d2f66613230376364302d336163612d313165622d616239302d6439616538313462323430642e706e67.png

小结

  • 父子关系的组件数据传递选择 props$emit进行传递,也可选择ref
  • 兄弟关系的组件数据传递可选择$bus,其次可以选择$parent进行传递
  • 祖先与后代组件数据传递可选择attrslisteners或者 ProvideInject
  • 复杂关系的组件数据传递可以通过vuex存放共享的变量

16、Vue中的$nextTick有什么作用?

  • NextTick

Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新

  • 使用场景

  • 如果想要在修改数据后立刻得到更新后的DOM结构,可以使用Vue.nextTick()
  • 第一个参数为:回调函数(可以获取最近的DOM结构)
  • 第二个参数为:执行函数上下文
// 修改数据
vm.message = '修改后的值'
// DOM 还没有更新
console.log(vm.$el.textContent) // 原始的值
Vue.nextTick(function () {
  // DOM 更新了
  console.log(vm.$el.textContent) // 修改后的值
})

组件内使用 vm.$nextTick() 实例方法只需要通过this.$nextTick(),并且回调函数中的 this 将自动绑定到当前的 Vue 实例上

this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
this.$nextTick(function () {
    console.log(this.$el.textContent) // => '修改后的值'
})

$nextTick() 会返回一个 Promise 对象,可以是用async/await完成相同作用的事情

this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
await this.$nextTick()
console.log(this.$el.textContent) // => '修改后的值'
  • 小结

  • 把回调函数放入callbacks等待执行
  • 将执行函数放到微任务或者宏任务中
  • 事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调

17、说说你对slot的理解?slot使用场景有哪些?

  • slot

  • 在HTML中 slot 元素 ,作为 Web Components 技术套件的一部分,是Web组件内的一个占位符
  • 该占位符可以在后期使用自己的标记语言填充
  • 使用场景

  • 通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理
  • 如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情
  • 通过slot插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用
  • 比如布局组件、表格列、下拉选、弹框显示内容等
  • 分类

  • 默认插槽
  • 子组件用<slot>标签来确定渲染的位置,标签里面可以放DOM结构,当父组件使用的时候没有往插槽传入内容,标签内DOM结构就会显示在页面
  • 父组件在使用的时候,直接在子组件的标签内写入内容即可 子组件Child.vue
<template>
    <slot>
      <p>插槽后备的内容</p>
    </slot>
</template>

父组件

<Child>
  <div>默认插槽</div>  
</Child>
  • 具名插槽
  • 子组件用name属性来表示插槽的名字,不传为默认插槽
  • 父组件中在使用时在默认插槽的基础上加上slot属性,值为子组件插槽name属性值 子组件Child.vue
<template>
    <slot>插槽后备的内容</slot>
  <slot name="content">插槽后备的内容</slot>
</template>

父组件

<child>
    <template v-slot:default>具名插槽</template>
    <!-- 具名插槽⽤插槽名做参数 -->
    <template v-slot:content>内容...</template>
</child>
  • 作用域插槽
  • 子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件v-slot接受的对象上
  • 父组件中在使用时通过v-slot:(简写:#)获取子组件的信息,在内容中使用 子组件Child.vue
<template> 
  <slot name="footer" testProps="子组件的值">
          <h3>没传footer插槽</h3>
    </slot>
</template>

父组件

<child> 
    <!-- 把v-slot的值指定为作⽤域上下⽂对象 -->
    <template v-slot:default="slotProps">
      来⾃⼦组件数据:{{slotProps.testProps}}
    </template>
  <template #default="slotProps">
      来⾃⼦组件数据:{{slotProps.testProps}}
    </template>
</child>
  • 小结

  • v-slot属性只能在<template>上使用,但在只有默认插槽时可以在组件标签上使用
  • 默认插槽名为default,可以省略default直接写v-slot
  • 缩写为#时不能不写参数,写成#default
  • 可以通过解构获取v-slot={user},还可以重命名v-slot="{user: newName}"和定义默认值v-slot="{user = '默认值'}"

18、怎么缓存当前的组件?缓存后怎么更新?说说你对keep-alive的理解是什么?

  • Keep-alive

keep-alive是vue中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM
keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们
keep-alive可以设置以下props属性:

  • include - 字符串或正则表达式。只有名称匹配的组件会被缓存
  • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存
  • max - 数字。最多可以缓存多少组件实例
    设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(activated与deactivated):
  • 首次进入组件时:beforeRouteEnter > beforeCreate > created> mounted > activated > ... ... > beforeRouteLeave > deactivated
  • 再次进入组件时:beforeRouteEnter >activated > ... ... > beforeRouteLeave > deactivated
  • 使用场景

使用原则:当我们在某些场景下不需要让页面重新加载时我们可以使用keepalive
举例:

  • 当我们从首页–>列表页–>商详页–>再返回,这时候列表页应该是需要keep-alive
  • 从首页–>列表页–>商详页–>返回到列表页(需要缓存)–>返回到首页(需要缓存)–>再次进入列表页(不需要缓存),这时候可以按需来控制页面的keep-alive
    在路由中设置keepAlive属性判断是否需要缓存
{
  path: 'list',
  name: 'itemList', // 列表页
  component (resolve) {
    require(['@/pages/item/list'], resolve)
 },
 meta: {
  keepAlive: true,
  title: '列表页'
 }
}

使用<keep-alive>

<div id="app" class='wrapper'>
    <keep-alive>
        <!-- 需要缓存的视图组件 --> 
        <router-view v-if="$route.meta.keepAlive"></router-view>
     </keep-alive>
      <!-- 不需要缓存的视图组件 -->
     <router-view v-if="!$route.meta.keepAlive"></router-view>
</div>
  • 缓存后如何获取数据

  • beforeRouteEnter
    每次组件渲染的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){
    next(vm=>{
        console.log(vm)
        // 每次进入路由执行
        vm.getData()  // 获取数据
    })
},

  • actived 在keep-alive缓存的组件被激活的时候,都会执行actived钩子
activated(){
    this.getData() // 获取数据
},

注意:服务器端渲染期间avtived不被调用

19、Vue常用的修饰符有哪些?有什么应用场景?

1.表单修饰符

  • lazy 在我们填完信息,光标离开标签的时候,才会将值赋予给value,也就是在change事件之后再进行信息同步
<input type="text" v-model.lazy="value">
<p>{{value}}</p>
  • trim 自动过滤用户输入的首空格字符,而中间的空格不会过滤
<input type="text" v-model.trim="value">
  • number 自动将用户的输入值转为数值类型,但如果这个值无法被parseFloat解析,则会返回原来的值
<input v-model.number="age" type="number">

2.事件修饰符

  • stop 阻止了事件冒泡,相当于调用了event.stopPropagation方法
<div @click="shout(2)">
  <button @click.stop="shout(1)">ok</button>
</div>
//只输出1
  • prevent 阻止了事件的默认行为,相当于调用了event.preventDefault方法
<form v-on:submit.prevent="onSubmit"></form>
  • self 只当在 event.target 是当前元素自身时触发处理函数
<div v-on:click.self="doThat">...</div>

使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击

  • once 绑定了事件以后只能触发一次,第二次就不会触发
<button @click.once="shout(1)">ok</button>
  • capture 使事件触发从包含这个元素的顶层开始往下触发
<div @click.capture="shout(1)">
    obj1
<div @click.capture="shout(2)">
    obj2
<div @click="shout(3)">
    obj3
<div @click="shout(4)">
    obj4
</div>
</div>
</div>
</div>
// 输出结构: 1 2 4 3 
  • passive 在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成  -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>

不要把 .passive 和 .prevent 一起使用,因为 .prevent 将会被忽略,同时浏览器可能会向你展示一个警告。
passive 会告诉浏览器你不想阻止事件的默认行为

  • native 让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件
<my-component v-on:click.native="doSomething"></my-component>

使用.native修饰符来操作普通HTML标签是会令事件失效的

3.鼠标按键修饰符

  • left 左键点击
  • right 右键点击
  • middle 中键点击
<button @click.left="shout(1)">ok</button>
<button @click.right="shout(1)">ok</button>
<button @click.middle="shout(1)">ok</button>

4.键值修饰符

键盘修饰符是用来修饰键盘事件(onkeyup,onkeydown)的,有如下:
keyCode存在很多,但vue为我们提供了别名,分为以下两种:

  • 普通键(enter、tab、delete、space、esc、up...)
  • 系统修饰键(ctrl、alt、meta、shift...)
// 只有按键为keyCode的时候才触发
<input type="text" @keyup.keyCode="shout()">

还可以通过以下方式自定义一些全局的键盘码别名

Vue.config.keyCodes.f2 = 113

5.v-bind修饰符

  • async 能对props进行一个双向绑定
//父组件
<comp :myMessage.sync="bar"></comp> 
//子组件
this.$emit('update:myMessage',params);

注意:

  1. 使用sync的时候,子组件传递的事件名格式必须为update:value,其中value必须与子组件中props中声明的名称完全一致
  2. 注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用
  3. 将 v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的
  • prop 设置自定义标签属性,避免暴露数据,防止污染HTML结构
<input id="uid" title="title1" value="1" :index.prop="index">
  • camel 将命名变为驼峰命名法,如将 view-Box属性名转换为 viewBox
<svg :viewBox="viewBox"></svg>

6.应用场景

  • .stop:阻止事件冒泡
  • .native:绑定原生事件
  • .once:事件只执行一次
  • .self :将事件绑定在自身身上,相当于阻止事件冒泡
  • .prevent:阻止默认事件
  • .caption:用于事件捕获
  • .once:只触发一次
  • .keyCode:监听特定键盘按下
  • .right:右键

20、你知道vue中key的原理吗?说说你对它的理解?

21、Vue中的过滤器了解吗?过滤器的应用场景有哪些?

22、什么是虚拟DOM?如何实现一个虚拟DOM?说说你的思路

23、了解过vue中的diff算法吗?说说看

24、Vue项目中有封装过axios吗?怎么封装的?

25、SSR解决了什么问题?有做过SSR吗?你是怎么做的?

26、说下你的Vue项目的目录结构,如果是大型项目你该怎么划分结构和划分组件呢?

27、跨域是什么?Vue项目中你是如何解决跨域的呢?

28、Vue要做权限管理该怎么做?控制到按钮级别的权限怎么做?

29、Vue项目如何部署?有遇到布署服务器后刷新404问题吗?

30、你是怎么处理vue项目中的错误的?

31、Vue3有了解过吗?能说说跟Vue2的区别吗?

vue3.0

1、Vue3.0的设计目标是什么?做了哪些优化?

2、Vue3.0 性能提升主要是通过哪几方面体现的?

3、Vue3.0里为什么要用 Proxy API 替代 defineProperty API ?

4、Vue3.0 所采用的 Composition Api 与 Vue2.x 使用的 Options Api 有什么不同?

5、说说Vue 3.0中Treeshaking特性?举例说明一下?

6、用Vue3.0 写过组件吗?如果想实现一个 Modal你会怎么设计?

性能

算法

css&&html&浏览器


1、网络中使用最多的图片格式有哪些?

jpeg、gif、png,最流行的事jpeg格式,可以把文件压缩到最小,在ps以jpeg格式存储时,提供11级压缩级别

2、请简述css盒子模型

一个css盒子从外到内可以分成四个部分:margin(外边距)、border(边框)、padding(内边距)、content(内容)。默认情况下,盒子的width和height属性只是设置content(内容)的宽和高

盒子真正的宽应该是:内容宽度 + 左右填充 + 左右边距 + 左右边框
盒子真正的高应该是:内容高度 + 上下填充 + 上下边距 + 上下边框

3、视频/音频标签的使用

视频: <video src=""></video>

68747470733a2f2f7374617469632e7675652d6a732e636f6d2f32356265363633302d336163372d313165622d616239302d6439616538313462323430642e706e67.png 视频标签属性:

  • src 需要播放的视频地址
  • width/height 设置播放视频的宽高,和img标签的宽高属性一样
  • autoplay 是否自动播放
  • controls 是否显示控制条
  • poster 没有播放之前显示的占位图片
  • loop 是否循环播放
  • perload 预加载视频(缓存)与autoplay相冲突,设置了autoplay属性。perload属性会失效
  • muted 静音模式

音频: <audio><source src=" type="></audio> 音频属性和视频差不多,不过宽高和poster属性不能用

4、html5新增的内容有哪些?

  1. 绘画canvas元素

canvas定义图形,绘制路径,矩形,圆形,字符以及添加图像的方法

  1. 音频audio和视频video

video也支持多个source元素,链接到不同的视频文件,浏览器将使用第一个可识别的格式

  1. 存储
    HTML5 提供了两种在客户端存储数据的新方法:

localStorage - 没有时间限制的数据存储
localStorage 方法存储的数据没有时间限制。第二天、第二周或下一年之后,数据依然可用。
sessionStorage - 针对一个 session 的数据存储
sessionStorage 方法针对一个 session 进行数据存储。当用户关闭浏览器窗口后,数据会被删除。

  1. 语义化标签

<header>:定义了文档的头部区域,在一个文档中可以定义多个<header>元素。它是块元素
<footer>:定义文档页脚,它不只是页面的最底部,在文档中也可以定义多个。
<article>:定义页面独立的内容区域,标签定义的内容本身必须是有意义且必须独立于文档的其他部分,可用在的地方:博客文章,新闻,评论等。
<aside>:定义页面的侧边栏内容
<time>:定义时间或日期,time标签中的datetime属性定义的时间不会被显示,但可能被其他应用使用
<ruby>:加注释,ruby标签有两个子元素,rt注释的内容,rp是该标签不显示时显示的文字
<details>:用于描述文档或者文档某一部分细节,summary是details元素的标题
<mark>:定义带有几号的文本,它会给你想要突出显示的文本加个 背景色
<nav>:定义导航栏
<progress>:progress显示数据的进度,属性value指定当前值,max最大值,最小值0不用设置
<section>:section一般有两个作用,1.定义文档中的节,和div类似。2.定义文章,这时一般带有标题
<video>:定义视频,属性src引入资源,controls视频的控制控件
<audio>:该标签可定义声音,及其他的音频文件,不加controls不显示音频的控制界面
<datalist>:提示可能的值,datalist及其选项不会被显示出来,它仅仅是合法输入值的列表使用input元素的list属性来邦定datalist,下面选项使用option定义
<embed>:定义插入的内容,如插件,flash,标签中间不要加内容会显现出来
<canvas>:canvas画布只是个容器,你可以通过控制坐标在canvas上绘制图形,一般配合js可以实现非常复杂的动画效果

  1. 新的表单类型

1、email
email 类型用于应该包含 e-mail 地址的输入域。在提交表单时,会自动验证 email 域的值。
E-mail: <input type="email" name="user_email" />
2、url
url 类型用于应该包含 URL 地址的输入域。在提交表单时,会自动验证 url 域的值。
Homepage: <input type="url" name="user_url" />
3、number
number 类型用于应该包含数值的输入域。还能够设定对所接受的数字的限定:
Points: <input type="number" name="points" min="1" max="10" />
4、range
range 类型用于应该包含一定范围内数字值的输入域。
range 类型显示为滑动条。还能够设定对所接受的数字的限定:
<input type="range" name="points" min="1" max="10" />
5、Date pickers (date, month, week, time, datetime, datetime-local)
HTML5 拥有多个可供选取日期和时间的新输入类型
Date: <input type="date" name="user_date" />
6、search
search 类型用于搜索域,比如站点搜索或 Google 搜索。search 域显示为常规的文本域。
7、color颜色的选择表单类型

5、html5新增的语义化标签有哪些?

语义化标签优点: 1、提升可访问性 2、seo 3、解构清晰,利于维护
<header>:页面头部
<main>:页面主要内容
<footer>:页面底部
<article>:加载页面一块独立内容
<aside>:定义页面的侧边栏内容
<time>:定义时间或日期
<ruby>:添加注释
<details>:描述细节
<mark>:高亮显示
<nav>:定义导航栏
<progress>:进度条
<section>:相当于div
<video>:加载视频
<audio>:加载音频(支持格式agg、mp3、wav)
<datalist>:提示可能的值
<embed>:加载插件的标签
<canvas>:画布

6、css3新增的特性

边框:

border-radios:添加圆角边框
border-shadow:给框添加阴影(水平位移、垂直位移、模糊半径、阴影尺寸、阴影颜色、insetr(内、外部阴影))
border-image:设置边框图像
border-image-source:边框图片的路径
border-image-slice:图片边框向内偏移
border-image-width:图片边框的宽度
border-image-outset:边框图像区域超出边框的量
border-image-repeat:图像边框是否平铺(repeat平铺、round铺满、stretch拉伸)

背景:

background-size:背景图片尺寸
background-orgin:规定background-position属性相对于什么位置定位
background-clip:规定背景的绘制区域(padding-box,border-box,content-box)

渐变:

linear-gradient():线性渐变
radial-gradient():径向渐变

文本效果:

word-break:定义如何换行
word-wrap:允许长的内容可以自动换行
text-overflow:指定当文本溢出包含它的元素,应该干啥
text-shadow:文字阴影(水平位移,垂直位移,模糊半径,阴影颜色)

转换:

transform:应用于2D3D转换,可以将元素旋转、缩放、移动、倾斜
transform-orgin:可以更改元素转换的位置(改变xyz轴)
transform-style:指定嵌套元素怎么样在三维空间中呈现

2D转换方法:

rotate旋转trandlate(x,y)指定元素在二维空间的位移scale(n)定义缩放转换

3D转换方法:

perspective(n)为3D转换translate rotate scale

过渡:

transition-proprety:过渡属性名
transition-duration:完成过渡效果需要花费的时间
transition-timing-function:指定切换效果的速度
transitio-delay:指定什么时候开始切换效果

动画animation:

animation-name:为@keyframes动画名称
animation-duration:动画需要花费的时间
animation-timing-function:动画如何完成一个周期
animation-delay:动画启动前的延迟间隔
animation-iteration-count:动画播放次数
animation-direction:是否轮流反向播放动画

7、清除浮动的方式有哪些?请说出各自的优点

高度塌陷:当所有的子元素浮动的时候,且父元素没有设置高度,这时候父元素就会产生高度塌陷。

  • 清除浮动的方式1:给父元素单独定义高度

优点:快速简单,代码少
缺点:无法进行响应式布局

  • 清除浮动方式2:父级定义overflow:hidden;zoom:1(针对ie6的兼容)

优点:简单快速,代码少,兼容性高
缺点:超出部分被隐藏,布局时候要注意

  • 清除浮动方式3:在浮动元素后面加一个空标签clear:both;height:0;overflow:hidden

优点:简单快速,代码少,兼容性较高
缺点:增加空标签,不利于页面优化

  • 清除浮动方式4:父级定义overflow:auto

优点:简单,代码少,兼容性好
缺点:内部宽高超过父级div时,会出现滚动条

  • 清除浮动方式5:万能清除法:给塌陷的元素添加伪对象
:after{
    content:'随便写';
    clear:both;
    display:block;
    height:0;
    overflow:hidden;
    visibility:hidden;
}

优点:写法固定,兼容性高
缺点:代码多

8、定位的属性值有何区别?

position有四个属性值:relative、absolute、fixed、static
relative:相对定位,不脱离文档流,相对于自身定位
absolute:绝对定位,脱离文档流,相对于父级定位
fixed:固定定位,脱离文档流,相对于浏览器窗口定位
static:默认值,元素出现在正常的流中

9、子元素如何在父元素中居中

  1. 子父元素宽度固定,子元素设置margin:auto,并且子元素不能设置浮动,否则居中失效
  2. 子父元素宽度固定,父元素设置text-aligin:center,子元素设置display:inline-block,并且子元素不能设置浮动,否则居中失效
  3. 子元素相对于父元素绝对定位,子元素top,left设置50%,子元素margin-topmargin-left减去各自宽高的一半
  4. 子元素相对于父元素绝对定位,子元素上下左右全为0,然后设置子元素margin:auto
  5. 父元素设置display:table-cell;vertical-algin:middle,子元素设置margin:auto
  6. 子元素相对定位,子元素top,left值为50%,transform:translate(-50%,-50%)
  7. 子元素相对父元素绝对定位,子元素top,left值为50%,transform:translate(-50%,-50%)
  8. 父元素设置弹性盒子:display;flex;justfy-content:center;aligin-item:center;justfy-content:center

10、border-box和content-box的区别

content-box:标准盒模型(width不包括padding和border)
border-box:怪异盒模型(width包括padding和border)

11、说说em/px/rem/vh/vw区别?

12、css选择器有哪些?优先级?哪些属性可以继承?

13、谈谈你对BFC的理解?

14、css中,有哪些方式可以隐藏页面元素?区别?

15、如果要做优化,CSS提高性能的方法有哪些?

16、说说对Css预编语言的理解?有哪些区别?