0%

面试指南

  • 什么是引用传递?{} == {} 是否等于true
  • 如何编写一个对象的深度拷贝函数?
  • new操作符具体做了哪些操作,重要知识点!
    1
    2
    3
    4
    5
    6
    7
    var p = [];
    var A = new Function();
    A.prototype = p;
    var a = new A;
    a.push(1);
    console.log(a.length);
    console.log(p.length);

    对象的三种类型介绍:

  • 内置对象,(String、Number、Boolean、Object、Function、Array)
  • 宿主对象,由Javascript解释器所嵌入的宿主环境定义的,表示网页结构的HTMLElement对象均是宿主对象,也可以当成内置对象
  • 自定义对象

    创建对象的四种方法:

  • 对象字面量 var obj = { a: 1 }
  • 使用new关键字构造形式创建 var obj = new Object({ a: 1})
  • 原型(prototype)创建
  • ES5的Object.create() 方法创建

    对象字面量创建

    对象字面量是由若干个键/值对组成的映射表,整个映射表用{}包括起来
    1
    2
    var obj = { a: 1 };
    console.log(obj.a);

    在ES6中增加了可计算属性名

    1
    2
    3
    4
    5
    6
    var variable = 2;
    var obj = {
    [1 + variable]: '我是一个可计算属性名'
    }

    console.log(obj); // {3: "我是一个可计算属性名"}

    对象的内容访问

    对象值的存入方式是多种多样的,存入在对象容器中的是这些属性的名称,学过C的同学可以想象一下指针的引用,在js中可以理解为对象的引用。内容访问可以通过以下两种符号:
  • . 指属性访问
  • [] 指键访问
    注意:对象中属性名永远必将是字符串,obj[2]看似2是整数,在对象属性名中数字是会被转换为字符串的
    1
    2
    3
    4
    5
    6
    7
    var obj = {
    'a': '属性访问',
    2: '键访问'
    }

    console.log(obj.a); // 属性访问
    console.log(obj[2]); // 键访问

    使用new关键字构造形式创建

    先介绍下new操作符构造对象的整个过程,这个很重要,明白之后有助于对后续的理解

    new操作符构造对象过程

  • 创建一个全新的对象
  • 新对象会被执行prototype操作(prototype之后会写文章专门进行介绍,感兴趣的童鞋可以先关注下)
  • 新对象会被绑定到函数调用的this
  • 如果函数没有返回新对象,new表达式中的函数调用会自动返回这个新对象(对于一个构造函数,即使它内部没有return,也会默认返回return this)
    看一道曾经遇到的面试题,如果在看本篇文章介绍之前,你能够正确理解并读出下面语句,那么恭喜你对这块理解很透彻
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var p = [2, 3];
    var A = new Function();
    A.prototype = p;

    console.log(A.prototype)

    var a = new A;

    console.log(a.__proto__)

    a.push(1);

    console.log(a.length); // 3
    console.log(p.length); // 2
    new A 时发生了什么?

1.创建一个新的对象obj
var obj = {}
2.新对象执行prototype操作,设置新对象的_proto_属性指向构造函数的A.prototype
obj._proto_ = A.prototype
3.构造函数的作用域(this)赋值给新对象
A.apply(obj)
4.返回该对象
上面示例中实例a已经不是一个对象,而是一个数组对象,感兴趣的童鞋可以在电脑上操作看下 A.prototypea.__proto__ 的实际输出结果

new操作符创建数组对象

数组属于内置对象,所以可以当作一个普通的键/值对来使用。

1
2
3
4
5
6
7
8
9
var arr = new Array('a', 'b', 'c'); // 类似于 ['a', 'b', 'c']

console.log(arr[0]); // a
console.log(arr[1]); // b
console.log(arr[2]); // c
console.log(arr.length); // 3

arr[3] = 'd';
console.log(arr.length); // 4

对象的create方法创建

