JavaScript复习

[toc]

复习

一、call和apply

call和apply是用来指定上下文运行函数的

  • 我们写了一个函数,比如:
    1
    2
    3
    function fn(){
    ...
    }

此时如果要运行它,可以直接加圆括号运算符:

1
fn()

此时函数的上下文就是window对象。所谓的上下文就是函数中出现的this是谁(此处指DOM开发中,node中的对象无法打印出来)。但很多时候我们需要让函数指定上下文运行,此时就要使用call和apply,它俩功能完全一样!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 改变某个值
// fn是一个函数,功能是让上下文对象的a属性变为100,但这个函数到底给谁的a属性变为了100,此时不知道,要看函数调用时指定的上下文是谁
function fn(){
this.a = 100;
}
var xiaoming = {
a : 8,
b : 9
}
// 此时想把xiaoming的a值变成100,思路是让xiaoming成为fn的this!
//运行fn函数,同时指定xiaoming对象是fn函数的上下文
fn.call(xiaoming);
console.log(xiaoming); // {a:100,b:9}

// fn.apply(xiaoming);
// console.log(xiaoming); // 结果和call是一样的

示例2:call和apply的区别(只有传参时有区别! )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// fn是一个函数,功能是让上下文对象的a属性变为传入的两个参数的和
function fn(m,n){
this.a = m + n;
}
var xiaoming = {
a : 8,
b : 9
}
// call规定第一个是上下文指定的对象,后面的是参数
fn.call(xiaoming,2,3);
console.log(xiaoming); // {a:5,b:9}

// apply规定第一个是上下文指定的对象,后面用数组(或类数组的可枚举的形式)来传参
// fn.apply(xiaoming,[2,3]);

示例3:call和apply的函数委托功能 – 假设有两个函数:厨师和服务员,服务员只负责把点的菜告诉厨师,厨师只负责根据传过来的菜单做菜,最初的思路可能是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function chushi(){
for(var i=0; i<arguments.length; i++){
console.log(arguments); // 4.此处将打印出1
// arguments是所有传入的实参列表,无视函数传进来多少参数
console.log("我是厨子,我要做" + arguments[i])
}
}

function fuwuyuan(){
console.log(arguments); // 2.此处打印出3
chushi(arguments); // 3.但如果将arguments作为参数直接传给chushi的话,将作为一个整体而不会自动展开(es6中的三个点展开运算符就是为了解决这个问题而设计的)
}

fuwuyuan("宫保鸡丁","鱼香肉丝","地三鲜"); // 1.调用并传入菜单

所以此时就要应用apply

1
2
3
4
5
6
7
8
9
10
11
12
function chushi(){
for(var i=0; i<arguments.length; i++){
// arguments是所有传入的是参列表,无视函数传进来多少参数
console.log("我是厨子,我要做" + arguments[i])
}
}

function fuwuyuan(){
chushi.apply(null, arguments); // 因为chushi里面没有出现this所以不需要指定上下文,设置为null就可以了
}

fuwuyuan("宫保鸡丁","鱼香肉丝","地三鲜");

示例4:求和及平均值

1
2
3
4
5
6
7
8
9
10
11
12
13
function sum(){
for(var i=0, _sum=0; i<arguments.lenght; i++ ){
_sum += arguments[i];
}
return _sum;
}

function average(){
var _sum = sum.apply(null, arguments);
return _sum / arguments.length;
}

console.log(average(4,4,4,5,5,5));

示例5:求最大值(Math.max)

1
2
3
4
5
console.log(Math.max(4,5,6,7,8)); // 当所有的数字是散点式(每个数字用逗号隔开)的时候,可以直接写

// 但当传入的是一个数组时,就用apply
var arr = [4,5,6,7,8];
console.log(Math.max.apply(null, arr));