Object.create(obj, [options])方法是ECMAScript5中定义的方法

  • obj 第一个参数是创建这个对象的原型
  • options 第二个为可选参数,用于描述对象的属性

    null创建一个没有原型的新对象

    1
    2
    3
    var obj = Object.create(null)

    console.log(obj.prototype); // undefined

    创建一个空对象

    以下 Object.create(Object.prototype) 等价于 {} 或 new Object()
    1
    2
    3
    var obj = Object.create(Object.prototype)

    console.log(obj.prototype); // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}

    创建原型对象

    1
    2
    3
    var obj = Object.create({ a: 1, b: 2 })

    console.log(obj.b); // 2

    原型prototype创建

    除了 null 之外的每一个对象都从原型继承属性,关于javascript的原型之后会有一篇文章进行讲解,本次主要讨论对象的一些内容,所以在这里不做过多讨论
  • new Object或者{}创建的对象,原型是 Object.prototype
  • new Array创建的对象,原型是 Array.prototype
  • new Date创建的对象,原型是 Date.prototype

    对象属性描述符

    ES5之后才拥有了描述对象检测对象属性的方法
  • 属性描述符含义
    • {value: 1, writable: true, enumerable: true, configurable: true}
    • value 属性值
    • writable 属性值是否可以修改
    • enumerable 是否希望某些属性出现在枚举中
    • configurable 属性是否可以配置,如果是可配置,可以结合 Object.defineProperty() 方法使用
  • Object.getOwnPropertyDescriptor(obj, prop)
    • 获取指定对象的自身属性描述符
    • obj 属性对象
    • prop 属性名称
      1
      2
      3
      4
      var obj = { a: 1 }
      var propertyDesc = Object.getOwnPropertyDescriptor(obj, 'a');

      console.log(propertyDesc); // {value: 1, writable: true, enumerable: true, configurable: true}
  • Object.defineProperty(obj, prop, descriptor)
    • 该方法会直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象
    • obj 属性对象
    • prop 属性名称
      1
      2
      3
      4
      var obj = { a: 1 }
      var propertyDesc = Object.getOwnPropertyDescriptor(obj, 'a');

      console.log(propertyDesc); // {value: 1, writable: true, enumerable: true, configurable: true}
  • Object.defineProperty(obj, prop, descriptor)
    • 该方法会直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象
    • obj 属性对象
    • prop 属性名称
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      var obj = { a: 1 };

      Object.defineProperty(obj, 'a', {
      writable: false, // 不可写
      configurable: false, // 设置为不可配置后将无法使用delete 删除
      })

      obj.a = 2;

      console.log(obj.a); // 1

      delete obj.a;

      console.log(obj.a); // 1
  • Object.preventExtensions(obj)
    • 禁止一个对象添加新的属性
    • obj 属性对象
      1
      2
      3
      4
      5
      6
      7
      var obj = { a: 1 };

      Object.preventExtensions(obj)

      obj.b = 2;

      console.log(obj.b); // undefined

      对象的存在性检测

      区分对象中的某个属性是否存在

      操作符in检查

      in操作符除了检查属性是否在对象中存在之外还会检查在原型是否存在
      1
      2
      3
      var obj = { a: 1 };

      console.log('a' in obj); // true

      hasOwnProperty

      1
      2
      3
      var obj = { a: 1 };

      console.log(obj.hasOwnProperty('a')); // true

      对象引用传递

      对象属于引用类型是属性和方法的集合。引用类型可以拥有属性和方法,属性也可以是基本类型和引用类型。
      avascript不允许直接访问内存中的位置,不能直接操作对象的内存空间。实际上操作的是对象的引用,所以引用类型的值是按引用访问的。准确说引用类型的存储需要内存的栈区和堆区(堆内存)共同完成,栈区内保存变量标识符和指向堆内存中该对象的指针(也可以说该对象在堆内存中的地址)。

      引用类型示例分析

1.引用类型比较
引用类型是按照引用访问的,因此对象(引用类型)比较的是堆内存中的地址是否一致,很明显a和b在内存中的地址是不一样的。

1
2
3
4
const a = {};
const b = {};

a == b //false

2.引用类型比较
下面对象d是对象c的引用,这个值d的副本实际上是一个指针,而这个指针指向堆内存中的一个对。因此赋值操作后两个变量指向了同一个对象地址,只要改变同一个对象的值另外一个也会发生改变。

1
2
3
4
5
6
7
8
9
10
const c = {};
const d = c;

c == d //true

c.name = 'zhangsan';
d.age = 24;

console.log(c); //{name: "zhangsan", age: 24}
console.log(d); //{name: "zhangsan", age: 24}

对象copy实现

  • 利用json实现
    可以利用JSON,将对象先序列化为一个JSON字符串,在用JSON.parse()反序列化,可能不是一种很好的方法,但能适用于部分场景
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const a = {
    name: 'zhangsan',
    school: {
    university: 'shanghai',
    }
    };

    const b = JSON.parse(JSON.stringify(a));

    b.school.university = 'beijing';

    console.log(a.school.university); // shanghai
    console.log(b.school.university); // beijing
  • es6内置方法
    ES6内置的 Object.assign(target,source1,source2, ...) ,第一个参数是目标参数,后面是需要合并的源对象可以有多个,后合并的属性(方法)会覆盖之前的同名属性(方法),需要注意 Object.assign() 进行的拷贝是浅拷贝
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const obj1 = {a: {b: 1}};
    const obj2 = Object.assign({}, obj1);

    obj2.a.b = 3;
    obj2.aa = 'aa';

    console.log(obj1.a.b) // 3
    console.log(obj2.a.b) // 3

    console.log(obj1.aa) // undefined
    console.log(obj2.aa) // aa
  • 实现一个数组对象深度拷贝

    对于下面这样一个复杂的数组对象,要做到深度拷贝(采用递归的方式),在每次遍历之前创建一个新的对象或者数组,从而开辟一个新的存储地址,这样就切断了引用对象的指针联系。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /**
    * [copy 深度copy函数]
    * @param { Object } elments [需要赋值的目标对象]]
    */
    function copy(elments){
    //根据传入的元素判断是数组还是对象
    let newElments = elments instanceof Array ? [] : {};

    for(let key in elments){
    //注意数组也是对象类型,如果遍历的元素是对象,进行深度拷贝
    newElments[key] = typeof elments[key] === 'object' ? copy(elments[key]) : elments[key];
    }

    return newElments;
    }
    需要赋值的目标对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const a = {
    name: 'zhangsan',
    school: {
    university: 'shanghai',
    },
    hobby: ['篮球', '足球'],
    classmates: [
    {
    name: 'lisi',
    age: 22,
    },
    {
    name: 'wangwu',
    age: 21,
    }
    ]
    };
    测试验证,复制出来的对象b完全是一个新的对象,修改b的值,不会在对a进行影响。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const b = copy(a);

    b.age = 24;
    b.school.highSchool = 'jiangsu';
    b.hobby.push('🏃');
    b.classmates[0].age = 25;

    console.log(JSON.stringify(a));
    //{"name":"zhangsan","school":{"university":"shanghai"},"hobby":["篮球","足球"],"classmates":[{"name":"lisi","age":22},{"name":"wangwu","age":21}]}
    console.log(JSON.stringify(b));

    //{"name":"zhangsan","school":{"university":"shanghai","highSchool":"jiangsu"},"hobby":["篮球","足球","🏃"],"classmates":[{"name":"lisi","age":25},{"name":"wangwu","age":21}],"age":24}

函数

函数是由事件驱动的或者当它被调用时执行的可重复使用的代码块。

函数声明与函数表达式

对于函数声明解释器会首先读取,并使其在执行任何代码之前可用;
对于函数表达式,则必须等到解释器执行到它所在的代码行,才会被真正解析。
例如下面例子,函数表达式test2必须声明在其调用之前才可用

1
2
3
4
5
6
7
8
9
10
11
12
console.log(test1(1, 2)); // 3
console.log(test2(1, 2)); // test2 is not defined

//函数声明
function test1(a, b){
return a + b;
}

//函数表达式
const test2 = function f(a, b){
return a + b;
}

内置函数

  • push()

    数组添加新值后的返回值,返回当前数组的Length。
    let a = [].push('test'); 输出a的值为1而不是['test'],因为push()返回的是数组的长度。

  • arguments对象

    系统内置的arguments对象,可以用于获取函数参数、参数长度等

    call和apply的使用与区别

  • apply使用情况
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function box(num1,num2){
    return num1+num2;
    }

    function sum(num1,num2){
    //this 表示全局作用域,浏览器环境下window,node环境global,[]表示传递的参数
    return box.apply(this,[num1,num2]);

    //或者下面写法arguments可以当数组传递
    //return box.apply(this,arguments);
    }

    console.log(sum(10,10)); //输出结果: 20
  • call的使用示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function box(num1,num2){
    return num1+num2;
    }

    function sum2(num1,num2){
    return box.call(this,num1,num2);
    }

    console.log(sum(10,10)); //输出结果: 20
    apply传递参数是按照数组传递,call是一个一个传递

面试

引用传递

javascript没有引用传递,如果传递的参数是一个值,是按值传递;如果传递的是一个对象,则传递的是一个对象的引用。

  • 示例一:js代码按值传递
    如果按引用传递,那么函数里面的num会变成类似全局变量,最后输出60
    1
    2
    3
    4
    5
    6
    7
    8
    function box(num){ // 按值传递
    num+=10;
    return num;
    }
    var num=50;

    console.log(box(num)); // 60
    console.log(num); // 50
  • 示例二:php代码传递一个参数:
    php中的引用传递,会改变外部的num值,最后num也会输出60。
    1
    2
    3
    4
    5
    6
    7
    8
    function box(&$num){ 
    //加上&符号将num变成全局变量
    $num+=10;
    return $num;
    }
    $num = 50;
    echo box($num); // 60
    echo $num; // 60
  • 示例三:js代码传递一个对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function box(obj){ // 按对象传递
    obj.num+=10;

    return obj.num;
    }
    var obj = { num: 50 };

    console.log(box(obj)); // 60
    console.log(obj.num); // 60

    匿名函数与闭包

    匿名函数就是没有名字的函数,闭包是可访问一个函数作用域里变量的函数,由于闭包作用域返回的局部变量资源不会被立刻销毁回收,所以可能会占用更多的内存。过度使用闭包会导致性能下降,建议在非常有必要的时候才使用闭包。

匿名函数的自我执行

1
2
3
(function(num){
return num;
})(1) //1

函数里放一个匿名函数将会产生闭包

  • 使用局部变量实现累加功能。
  • 定义函数test1,返回一个匿名函数形成一个闭包
  • 将test1赋给test2,此时test2会初始化变量a,值为test1返回的匿名函数
  • 执行test2()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    {
    function test1(){
    var a = 1;

    return function(){
    // a++;
    // return a;
    // 或以下写法
    return ++a;
    }
    }

    var test2 = test1();

    console.log(test2()); // 2
    console.log(test2()); // 3
    console.log(test2()); // 4

    //不能这样写,这样外层函数每次也会执行,从而age每次都会初始化
    console.log(test1()()); // 2
    console.log(test1()()); // 2
    console.log(test1()()); // 2
    }