二、this

  1. this是什么?

    this就是函数的上下文。所以说,==函数的上下文,是除了参数之外的,最常用的使信息进入函数内部的手段==。

  2. this是谁?怎么判断?

    一定要死记:function定义的函数,this是谁,要看如何调用,而不是看如何定义!

  • 判断this的七个规则:

    • 规则1:函数直接用圆括号运行,上下文是window

      fn(); 上下文是window

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      var obj = {
      a : 3,
      fun : function(){
      var a = 5;
      return function(){
      alert(this.a);
      }
      }
      }

      obj.fun()(); // undefined,因为最终调用函数(最后一个括号)的是window,而全局没有a这个变量
    • 规则2:对象打点调用函数,上下文是这个对象

      obj.fn(); 上下文是obj

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      var obj = {
      a : 3,
      fun : (function(){
      var a = 5;
      return function(){
      alert(this.a);
      }
      })();
      }

      obj.fun(); // 3,因为最终调用函数(最后一个括号)的是obj
    • 规则3:数组(类数组对象)中枚举出函数,上下文是这个数组

      arridx; 上下文是arr

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      function fn1(fn){
      // 类数组对象中枚举出函数然后运行,上下文是这个类数组
      arguments[0](3,4);
      }
      function fn2(){
      // 也就是说fn2函数里的this居然是fn1的实际参数列表
      alert(this.length);
      }

      fn1(fn2,5,6,7,8); // 所以弹出5(一共有5个实际参数)
    • 规则4:定时器调用函数,上下文是window

    • 规则5:被当做了事件处理函数,上下文是触发事件的DOM元素
    • 规则6:用new调用函数,上下文是函数体内秘密创建的空白对象

      用new调用函数会经过四步走:

      • 秘密创建空对象
      • 将this绑定到这个空对象中
      • 执行语句
      • 返回这个对象
    • 规则7:用apply、call指定上下文

  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
    25
    26
    27
    28
    29
    30
    31
    32
    33
    function Foo(){
    function getName(){
    alert(1);
    }
    return this;
    }
    Foo.getName = function(){
    alert(2);
    }
    Foo.prototype.getName = function(){
    alert(3);
    }
    var getName = function(){
    alert(4);
    }
    function getName(){
    alert(5);
    }

    // 1
    getName();

    //2
    Foo().getName();

    //3
    new Foo().getName();

    //4
    new Foo.getName();

    //5
    new new Foo().getName();

解题思路:
1)首先排除123,不是4就是5;而函数的声明优先提升,所以先走5,然后提升变量的定义,走4,此时4会将5覆盖掉,所以答案是4
2)Foo()访问的是调用Foo的上下文,因为return的是this(与其内部闭包函数无关),这里就是window,也就是相当于window.getName(),和1一样弹出4
3)记住new xxx()的优先级是非常高的!(注意是带括号的)一定要先完成new Foo()的部分,然后再去考虑getName()的部分!new Foo()是将第一个函数做了实例化,所以这道题的考点是原型链查找,对象能够沿着原型链,访问自己构造函数prototype上的属性和方法!所以这道题的答案是3
4)如果只是new,则优先级没那么高,所以这道题要先考虑后面Foo.getName()的部分,然后再考虑new!而后面就是一个函数,就相当于用new去调了一下这个函数,所以结果是2
5)优先级问题,首先看new Foo()部分,它返回一个对象Foo,然后这个对象打点调用了getName(),也就是通过原型链调用了自己构造函数的getName方法,最后还是用new调用了一下,相当于用new调普通函数,所以结果还是3


第二道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function getLength(){
return this.length; // this未知,要看如何调用
}
function foo(){
this.length = 1; // this未知,要看如何调用
return (function(){
var length = 2;
return {
length: function(a,b,c){
return this.arr.length;
},
arr : [1,2,3,4],
info : function(){
return getLength.call(this.length); //以this.length作为上下文调用getLength函数
}
}
})();
}
var result = foo().info();
alert(result);

解题思路:foo()适用于规则1,即被window调用,此时函数foo返回一个自执行函数,也就是说foo()就等同于