闭包中使用this对象将会导致的一些问题

在闭包中使用this对象也可能会导致一些问题,this对象是在运行时基于函数的执行环境绑定的,如果this在全局范围就是window,如果在对象内部就指向这个对象。而闭包却在运行时指向window的,因为闭包并不属于这个对象的属性或方法
返回object

1
2
3
4
5
6
7
var box={
getThis:function(){
return this;
}
}

console.log(box.getThis()); // { getThis: [Function: getThis] }

闭包中的this将返回全局对象,浏览器中window对象,Node.js中global对象,可以使用对象冒充或者赋值来解决闭包中this全局对象问题。

1
2
3
4
5
6
7
8
9
10
var box={
user: 'zs',
getThis:function(){
return function(){
return this;
};
}
}

console.log(box.getThis()());

对象冒充

1
2
3
4
5
6
7
8
9
10
var box={
user: 'zs',
getThis:function(){
return function(){
return this;
};
}
}

console.log(box.getThis().call(box)); // { user: 'zs', getThis: [Function: getThis] }

赋值

1
2
3
4
5
6
7
8
9
10
11
var box={
user: 'zs',
getThis:function(){
var that = this; // 此时的this指的是box对象
return function(){
return that.user;
};
}
}

console.log(box.getThis()()); // zs

一个例子看懂循环和闭包之间的关系
下例,循环中的每个迭代器在运行时都会给自己捕获一个i的副本,但是根据作用域的工作原理,尽管循环中的五个函数分别是在各个迭代器中分别定义的,但是它们都会被封闭在一个共享的全局作用域中,实际上只有一个i,结果每次都会输出6

1
2
3
4
5
for(var i=1; i <= 5; i++){
setTimeout(function(){
console.log(i);
})
}

解决上面的问题,在每个循环迭代中都需要一个闭包作用域,下面示例,循环中的每个迭代器都会生成一个新的作用域。

1
2
3
4
5
6
7
for(var i=1; i <= 5; i++){
(function(j){
setTimeout(function(){
console.log(j);
})
})(i)
}

也可以使用let解决,let声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。

1
2
3
4
5
for(let i=1; i <= 5; i++){
setTimeout(function(){
console.log(i);
})
}

函数表达式实现一个阶乘函数

1
2
3
4
5
6
7
8
9
const factorial = (function f(num){
if(num <= 1){
return 1;
}else{
return num * f(num -1);
}
});

console.log(factorial(3)); // 6

两种错误认识

指向自身

this 的第一个错误认识是,很容易把 this 理解成指向函数自身,其实this 的指向在函数定义阶段是无法确定的,只有函数执行时才能确定 this 到底指向谁,实际 this 的最终指向是调用它的那个对象。
下面示例,声明函数 foo(),执行 foo.count=0 时,像函数对象 foo 添加一个属性 count。但是函数 foo 内部代码 this.count 中的 this 并不是指向那个函数对象,for 循环中的 foo(i) 掉用它的对象是 window,等价于 window.foo(i),因此函数 foo 里面的 this 指向的是 window。

1
2
3
4
5
6
7
8
9
10
11
function foo(num){
this.count++; // 记录 foo 被调用次数
}
foo.count = 0;
window.count = 0;
for(let i=0; i<10; i++){
if(i > 5){
foo(i);
}
}
console.log(foo.count, window.count); // 0 4

指向函数的作用域

对 this 的第二种误解就是 this 指向函数的作用域
以下这段代码,在 foo 中试图调用 bar 函数,是否成功调用,取决于环境。

  • 浏览器:在浏览器环境里是没有问题的,全局声明的函数放在了 window 对象下,foo 函数里面的 this 代指的是 window 对象,在全局环境中并没有声明变量 a,因此在 bar 函数中的 this.a 自然没有定义,输出 undefined。
  • Node.js:在 Node.js 环境下,声明的 function 不会放在 global 全局对象下,因此在 foo 函数里调用 this.bar 函数会报TypeError: this.bar is not a function错误。要想运行不报错,调用 bar 函数时省去前面的 this。
    1
    2
    3
    4
    5
    6
    7
    8
    function foo(){
    var a = 2;
    this.bar();
    }
    function bar(){
    console.log(this.a);
    }
    foo();

    This 四种绑定规则

    默认绑定

    当函数调用属于独立调用(不带函数引用的调用),无法调用其他的绑定规则,我们给它一个称呼 “默认绑定”,在非严格模式下绑定到全局对象,在使用了严格模式 (use strict) 下绑定到 undefined。

严格模式下调用

1
2
3
4
5
6
7
'use strict'
function demo(){
// TypeError: Cannot read property 'a' of undefined
console.log(this.a);
}
const a = 1;
demo();

非严格模式下调用
在浏览器环境下会将 a 绑定到 window.a,以下代码使用 var 声明的变量 a 会输出 1。

1
2
3
4
5
function demo(){
console.log(this.a); // 1
}
var a = 1;
demo();

以下代码使用 let 或 const 声明变量 a 结果会输出 undefined

1
2
3
4
5
function demo(){
console.log(this.a); // undefined
}
let a = 1;
demo();

顶层对象的概念,顶层对象(浏览器环境指 window、Node.js 环境指 Global)的属性和全局变量属性的赋值是相等价的,使用 var 和 function 声明的是顶层对象的属性,而 let 就属于 ES6 规范了,但是 ES6 规范中 let、const、class 这些声明的全局变量,不再属于顶层对象的属性。

隐式绑定

在函数的调用位置处被某个对象包含,拥有上下文,看以下示例:

1
2
3
4
5
6
7
8
function child() {
console.log(this.name);
}
let parent = {
name: 'zhangsan',
child,
}
parent.child(); // zhangsan

函数在调用时会使用 parent 对象上下文来引用函数 child,可以理解为child 函数被调用时 parent 对象拥有或包含它。

隐式绑定的隐患

被隐式绑定的函数,因为一些不小心的操作会丢失绑定对象,此时就会应用最开始讲的绑定规则中的默认绑定,看下面代码:

1
2
3
4
5
6
7
8
9
10
function child() {
console.log(this.name);
}
let parent = {
name: 'zhangsan',
child,
}
let parent2 = parent.child;
var name = 'lisi';
parent2(); // lisi

将 parent.child 函数本身赋给 parent2,调用 parent2() 其实是一个不带任何修饰的函数调用,因此会应用默认绑定。

显示绑定

显示绑定和隐式绑定从字面意思理解,有一个相反的对比,一个表现的更直接,一个表现的更委婉,下面在看下两个规则各自的含义:

  • 隐式绑定:在一个对象的内部通过属性间接引用函数,从而把 this 隐式绑定到对象内部属性所指向的函数(例如上例中的对象 parent 的 child 属性引用函数 function child(){})。
  • 显示绑定:需要引用一个对象时进行强制绑定调用,js 有提供 call()、apply() 方法,ES5 中也提供了内置的方法 Function.prototype.bind
    call()、apply() 这两个函数的第一个参数都是设置 this 对象,区别是 apply 传递参数是按照数组传递,call 是一个一个传递。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function fruit(...args){
    console.log(this.name, args);
    }
    var apple = {
    name: '苹果'
    }
    var banana = {
    name: '香蕉'
    }
    fruit.call(banana, 'a', 'b') // 香蕉 [ 'a', 'b' ]
    fruit.apply(apple, ['a', 'b']) // 苹果 [ 'a', 'b' ]
    下面是 bind 绑定的示例,只是将一个值绑定到函数的 this 上,并将绑定好的函数返回,只有在执行 fruit 函数时才会输出信息,例:
    1
    2
    3
    4
    5
    6
    7
    8
    function fruit(){
    console.log(this.name);
    }
    var apple = {
    name: '苹果'
    }
    fruit = fruit.bind(apple);
    fruit(); // 苹果
    除了以上 call、apply、bind 还可以通过上下文 context,例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function fruit(name){
    console.log(`${this.name}: ${name}`);
    }
    const obj = {
    name: '这是水果',
    }
    const arr = ['苹果', '香蕉'];
    arr.forEach(fruit, obj);
    // 这是水果: 苹果
    // 这是水果: 香蕉

    new 绑定

    new 绑定也可以影响 this 调用,它是一个构造函数,每一次 new 绑定都会创建一个新对象。
    1
    2
    3
    4
    5
    6
    7
    function Fruit(name){
    this.name = name;
    }

    const f1 = new Fruit('apple');
    const f2 = new Fruit('banana');
    console.log(f1.name, f2.name); // apple banana

    优先级

    如果 this 的调用位置同时应用了多种绑定规则,它是有优先级的:new 绑定 -> 显示绑定 -> 隐式绑定 -> 默认绑定。

    箭头函数

    箭头函数并非使用 function 关键字进行定义,也不会使用上面所讲解的 this 四种标准规范,箭头函数会继承自外层函数调用的 this 绑定。
    执行 fruit.call(apple) 时,箭头函数 this 已被绑定,无法再次被修改。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function fruit(){
    return () => {
    console.log(this.name);
    }
    }
    var apple = {
    name: '苹果'
    }
    var banana = {
    name: '香蕉'
    }
    var fruitCall = fruit.call(apple);
    fruitCall.call(banana); // 苹果

    This 使用常见问题

    通过函数和原型链模拟类

    以下示例,定义函数 Fruit,之后在原型链上定义 info 方法,实例化对象 f1 和定义对象 f2 分别调用 info 方法。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function Fruit(name) {
    this.name = name;
    }
    Fruit.prototype.info = function() {
    console.log(this.name);
    }
    const f1 = new Fruit('Apple');
    f1.info();
    const f2 = { name: 'Banana' };
    f2.info = f1.info;
    f2.info()
    输出之后,两次结果是不一样的,原因是 info 方法里的 this 对应的不是定义时的上下文,而是调用时的上下文,根据我们上面讲的几种绑定规则,对应的是隐式绑定规则。
    1
    2
    Apple
    Banana

    原型链上使用箭头函数

    如果使用构造函数和原型链模拟类,不能在原型链上定义箭头函数,因为箭头函数的里的 this 会继承外层函数调用的 this 绑定。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function Fruit(name) {
    this.name = name;
    }
    Fruit.prototype.info = () => {
    console.log(this.name);
    }
    var name = 'Banana'
    const f1 = new Fruit('Apple');
    f1.info(); // Banana

    在事件中的使用

    举一个 Node.js 示例,在事件中使用时,当我们的监听器被调用时,如果声明的是普通函数,this 会被指向监听器所绑定的 EventEmitter 实例,如果使用的箭头函数方式 this 不会指向 EventEmitter 实例。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const EventEmitter = require('events');
    class MyEmitter extends EventEmitter {
    constructor() {
    super();
    this.name = 'myEmitter';
    }
    }
    const func1 = () => console.log(this.name);
    const func2 = function () { console.log(this.name); };
    const myEmitter = new MyEmitter();
    myEmitter.on('event', func1); // undefined
    myEmitter.on('event', func2); // myEmitter
    myEmitter.emit('event');