1
2
3
4
5
6
7
8
9
{
length: function(a,b,c){
return this.arr.length;
},
arr : [1,2,3,4],
info : function(){
return getLength.call(this.length); //以this.length作为上下文调用getLength函数
}
}

foo().info()则符合规则2,即info中的this指的是对象本身,而对象本身的length是

1
2
3
length: function(a,b,c){
return this.arr.length;
}

而这个小函数将被call指定为getLength的上下文传入,所以最终getLength得到的长度是这个小函数的长度,函数的长度为函数的形参列表的长度,小函数的形参列表是(a,b,c),所以长度为3!(注意:函数的长度是形参列表的长度;arguments.length是函数实参列表的长度!arguments.callee表示函数本身,所以arguments.callee.length表示形参列表的长度!)


第三题

1
2
3
4
5
6
7
8
9
10
function fun(){
var a = 1;
this.a = 2;
function fn(){
return this.a;
}
fn.a = 3;
return fn;
}
alert(fun()()); // fun()时,由于是被window调用,所以this.a=2就是在全局定义了一个变量a,fun()返回的是一个函数fn,fn再执行()的结果依旧是被window调用,此时全局的a为2

第四题

1
2
3
4
function fun(){}
console.log(typeof fun); // function
console.log(fun instanceof Function); // true
console.log(fun instanceof Object); // true

wx20200222-213601@2x.png

第五题

1
2
3
4
5
6
7
8
9
var a = 1;
var obj = {
a : 2,
getA : function(){
return this.a;
}
}
var getA = obj.getA;
getA(); // 规则1,this是window,所以this.a是1

第六题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var obj = {
a:1,
b:2,
c:[{
a:3,
b:4,
c:fn
}]
}

function fn(){
alert(this.a);
}

var a=5;
obj.c[0].c(); // obj.c[0]是{a:3,b:4,c:fn}这个对象!所以obj.c[0].c()适用规则2,而这个this代表的是这个对象,所以this.a是3!

第七题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var number = 2;
var obj = {
number : 4,
fn1: (function() {
this.number *= 2;
number = number *2; // 局部变量number
var number = 3; // 此处声明提升,使局部变量number值为3
return function() {
this.number *= 2; // 此处this指的是全局的number,并且会把原来的2修改为4
number *=3;
alert(number);
}
})()
}
alert(number); // 4

var fn1 = obj.fn1;
fn1(); // 9
obj.fn1(); // 27
alert(window.number); // 8
alert(obj.number); // 8

三、算法类

  1. 数组的相关算法

数组去重:最简单的方法就是ES6中的Set数据结构。Set说白了就是不能有重复项的数组。用数组来构建Set,数组会自动去重

1
2
3
4
var arr = [1,1,1,1,2,2,2,2,3,3,3,3];

const set = new Set(arr);
console.log([...set]); // 三个.是ES6的强制结构的运算符,直接输出set的话是一个对象

传统方法

1
2
3
4
5
6
7
8
9
10
11
var arr = [1,1,1,1,2,2,2,2,3,3,3,3];
var _result = [];
function uniq(arr){
for(var i = 0; i < arr.lenght; i++){
if(!_result.includs(arr[i])){ // includs用到的是===,即如果数组中有"1"也会被保留下来
_result.push(arr[i])
}
return _result
}
}
console.log(uniq(arr));

如何看数组的时间复杂度:时间复杂度用o()来表示,n表示数组的长度。那么这个例子中的算法的时间复杂度即为o(n),表示时间复杂度随着数组长度线性变化

  1. 数组排序