https://www.nodejs.red/整理

常见问题

  • JavaScript 七种内置类型:number、string、boolean、undefined、null、object、symbol(ES6新增加)
  • 基本类型: 指保存在栈内存中的数据
    引用类型: (对象引用)指保存在堆内存中的对象,传递的是引用的地址
  • (typeof null === ‘object’) = true正确的返回值应该是null,但是这个bug由来已久
    (undefined == null) = true
  • indexOfECMAScript5新方法,IE8及以下不支持
  • setTimeout(callback, 100),setTimeout只接受一个函数做为参数不接受闭包,因为闭包会自执行,Nodejs 下最小延迟 1ms

    undefined与undeclared的区别

    undefined: 已在作用域中声明但还没有赋值的变量是undefined。
    undeclared:还没有在作用域中声明过的变量是undeclared,对于undeclared这种情况typeof处理的时候返回的是undefined。尝试访问一个undeclared的变量,浏览器会报错,JS执行会中断。

    欺骗词法作用域

    词法作用域由写代码期间函数所声明的位置来定义,javascript有两种机制(eval()、with)在运行时来修改词法作用域,这样做通常会导致性能下降,内存泄漏问题。

  • eval函数接收一个字符串为参数,解析字符串生成代码并运行
1
2
3
4
5
6
7
8
9
10
11
function test(str, b){
eval(str);

console.log(a, b);
}

var a = 1;

test("var a = 3", 2); // 3 2

console.log(a); // 1

上面这段代码示例,eval调用的str相当于在test函数作用域内部声明了一个新的变量b,当console.log()在打印时会在foo函数内部找到a和b,将无法找到外部的a,因此最终输出结果是3和2,最外层a仍就输出是1,两者比较可以看到效果。

  • with通常被当作重复引用同一个对象中的多个属性的快捷方式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    {
    function withObj(obj){
    with(obj){
    a = 2
    }
    }

    let o1 = {
    a: 1,
    }

    let o2 = {
    b: 1,
    }

    withObj(o1);
    console.log(o1.a); // 2

    withObj(o2);
    console.log(o2.a); // undefined
    console.log(a); // 2
    }
    以上示例中withObj(obj)函数接受一个obj参数,该参数是一个对象引用,执行了with,o1传进去,a=2赋值操作找到了o1.a并将2赋值给它,o2传进去,因为o2没有a属性,就不会创建这个属性,o2.a保持undefined,这个时候就会创建一个新的全局变量a。
  • 对性能的影响
    javascript引擎在编译阶段会进行性能优化,很多优化依赖于能够根据代码词法进行静态分析,预先确定了变量和函数的定义位置,才能快速找到标识符,但是在词法分析阶段遇到了with或eval无法明确知道它们会接收什么代码,也就无法判断标识符的位置,最简单的做法就是遇到with或eval不做任何优化,使用其中一个都会导致代码运行变慢,因此,请不要使用他们。

    类型检测

  • typeof:基本类型用typeof来检测
  • instanceof:用来检测是否为数组、对象、正则
    1
    2
    3
    4
    5
    6
    7
    8
    let box = [1,2,3];
    console.log(box instanceof Array); //true

    let box1={};
    console.log(box1 instanceof Object); //true

    let box2=/g/;
    console.log(box2 instanceof RegExp); //true

    错误

  • ReferenceError错误

    如果在所有嵌套的作用域中遍寻不到所需的变量,引擎会抛出ReferenceError错误,意味这,这是一个未声明的变量,这个错误是一个非常重要的异常类型。

    1
    2
    console.log('a: ', a); // Uncaught ReferenceError: a is not defined
    let a = 2;
  • TypeError错误

    这种错误表示作用域判别成功,但是进行了非法的操作,例如,对一个非函数类型的值进行函数调用,或者引用null、undefined类型的值中的属性,将会抛出TypeError异常错误。

    1
    2
    let a = null; // 或者a = undefined
    console.log(a.b); // Uncaught TypeError: Cannot read property 'b' of null
    对一个非函数类型的值进行函数调用
    1
    2
    let a = 2;
    a(); // TypeError: a is not a function

    数组去重的三种实现方式

  • Set数组去重

    ES6新的数据结构Set,类似于数组,它的元素都是唯一的。

    1
    2
    3
    4
    5
    {
    let arr = [1, 22, 33, 44, 22, 44];

    console.log([...new Set(arr)]); //[1, 22, 33, 44]
    }
  • reduce数组对象去重

    reduce对数组中的每一个元素依次执行回调函数,不含数组中未赋值、被删除的元素,回调函数接收四个参数

  • callback:执行数组中每个值的函数,包含四个参数
    • previousValue:上一次调用回调返回的值,或者是提供的初始值(initialValue)
    • currentValue:数组中当前被处理的元素
    • index:当前元素在数组中的索引
    • array:调用 reduce 的数组
    • initialValue:可选,作为第一次调用 callback 的第一个参数。
      示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      let hash = {};

      function unique(arr, initialValue){
      return arr.reduce(function(previousValue, currentValue, index, array){
      hash[currentValue.name] ? '' : hash[currentValue.name] = true && previousValue.push(currentValue);

      return previousValue
      }, initialValue);
      }

      const uniqueArr = unique([{name: 'zs', age: 15}, {name: 'lisi'}, {name: 'zs'}], []);

      console.log(uniqueArr); // uniqueArr.length == 2

      数组降维

  • 方法一:将数组字符串化

    利用数组与字符串的隐式转换,使用+符号链接一个对象,javascript会默认调用toString方法转为字符串,再使用字符串分割成字符串数组,最后转成数值形数组

    1
    2
    3
    4
    5
    6
    let arr = [[222, 333, 444], [55, 66, 77], 11, ]
    arr += '';
    arr = arr.split(',');
    arr = arr.map(item => Number(item));

    console.log(arr); // [222, 333, 444, 55, 66, 77, 11]
  • 方法二:利用apply和concat转换

    concat() 方法用于连接两个或多个数组。该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。

    1
    2
    3
    4
    5
    6
    7
    8
    {
    function reduceDimension(arr) {
    return Array.prototype.concat.apply([], arr);
    }

    console.log(reduceDimension([[123], 4, [7, 8],[9, [111]]]));
    // [123, 4, 7, 8, 9, Array(1)]
    }
  • 方法三 自定义函数实现

    推荐使用,经测试这个是执行效率最高的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    function reduceDimension(arr){
    let ret = [];

    let toArr = function(arr){
    arr.forEach(function(item){
    item instanceof Array ? toArr(item) : ret.push(item);
    });
    }

    toArr(arr);

    return ret;
    }

    let arr = [[12], 4, [333, [4444, 5555]], [9, [111, 222]]];

    for(let i = 0; i < 100000; i++){
    arr.push(i);
    }

    let start = new Date().getTime();

    console.log('reduceDimension: ', reduceDimension(arr));
    console.log('耗时: ', new Date().getTime() - start);

地址:https://docs.github.com/en/github/authenticating-to-github/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent
1、Open Terminal.
2、Paste the text below, substituting in your GitHub email address.

1
$ ssh-keygen -t ed25519 -C "your_email@example.com"
  • Note: If you are using a legacy system that doesn’t support the Ed25519 algorithm, use:
    1
    $ ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
  • This creates a new ssh key, using the provided email as a label.
    Generating public/private ed25519 key pair.
    3、When you’re prompted to “Enter a file in which to save the key,” press Enter. This accepts the default file location.
    1
    Enter a file in which to save the key (/Users/you/.ssh/id_ed25519): [Press enter]
    4、At the prompt, type a secure passphrase. For more information, see “Working with SSH key passphrases”.
    1
    2
    Enter passphrase (empty for no passphrase): [Type a passphrase]
    Enter same passphrase again: [Type passphrase again]