冒泡排序:5个球进行排序,需要比较4趟,共比较次数4+3+2+1=10次。也就是说,长度为n的数组进行冒泡,共需要比较n-1趟,共比较wx20200225-183922@2x.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// bubbleSort不是纯函数,不是pure的,因为我们改变了传入到它内部的参数的值
function bubbleSort(arr){
for(let i =0; i< arr.length - 1; i++){
for(let j = 0; j<arr.length - 1 - i; j++){
if(arr[j] > arr[j+1]){
// 交换数字的位置,需要引入第三方变量做周转
var temp = arr[i];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;
}
var arr = [7,3,5,9,10,56,33,6];
console.log(bubbleSort(arr));

纯函数写法:浅克隆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function bubbleSort(arr){
// 先浅克隆数组
var _arr = [].concat(arr);

for(let i =0; i< arr.length - 1; i++){
for(let j = 0; j<_arr.length - 1 - i; j++){
if(_arr[j] > _arr[j+1]){
// 交换数字的位置,需要引入第三方变量做周转
var temp = _arr[i];
_arr[j] = _arr[j + 1];
_arr[j + 1] = temp;
}
}
}
return _arr;
}
const arr = [7,3,5,9,10,56,33,6];
var result = bubbleSort(arr)
console.log(arr); // 数组不变
console.log(result);

快速排序(二分法排序)思路:选择数组的第0项作为标杆,比他大的放一起,小的放一起,然后再用递归分别排两边的数组,直到顺序正确

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 quickSort(arr){
// 停止递归的条件
if(arr.length <=1){
return arr;
}
// 标杆
const pivot = arr[0];
// 比标杆大的
var bigger = [];
// 比标杆小的
var smaller = [];

// 遍历,区分大小
for(let i=1; i < arr.length; i++){
if(arr[i] >= pivot){ // >=时,i从1开始,>时i从0开始
bigger.push(arr[i]);
}else{
smaller.push(arr[i]);
}
}
return quickSort(smaller).concat(pivot, quickSort(bigger)); // 运用递归,是两个数组完成排序,注意要有递归停止条件(最上)
}
var arr = [56,332, 24,64,3,45,12,456,88,99,32];
console.log(quickSort(arr));

冒泡排序的时间复杂度是o((n²-n)/2),快速排序的时间复杂度是o(nlogn),比冒泡排序快很多

let和var的区别:let表示块级作用域,在for循环中有奇效,它可以自动创建闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var arr = [];
for(var i = 0; i < 10; i++){
arr[i] = function(){
console.log(i);
}
}

arr[5](); // 输出10,因为i是用var声明的全局变量,循环体走完后值为10

// 如果想每次输出i的值,传统做法(ise):
for(var i = 0; i < 10; i++){
function(i){ // 此时i为局部变量
arr[i] = function(){
console.log(i);
}
}(i)
}

// 新办法(用let):
for(let i = 0; i < 10; i++){ // let创建了一个小的闭包,使i只在本次循环生效
arr[i] = function(){
console.log(i);
}
}
  1. 递归:面试大致只有6种递归题型:
  • 阶乘(n!)
  • 数组扁平化
  • 深克隆
  • 数组的快速排序
  • 杨辉三角等数学模型的建立
  • 脑筋急转弯,比如不用while、for等输出1、2、3……100
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 数组扁平化
    var arr = [[1,2,3],[4,[[[[5,6],7,8]]],9],10];
    function flattenArray(arr){
    var _arr = [];
    // 每一项进行遍历,看看是常数还是数组
    for(let i = 0; i < arr.length; i++){
    // 数组的识别用isArray,typeof结果是object
    if(!Array.isArray(arr[i])){
    _arr.push(arr[i]);
    }else{
    // 如果这项是数组,那么重复这次的遍历模式(递归)
    _arr = _arr.concat(flattenArray(arr[i]));
    }
    }
    return _arr;
    }
    console.log(flattenArray(arr));

拓展功能:想都不要想,一定是用prototype

1
2
3
4
5
6
7
// 求数组最大值
Array.prototype.max = function(){
// return Math.max.apply(null, this);
return Math.max(...arr);
}
var arr = [343,23,478,980];
console.log(arr.max()); // 注意是arr.max()

函数的柯里化:函数少穿一个实参就会返回另一个函数,这个函数虚位以待,等待你随时传入最后的参数!假设有一个函数功能是求传入四个参数的和,可如果只传入三个函数,则返回NaN(undefined加任何数都是NaN),此时为避免这种情况,就需要让函数返回另一个把现有参数先加完,并随时准备接受剩余参数继续相加的函数,这个过程就叫做函数的柯里化(发明此种功能的人叫做柯里curry)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function curry(fn){
return function(){
// 备份实参
var args = arguments;
return function(){
return fn(...args, ...arguments);
}
}
}

function fun(a,b,c,d){
return a+b+c+d;
}
// 柯里化
fun = curry(fun);

var fn = fun(1,2);
console.log(fn(3,4));

深浅克隆:首先要区分基本类型值和引用类型值,如下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 基本类型值:在内存中拷贝了一份,所以源数据改变时它不跟着变
var a = 3;
var b = a;

a++;
console.log(b); // 3

// 引用类型值:在内存中共用一片堆内存,所以源数据改变时它跟着变
var a = [1,2,3,4];
var b = a;

a.push(5);
console.log(b); // [1,2,3,4,5]

基本类型值:

  • 种类:number、string、boolean、undefined
  • 特点:做变量传值的时候,内存中会复制一份。在做==判断或===判断时,仅比较值是否相当
    wx20200306-095640@2x.png
    引用类型值:
  • 种类:function、object、array、regexp、null
  • 特点:做变量传值的时候,内存中不会复制。在做==判断或===判断时,要看是否是内存中的同一个对象。
    wx20200306-095835@2x.png
    所以有经典面试题
    1
    2
    3
    [] == [] // false
    {} == {} // false
    [1] == [1] // false

那么如何复制一个数组(对象)呢?

浅克隆:只表层克隆一层,如果数组的某项也是数组,这个内层数组还是内存中的同一对象

1
2
3
4
5
6
7
var arr = [1,2,3,4,[55,66,77]];
var _arr = [];
for(let i = 0; i<arr.length; i++){
_arr.push(arr[i]);
}
console.log(_arr == arr); // false,克隆成功,现在arr和_arr已经是两个对象了
console.log(_arr[4] == arr[4]); // true,但是它们的第四项内部数组还是同一个

关于const:定义常量时,值不能改!但定义对象的话,其属性值可以改

1
2
3
4
5
6
7
8
9
function fn() {
const ceshi = {
name: 'a',
age: 1
}
ceshi.name = 'b'
return ceshi
}
console.log(fn()) // b

补充一点关于arguments的知识

1
2
3
4
5
6
7
8
9
function fn(a,b){
console.log(a === arguments[0]); // true
console.log(b === arguments[1]); // true
a = 123;
b = 456;
console.log(a === arguments[0]); // true
console.log(b === arguments[1]); // true
}
fn(3,4);

arguments表示实参类数组对象,它总是能跟着具名形参的变化而变化。当a、b重新赋值的时候,arguments[0]和arguments[1]也就同步更新 了。但是在严格模式下就不会这样:

1
2
3
4
5
6
7
8
9
10
function fn(a,b){
"use strict"
console.log(a === arguments[0]); // true
console.log(b === arguments[1]); // true
a = 123;
b = 456;
console.log(a === arguments[0]); // false
console.log(b === arguments[1]); // false
}
fn(3,4);

如果函数有默认参数,那么默认参数不算arguments的length。你传入了几个实参,长度就是几。

1
2
3
4
function fn(a, b=3){
console.log(arguments.length);
}
fn(333); // 答案是1,因为只传了一个参数

同时,当参数有默认值时,改变a和b的值,不会对arguments造成影响,并且不能进入严格模式

1
2
3
4
5
6
7
8
9
10
function fn(a, b=3){
console.log(arguments.length); // 1,因为只传了一个参数
console.log(a === arguments[0]); // true
console.log(b === arguments[1]); // false,因为arguments[1]是undefined
a = 123;
b = 456;
console.log(a === arguments[0]); // false,注意这里不会变
console.log(b === arguments[1]); // false,注意这里不会变
}
fn(333);
0%