[http://www.ptbird.cn/http-http1-1-http2-https.html]整理

一、http协议

地址:http://www.cnblogs.com/ranyonsue/p/5984001.html
文章内容:
1.HTTP简介
2.主要特点
3.HTTP之URL
4.URI和URL的区别
5.HTTP之请求消息Request
6.HTTP之响应消息Response
7.HTTP之状态码
8.HTTP请求方法
9.HTTP工作原理
10.GET和POST请求的区别

二、http头字段

地址: http://www.jianshu.com/p/6e86903d74f7
内容:
http头字段列表

三、http2 (包括与http1.1的比较)

地址:https://www.qcloud.com/community/article/541321
内容:
一、多路复用的单一长连接
二、头部压缩和二进制格式
三、服务端推送Server Push

四、《http2讲解》 gitbook翻译

地址: https://ye11ow.gitbooks.io/http2-explained/content/
内容:
有比较详细的对http2的介绍,但是翻译的质量很一般,不是特别建议。
上面http2 腾讯云上的文章已经足够了解http2了

四、https的内容

地址::http://www.cnblogs.com/zhuqil/archive/2012/07/23/2604572.html
内容

  • HTTPS简介
    客户端发起HTTPS请求
    服务端的配置
    传送证书
    客户端解析证书
    传送加密信息
    服务段解密信息
    传输加密后的信息
    客户端解密信息

    五、http2 图

    RUNOOB 图标

[http://www.ptbird.cn/http-cache-expires-max-age.html]整理

一、说明

  • 内容来自《前端工程化体系设计与实践》
    浏览器缓存静态资源实际上是通过 HTTP 协议缓存策略,有两种策略分别是强制缓存协商缓存
    强制缓存会根据过期时间判断是使用本地缓存还是请求新的资源。
    协商缓存每次都会发出请求,经过服务器对比之后决定采用本地缓存还是新的资源。
    使用哪种缓存策略是通过 HTTP 协议的 header 信息决定。

    二、强制缓存:Expires 和 Cache-control

    Expires 和 max-age 是强制缓存的关键的信息,都会在 http 响应的 header 信息。

1、Expires
Expires 是通过指定一耳光明确的时间点作为缓存资源的过期时间,在这个时间之前,客户端都是使用本地缓存的文件来响应 HTTP 请求,不会向服务器发出实体请求不过调试的时候能够发现这个请求,并且这个请求是 200
Expires 的优点:

  • 在缓存过期时间内减少客户端的 HTTP 请求,不仅节省客户端处理时间和提高 Web 应用执行速度,也减少了服务器负载以及客户端网络资源的损耗
    Expires 的 header 信息示例:
    1
    Expires:Wed, 23 Aug 2019 14:00:00 GMT
    上面的意思是缓存过期时间是 2019年8月23日 14:00:00
    Expires 的 缺点:
  • 指定的时间是以服务端为准但是客户端进行过期判断时是将本地的时间和这个时间进行对比
  • 如果客户端端时间和服务端时间存在差异,则会存在问题

2、Cache-control
为了解决 Expires 的缺点,HTTP 1.1 增加了新的 header 字段 Cache-control 来更精准的控制缓存常用的 Cache-control 信息有下面几个:

  • no-cache 和 no-store
  • public 和 private
  • max-age
    1)no-cache 和 no-store
    no-cache 不是禁止缓存的意思,需要先和服务器确认返回的响应时否发生了变化,如果资源没变化,使用缓存的副本
    no-store 是禁止缓存,每次资源请求都会向服务器发送新的请求
    2)public 和 private
    public 表示可以被浏览器和中间CDN缓存(一般不用,都是使用 max-age),而
    private 表示可以被浏览器缓存但是不能被 CDN 缓存
    3)max-age
    这个是最重要的 Cache-control 信息,一般都会通过 max-age 控制缓存的有效时长。
    从请求的时刻开始计算,能够控制最长保留多久,单位是 s。
    比如 max-age=3600 表示浏览器在1小时内使用缓存,不会发送实体请求到服务器。
    (http://www.ptbird.cn/usr/uploads/2019/07/88767257.png)
    相比于 Expires ,max-age 通过控制时间长度而不需要与服务端时间戳进行计算,控制的更加精准,没有时间误差。

3、没有指定 no-cache 的缓存判断流程图
RUNOOB 图标

三、协商缓存

1、Etag
Etag 是服务器给资源分配的字符串形式唯一性标识,作为响应的 header 信息返回给浏览器,浏览器在 Cache-control指定 no-cache 或者是 max-age 和 expires 都过期的情况下,将 Etag 值通过 If-none-match 作为请求首部信息发送给服务器,服务器接收到请求之后,对比锁清秋资源的 Etag 值是否改变:

  • 如果没改变,会返回 304 Not Modified ,并且根据之前的缓存策略分配新的 Cache-control 信息
  • 如果发生了改变,会返回新的资源并且分配新的 Etag
    RUNOOB 图标
    如果要强制使用协商缓存,则需要将 Cache-control 设置为 no-cache,这样不回去判断 max-age 和 Expires ,每次都会经过服务器的 Etag 对比。
    RUNOOB 图标
    协商缓存并非比强制缓存低级,而是要看使用场景,在 HTML 文件场景下,如果一个 URL www.example.com/index.html 其中 index.html 是不能强制缓存的,因为要保证内容的实时更新,因此必须使用协商缓存

1
2
3
4
气沉丹田
知识体系扫描
合并别人笔记
容易记忆