高阶面向对象

Misaka10032Javascript高阶Class闭包原型链大约 29 分钟

值类型和引用类型

判断方式

typeof 判断值类型

instanceof 判断引用类型

存储机制

基本数据类型存放于栈内存。在 JS 中用于在编译器和内存中保存变量、方法调用。

引用类型存放于堆内存。它的赋值是堆内存地址的引用(指针), 所以两个变量指向的还是同一个对象,对任何一个的操作都会相互的影响。(浅拷贝概念)。

实例对象创建方法

1.new 操作符 + 2.对象字面量表示

代码块解析

对于采用大括号表示的语句,JS 一律解释为代码块。

在大括号外加上圆括号,解释为对象。

静态方法

点表示法和方括号表示法

  1. 方括号表示法总是能代替点表示法,但点表示法却不一定能全部代替方括号表示法。

  2. 方括号表示法可以用变量名作为属性名,点表示法不能。

  3. 方括号表示法可以用纯数字为属性名,点表示法不能。

  4. 方括号表示法可以用打空格的字符串为属性名,点表示法不能。

in 运算符

in 运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返回 true, 否则返回 false。它的左边是一个字符串,表示属性名,右边是一个对象。

hasOwnProperty

hasOwnProperty(propertyName)方法 是用来检测属性是否为对象的自有属性,如果是,返回 true,否者 false; 参数 propertyName 指要检测的属性名; 用法:object.hasOwnProperty(propertyName) // true/false

hasOwnProperty() 方法是 Object 的原型方法(也称实例方法),它定义在 Object.prototype 对象之上,所有 Object 的实例对象都会继承 hasOwnProperty() 方法。

hasOwnProperty() 只会检查对象的自有属性,对象原形上的属性其不会检测;但是对于原型对象本身来说,这些原型上的属性又是原型对象的自有属性,所以原形对象也可以使用 hasOwnProperty()检测自己的自有属性;

属性遍历

它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。

它不仅遍历对象自身的属性,还遍历继承的属性。

如果继承的属性是可遍历的,那么就会被 for...in 循环遍历到。但是,一般情况下,都是只想遍 历对象自身的属性,所以使用 for...in 的时候,应该结合使用 hasOwnProperty 方法,在循环内部判 断一下,某个属性是否为对象自身的属性。

属性查看

查看一个对象本身的所有属性,可以使用 Object.keys 方法。该方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和使用 for...in 循环遍历该对象时返回的顺序一致。

属性名获取

返回一个数组,该数组对元素是 obj 自身拥有的枚举或不可枚举属性名称字符串。

var arr = ["a", "b", "c"];
console.log(Object.getOwnPropertyNames(arr).sort()); // ["0", "1", "2", "length"]

// 类数组对象
var obj = { 0: "a", 1: "b", 2: "c" };
console.log(Object.getOwnPropertyNames(obj).sort()); // ["0", "1", "2"]

// 使用 Array.forEach 输出属性名和属性值
Object.getOwnPropertyNames(obj).forEach(function (val, idx, array) {
  console.log(val + " -> " + obj[val]);
});
// 输出
// 0 -> a
// 1 -> b
// 2 -> c

//不可枚举属性
var my_obj = Object.create(
  {},
  {
    getFoo: {
      value: function () {
        return this.foo;
      },
      enumerable: false,
    },
  }
);
my_obj.foo = 1;

console.log(Object.getOwnPropertyNames(my_obj).sort()); // ["foo", "getFoo"]

属性删除

delete 命令删除对象 obj 的 p 属性。删除后,再读取 p 属性就会返回 undefined,而且 Object.keys 方法的返回值也不再包括该属性。

注意:

  1. 删除一个不存在的属性,delete 不报错,而且返回 true。

  2. delete 命令只能删除对象本身的属性,无法删除继承的属性。

get&set

对象中有 get 和 set 方法,在读取和设定值的时候触发。vue 中的数据绑定就是通过这个来实现的。

  1. 直接在对象内使用

get 用法:

var user = {
  info: {
    name: "张三",
  },
  get name() {
    return this.info.name;
  },
};
console.log(user.info.name); // '张三'
console.log(user.name); // '张三'

作用:

  1. 在对象内属性嵌套层级过多时,可以直接在对象下读取到对应属性,简化调用;

  2. 在 get 时可以任意设置属性名,可以不暴露组件内部属性名。

set 用法:

var user = {
  info: {
    name: "张三",
  },
  set name(val) {
    console.log("我改名了");
    this.info.name = val;
  },
};
console.log(user.name); // '张三'

user.name = "李四"; // '我改名了'
console.log(user.name); // '李四'

作用:

  1. 在对象内属性嵌套层级过多时,可以直接在对象下设置到对应属性,简化层级;
  2. set 方法内的逻辑在赋值时会自动执行,可以监听属性值的改变

  1. 使用 Object.defineProperty()

方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

Object.defineproperty( object,‘ propName ’ ,descriptor);

参数:

  • object :要定义或修改属性的对象;

  • propName :要定义或修改的属性的名称;

  • descriptor:要定义或修改的属性描述符。

descriptor四个属性:

value:设置属性的值,默认undefined

writable:值是否可重写。默认false

enumerable:目标属性是否可以被枚举。默认false

configurable:目标属性是否可以被删除或是否可以再次修改特性。默认false

var user = {
  user_name: "张三",
};

Object.defineProperty(user, "name", {
  get() {
    return user.user_name;
  },
  set(val) {
    console.log("我改名了");
    user.user_name = val;
  },
});

console.log(user.name); // '张三'
user.name = "王二"; // '我改名了'

console.log(user.name); // '王二'
console.log(user.user_name); // '王二'

作用: set 方法可以监听对应属性值的改变,vue 的数据动态绑定就是通过这个方法实现的,监听到 vue 实例中的 data 属性发生改变时,在 set 方法中触发模版重新渲染逻辑。


  1. 使用 Object.defineProperties()
var user = {
  name: "张三",
};

Object.defineProperties(user, {
  nameGet: {
    value: function () {
      console.log("读取");
      return this.name;
    },
  },
  nameSet: {
    value: function (name) {
      console.log("设置");
      this.name = name;
    },
  },
});

console.log(user.nameGet); // '读取'  '张三'
user.nameSet = "王二"; // '设置'

console.log(user.nameSet); // '王二'

作用: 和方法 1 直接在对象中设置效果和原理相似

其他静态方法

(1)对象属性模型的相关方法

Object.getOwnPropertyDescriptor():获取某个属性的描述对象。

Object.defineProperty():通过描述对象,定义某个属性。

Object.defineProperties():通过描述对象,定义多个属性。

(2)控制对象状态的方法

Object.preventExtensions():防止对象扩展。

Object.isExtensible():判断对象是否可扩展。

Object.seal():禁止对象配置。

Object.isSealed():判断一个对象是否可配置。

Object.freeze():冻结一个对象。

Object.isFrozen():判断一个对象是否被冻结。

(3)原型链相关方法

Object.create():该方法可以指定原型对象和属性,返回一个新的对象。

Object.getPrototypeOf():获取对象的 Prototype 对象。

实例方法

toString & valueOf

js 中一切皆对象,Number、string、boolean 都有 tostring 和 valueof,但是 null 和 undefined 没有。

  1. toString()

返回一个表示该对象的字符串。

每个对象都有一个 toString() 方法,当对象被表示为文本值时或者当以期望字符串的方式引用 对象时,该方法被自动调用。

  1. valueOf()

返回指定对象的原始值

调用 valueOf() 方法用来把对象转换成原始类型的值(数值、字符串和布尔值) 默认情况下, valueOf() 会被每个实例对象(Object)继承。

  1. toString()和 valueOf()同时存在
  • 函数

当函数 fn 用+连接一个字符串或者是数字的时候,如果我们没有重新定义 valueOf 和 toString, 其隐式转换会调用默认的 toString()方法,将函数本身内容作为字符串返回; 如果我们自己重新定义 toString/valueOf 方法,那么其转换会按照我们的定义来,其中 valueOf 比 toString 优先级更高。

  • 对象/数组

和函数结果一样。

  • Date

默认调用 toString()。字符串拼接调用 toString

涉及数值类型类型转换调用 valueOf()。

总结

toString:所有方法均可调用,自动调用。可覆盖。

valueOf:如果对象没有原始值,返回对象本身。

同时存在:默认 toString,覆盖时 valueOf 优先级更高。

特殊情况:alert() []调用 toString date()

数据类型转换

强制转换

  1. Number

Number:原值

String:

1.可以被解析为数值,则转为数值;否则 NaN

2.空字符串转为 0

Boolean:true 为 1,false 为 0

undefined:NaN

null:0

Object:Number 方法的参数是对象时,返回 NaN,除非对象为单元素数值数组 [10]。

转换规则:

1)调用对象自身的 valueOf 方法。如果返回原始类型的值,则直接对该值使用 Number 函数, 不再进行后续步骤。

2)如果 valueOf 方法返回的还是对象,则改为调用对象自身的 toString 方法。如果 toString 方法返回原始类型的值,则对该值使用 Number 函数,不再进行后续步骤。

3)如果 toString 方法返回的是对象,就报错。


  1. 特有 toString 方法

1)toString 方法可以接受一个参数,表示输出的进制。

2)数字调用 tostring 要加括号(这样表明后面的点表示调用对象属性), 不加括号,这个点 会被 JavaScript 引擎解释成小数点,从而报错。

3)只要能让 JavaScript 引擎 不混淆小数点和对象的点运算符,各种写法都能用。


  1. String

1.原始类型

不解释

2.对象

对象,返回一个类型字符串;数组,返回该数组的字符串形式。

转换规则:

1)先调用对象自身的 toString 方法。如果返回原始类型的值,则对该值使用 String 函数,不 再进行以下步骤。

2)如果 toString 方法返回的是对象,再调用原对象的 valueOf 方法。如果 valueOf 方法返回原 始类型的值,则对该值使用 String 函数,不再进行以下步骤。

3)如果 valueOf 方法返回的是对象,就报错。


  1. Boolean

undefined null -0 +0 NaN ""(空串) 全为 false

其他全为 true

所有对象(包括空对象)转换结果都是 true

自动转换

  1. 不同类型数据互相运算。

  2. 非布尔值类型数据求布尔值。

  3. 非数值类型的值使用一元运算符(+ - )。

  4. 自动转换规则:

预期什么类型的值,就调用该类型的转换函数。

  1. 自动转换为布尔值

JavaScript 遇到预期为布尔值的地方(比如 if 语句的条件部分),就会将非布尔值的参数自动 转换为布尔值。系统内部会自动调用 Boolean 函数。

  1. 自动转换为字符串

遇到预期为字符串的地方,就会将非字符串的值自动转为字符串。

先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串。

  1. 自动转换为数值

遇到预期为数值的地方,就会将参数值自动转换为数值。系统内部会自动调用 Number 函数。 除了加法运算符(+)有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值。

null 转数值为 0,undefined 转数值为 0

基本包装类型

对象是 JavaScript 语言最主要的数据类型,在 js 中三种原始类型的值——数值、字符串、布 尔值——在一定条件下,也会转为对象,也就是原始类型的“包装对象”(wrapper)。

所谓“包装对象”,指的是与数值、字符串、布尔值分别相对应的 Number、String、Boolean 三 个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。

在读取字符串、数字和布尔值的属性值或方法(实际上是 它们对应包装对象的属性值或方法)表现的像对象一样。但是如果你试图给属性赋值,则会忽略这 个操作:修改只是发生在临时对象身上,而这个临时对象并不会继续保留下来,意思是不允许对包 装对象 设置属性,他的属性是只读的比如 length。

设计目的

首先是使得“对象”这种类型可以覆盖 JavaScript 所有的值,整门语言有一个通用的数据模型, 其次是使得原始类型的值也有办法调用自己的方法。

实例方法

数值的自定义方法,只能定义在它的原型对象 Number.prototype 上面,数值本身是无法自定义属性的。

valueOf() toString()

原始类型与实例对象自动转换

某些场合,原始类型的值会自动当作包装对象调用,即调用包装对象的属性和方法。这时, JavaScript 引擎会自动将原始类型的值转为包装对象实例,并在使用后立刻销毁实例。

执行机制分析

JS 引擎的执行过程,分为三个阶段:

语法分析 → 预编译阶段 → 执行阶段

说明:浏览器先按照 js 的顺序加载< script >标签分隔的代码块,js 代码块加载完毕之后,立刻进入 到上面的三个阶段,然后再按照顺序找下一个代码块,再继续执行三个阶段,无论是外部脚本文件 (不异步加载)还是内部脚本代码块,都是一样的,并且都在同一个全局作用域中。

语法分析

js 的代码块加载完毕之后,会首先进入到语法分析阶段,该阶段的主要作用: 分析该 js 脚本代码块的语法是否正确,如果出现不正确会向外抛出一个语法错误 (syntaxError),停止该 js 代码的执行,然后继续查找并加载下一个代码块;如果语法正确,则 进入到预编译阶段。

预编译阶段

js 代码块通过语法分析阶段之后,语法都正确的下回将进入预编译阶段。

1.运行环境

运行环境分类:1.全局环境;2、函数环境;3、eval 环境(不建议使用)。

每进入到一个不同的运行环境都会创建 一个相应的执行上下文(execution context),那么 在一段 js 程序中一般都会创建多个执行上下文,js 引擎会以栈的数据结构对这些执行进行处理,形 成函数调用栈(call stack),栈底永远是全局执行上下文(global execution context),栈顶则 永远是当前的执行上下文。

2.执行环境

  1. 栈数据结构

先进后出、后进先出。


  1. EC 执行环境(执行上下文)Execution Context

创建变量对象 → 创建作用域链 → 确定 this 指向。

  • 变量对象 VO(Variable Object)

过程:

创建 arguments 对象 → 检查 function 函数声明创建属性 → 检查 var 变量声明创建属性

1、 创建 arguments 对象,检查当前上下文的参数,建立该对象的属性与属性值,仅在函数环境 (非箭头函数)中进行的,全局环境没有此过程。

2、 检查当前上下文的函数声明,按照代码顺序查找,将找到的函数提前声明,如果当前上下文的 变量对象没有该函数名属性,则在该变量对象以函数名建立一个属性,属性值则指向该函数所在堆 内存地址引用,如果存在,则会被新的引用覆盖掉。

3、 检查当前上下文的变量声明,按照代码顺序查找,将找到的变量提前声明,如果当前上下文的 变量对象没有变量名属性,则在该变量对象以变量名建立一个属性,属性值为 undefined;如果存 在,则忽略该变量声明。

说明:在全局环境中,window 对象就是全局执行上下文的变量对象,所有的变量和函数都是 window 对象的属性方法。

函数声明提前和变量声明提升是在创建变量对象中进行的,且函数声明优先级高于变量声明

  • scope 属性 指向作用域

作用域链由当前执行环境的变量对象(未进入到执行阶段前)与上层环境的一系列活动对象组成, 保证了当前执行环境对符合访问权限的变量和函数有序访问。

1、 作用域链的第一项永远是当前作用域(当前上下文的变量对象或者活动对象);

2、 最后一项永远是全局作用域(全局上下文的活动对象);

3、 作用域链保证了变量和函数的有序访问,查找方式是沿着作用域链从左至右查找变量或者函 数,找到则会停止找,找不到则一直查找全局作用域,再找不到就会排除错误。

  • this 指针

1、 在全局环境下,全局执行的上下文中变量对象的 this 属性指向为 window;

2、 在函数环境下的 this 指向比较灵活,需要根据执行环境和执行方法确定,列举典型例子来分析


  1. 执行环境栈(Execution Context Stack)

当一个脚本第一次执行的时候,js 引擎会解析这段代码,并将其中的同步代码按照执行顺序加 入执行栈中,然后从头开始执行。如果当前执行的是一个方法,那么 js 会向执行栈中添加这个方法 的执行环境,然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返 回结果后,js 会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。。这个过程 反复进行,直到执行栈中的代码全部执行完毕。

  1. AO 激活对象(Active Object)

有了变量对象存每个上下文中的东西,但是它什么时候能被访问到呢?就是每进入一个执行上 下文时,这个执行上下文儿中的变量对象就被激活,也就是该上下文中的 函数标示符、形参 arguments、变量声明等就可以被访问到了。

其实变量对象和激活对象都是一个东西,只是变量对象是全局,激活对象是函数内部。

执行环境的作用

除此之外,在 js 代码的任何位置打印 this,它总是有值的,尽管大多数情况下 this 都指向 window 对象(this 的具体指向以后会提到),这说明 this 也是在执行环境中就准备好的。

现在能清楚执行环境中准备了哪些东西了。

变量声明、函数声明、arguments(函数体)、this,以上内容都会被保存在变量对象中。由此可见,执行环境的作用就是生成一个变量对象,将代码执行过程中可能用到的所有变量都提前准备好。

执行流和执行环境

JS 执行流其实是一个压栈出栈的过程——执行上下文栈

执行环境
执行环境

1.JavaScript 执行在单线程上,所有的代码都是排队执行。

2.一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。

3.每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。

4.浏览器的 JS 执行引擎总是访问栈顶的执行上下文。

5.全局上下文只有唯一的一个,它在浏览器关闭时出栈。

执行环境的生命周期

总生命周期:创建 → 执行 → 出栈等待销毁

创建阶段:

  • 创建作用域链

  • 创建变量对象 AO:初始化函数参数 arguments→ 初始化函数声明 → 初始化变量。

  • 执行阶段:执行变量赋值、代码执行。

  • 回收阶段:执行上下文出栈等待,回收执行。

执行环境生命周期
执行环境生命周期

执行阶段

解释阶段

词法分析:JavaScript 中在调用函数的那一瞬间之前,会先进行词法分析(当函数调用的前一瞬间, 会先形成一个激活对象:Avtive Object(AO),并进行分析)。

JS 词法分析分为 3 个步骤:

1.分析形参 →2.分析变量声明 →3.分析函数声明

作用域规则确定:判断一个变量是什么时,根据它定义时的作用域,代码在解释阶段就要确定好。

执行阶段

创建执行上下文 → 执行函数代码 → 垃圾回收

JS 代码整体运行分为两个阶段:词法分析期和运行期。在 JS 代码自上而下执行前,会有一个“词法分析过程”。

词法分析主要有 3 个步骤:分析函数参数、分析变量声明、分析函数声明。

具体步骤:

1.分析函数参数

将函数的参数添加为 AO 属性,属性默认值为 undefined。

接收函数的实参,覆盖原属性值。

2.分析变量声明/分析局部变量

若 AO 中不存在与声明的变量所对应的属性,则添加 AO 属性为 undefined。

若 AO 中已存在与声明的变量所对应的属性,则不做任何修改。

3.若 AO 中存在与函数名所对应的属性,则覆盖原属性为一个函数表达式。

Class 类

继承

(1)class 关键字创建,首字母大写

(2)构造函数(有参):constructor()

(3)constructor 不写也会自动生成。new 的时候自动调用

(4)生成实例 new 不能省略。

(5)注意语法规范,创建类,类名后面不要加小括号。实例后面加小括号,构造函数不需要 function。

类的继承

子类可继承父类构造方法之外的方法

super():访问和调用对象父类的函数,可以调用父类的构造函数也可以调用普通函数。

方法重写

指继承父类的子类将父类的同名方法重写为子类独有方法

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  say() {
    return "我的名字是" + this.name;
  }

  set birth(value) {
    this._birth = `${value}-`;
  }
  get birth() {
    return this._birth;
  }
}

class Animal extends Person {
  // 继承拿到所有Person的属性
  constructor(name, age) {
    super(name, age); // 继承name
    this.age1 = age; // 通过继承的属性新建一个属性
  }
  say() {
    // 重写say这个函数
    return "My name is" + this.name;
  }
}
const animal = new Animal("嘻嘻", 5); // 实例化对象并赋值
const person = new Person("哈哈", 11);
console.log("animal", animal);
console.log("person", person);
person.birth = 123; // 相当于用set赋值
console.log("birth", person.birth); // get拿到值
console.log("birth", person.say);
console.log("birth", animal.say);

super

super 作为函数使用,代表父类的构造函数,只能用在子类的构造函数中;

super 作为对象使用,在普通方法之中指向父类的原型对象,在静态方法之中指向父类;在普通方法中调用父类方法,super 内部的 this 指向子类的实例,在静态方法中指向子类。

本质

类的本质还是 function,也可以简单认为类就是构造函数另一种写法。

1.类有原型对象 prototype

2.类原型对象 prototype 里面有 constructor 指向类本身

3.类可以通过原型对象添加方法

4.类创建的实例对象有__proto__原型指向类的原型对象

ES6 类其实就是语法糖(一种便捷写法,简单理解,有两种方法可以实现同样的功能,更加清晰方便的方法就是语法糖)

构造函数

构造函数习惯首字母大写

构造函数和普通函数区别就是调用方式不同,普通函数是直接调用,而构造函数需要 new 来调用。

执行流程:

立刻创建一个新对象 → 新建对象设置为 this→ 逐行执行函数中的代码 → 将新建对象返回

instance of 与 java 一致,所有对象都是 Object 的后代。

方法理解:

在 Person 构造函数中,为每一个对象都添加了一个方法,构造函数每执行一次就会创建一个新的方法,所有实例的方法都是唯一的。

重点:函数体写在对象内部 or 全局作用域

1.首字母大写

2.和 new 一起使用

function Man(name) {
  this.name = name;
  this.age = 18;
}
var man = new Man("张三"); // 创建一个name名为张三的构造函数Man

实例成员和静态成员

  1. 实例成员就是构造函数内部通过 this 添加的成员。只能通过实例化的对象访问。

  2. 静态成员是指在构造函数本身上添加的成员,只能通过构造函数访问。不能通过对象访问。

构造函数原型 prototype

我们所创建的每一个函数,解析器都会向函数中添加一个属性 prototype。这个属性对应着一个对象,这个对象就是我们所谓的原型对象。

1.作为普通函数调用 prototype 没有任何作用。

2.以构造函数调用,创建对象中隐含属性指向该构造函数的原型对象,通过 __proto__ 来访问该属性。

3.当我们访问对象的一个属性或方法时,它会先在对象自身中寻找,如果没有则在原型对象中寻找,如果找到则直接使用。如果原型中没有则去原型的原型中寻找,直到找到 Object 的原型,Object 的原型没有原型,如果在 Object 中仍然没有找到,则返回 undefined。

4.创建构造函数时,可以将这些对象共有的属性和方法,统一添加到构造函数的原型对象中,这样不用分别为每一个对象添加,也不会影响到全局作用域,就可以使每个对象都具有属性和方法。

对象原型 __proto__

对象有一个属性__proto__,指向 prototype 原型对象。

__proto__对象原型和原型对象 prototype 等价

__proto__对象原型的意义在于为对象的查找机制提供一个方向,但属于非标准属性,实际开发中不可以使用这个属性,它只是指向原型对象 prototype。

constructor 构造函数

在 JavaScript 中, constructor 属性返回对象的构造函数。

返回值是函数的引用,不是函数名:

JavaScript 数组 constructor 属性返回 function Array() { [native code] }

JavaScript 数字 constructor 属性返回 function Number() { [native code] }

JavaScript 字符串 constructor 属性返回 function String() { [native code] }

如果一个变量是数组你可以使用 constructor 属性来定义。

原型链(重要)

原型对象三角关系
原型对象三角关系
原型链
原型链

成员查找规则

  1. 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。

  2. 如果没有就查找它的原型(也就是 __ proto __ 指向的 prototype 原型对象)。

  3. 如果还没有就查找原型对象的原型(Object 的原型对象)。

  4. 以此类推一直找到 Object 为止(null)。

  5. __ proto __ 对象原型的意义在于为对象成员查找机制提供方向。

扩展内置对象须知

数组和字符串内置对象不能给原型对象覆盖操作Array.prototype = {},只能是Array.prototype.xxx = function() {}的方式。

函数进阶

调用方式

普通函数、对象方法、构造函数、绑定时间函数、定时器函数、立即执行函数。

this 指向(重要)

调用方式this 指向
普通函数调用window
构造函数调用实例对象,原型对象里面的方法也指向实例对象
对象方法调用该方法所属对象
事件绑定方法绑定事件对象
定时器函数window
立即执行函数window

解析器在调用函数每次都会向函数内部传递进一个隐含的参数,这个隐含参数就是 this,this 指向的是一个对象,这个对象称为函数执行的上下文对象。根据函数的调用方式不同,this 会指向不同对象。

1.以函数的形式调用时,this 永远都是 window;2.以方法的形式调用时,this 就是调用方法的那个对象

2.this 适用于函数作为某个对象的方法存在时使用,用于表示该对象。

3.使用工厂方法批量创建对象,使用的构造函数都是 Object,导致我们无法区分多种不同类型对象。

this 总结

1.当以函数形式调用时,this 就是 window。

2.当以方法的形式调用时,谁调用方法 this 就是谁

3.当以构造函数形式调用时,this 就是新创建的那个对象。

改变 this 指向

call

call()方法调用一个函数,简单理解为一次函数的立即调用,但它可以改变函数的 this 指向。

fun.call(thisArg, arg1, arg2, ...) // 第二个参数开始表示执行该函数传递的参数
  • thisArg:在 fun 函数运行时执行的 this 值

  • arg1, arg2:传递的其他参数

  • 返回值就是函数的返回值,因为它就是一次函数调用

apply

apply()方法调用一个函数,简单理解为一次函数的立即调用,但它可以改变函数的 this 指向。

fun.apply(thisArg, [argsArray]);
  • thisArg:在 fun 函数运行时指定的 this 对象

  • argsArray:传递的参数,必须以数组形式呈现

  • 返回值就是函数的返回值,因为它就是一次函数调用

bind

bind()方法不会调用函数,但是能改变函数内部 this 指向

fun.bind(thisArg, arg1, arg2. ...)
  • thisArg:在 fun 函数运行时执行的 this 值

  • arg1, arg2:传递的其他参数

  • 返回由指定的 this 值和初始化参数改造的原函数拷贝

小结

相同点:都可以改变函数内部的 this 指向。

区别:

1.call 和 apply 会调用函数,并且改变函数内部 this 指向。

2.call 和 apply 传递参数不一样,call 传递参数,apply 必须数组形式传参。

3.bind 不会调用函数,可以改变函数内部 this 指向。

主要应用场景:

1.call 经常做继承。

2.apply 经常跟数组有关系,比如借助于 Math 求值。

3.bind 不调用函数,能改变 this 指向,比如改定时器内部的 this 指向。

严格模式

严格模式是采用具有限制性 JavaScript 变体的一种方式,即在严格的条件下运行 JS 代码。

严格模式在 IE10 以上的版本浏览器中才会被支持,旧版本浏览器会被忽略。

严格模式的更改:

1.消除了 JavaScript 语法的一些不合理、不严谨之处,减少了一些怪异行为。

2.消除代码运行的一些不安全之处,保证代码运行的安全。

3.提高编译器效率,增加运行速度。

4.禁用了 EMCAScript 的未来版本中可能定义的一些语法,为未来新版本 JS 做铺垫。一些保留字:class、enum、export 等不能做变量名。

开启严格模式

分为 为脚本开启严格模式、为函数开启严格模式。

<script>
  "use strict";
  console.log("这是严格模式");
</script>

将整个脚本文件放在一个立即执行函数中开启严格模式,独立创建作用域不影响其他脚本文件。

(function () {
  "use strict";
  var num = 10;
  function fn() {}
})();

把"use strict"声明放在函数体所有语句之前,仅该函数体内执行严格模式。

严格模式简要规则

  1. 不能使用未定义的变量

  2. 不允许删除变量或者对象或者函数

  3. 不允许参数重名

  4. 禁止 this 关键字指向全局对象

闭包(重要)

定义

闭包指有权访问另一个函数作用域中变量的函数。

简单理解:一个作用域可以访问另一个函数内部的局部变量。

概念:一个函数 f 内创建另一个函数 f1,f1 可以访问到 f 的局部变量,返回 f1。

案例:利用闭包获得 ul>li 的索引号

for (var i = 0; i < lis.length; i++) {
  (function (i) {
    lis[i].onclick = function () {
      console.log(i);
    };
  })(i);
}

立即执行函数也称为小闭包,因为立即执行函数里任何一个函数都可以使用它的 i 变量。

闭包不一定是最优选项,容易造成变量短期内无法销毁,易造成内存泄漏。

作用

延伸变量的作用范围。

递归

一个函数在内部调用其本身,函数自己调用自己本身,或者在自己函数调用的下级函数中调用自己。

必须加退出条件 return,否则容易堆栈溢出。

步骤

  1. 假设递归函数已经写好

  2. 寻找递推关系

  3. 将递推关系的结构转换为递归体

  4. 将临界条件加入到递归体中

案例 1:求和

function sum(n) {
  if (n == 1) return 1;
  return sum(n - 1) + n;
}

案例 2:斐波拉契数列

// 递归方法
function fib(n) {
  if (n === 1 || n === 2) return n - 1;
  return fib(n - 1) + fib(n - 2);
}
console.log(fib(10)); // 34
//非递归方法 //
function fib(n) {
  let a = 0;
  let b = 1;
  let c = a + b;
  for (let i = 3; i < n; i++) {
    a = b;
    b = c;
    c = a + b;
  }
  return c;
}
console.log(fib(10)); // 34

案例 3:爬楼梯(动态规划)

假如楼梯有 n 个台阶,每次可以走 1 个或 2 个台阶,请问走完这 n 个台阶有几种走法

function climbStairs(n) {
  if (n == 1) return 1;
  if (n == 2) return 2;
  return climbStairs(n - 1) + climbStairs(n - 2);
}

对象拷贝

浅拷贝(单层)

仅第一层对象脱离原址,第二层对象仍为引用。

let obj = {
  id: 1,
  name: "ZhangSan",
  msg: {
    age: 18,
    address: "China",
  },
};
let newObj = {};
for (const key in obj) {
  newObj[key] = obj[key];
}
obj.msg.age = 20;
console.log(newObj);

深拷贝

通过递归调用,实现全部对象内容拷贝,全部脱离原引用地址。

function deepCopy(newObj, oldObj) {
  for (const key in oldObj) {
    if (oldObj[key] instanceof Array) {
      let tempObj = [];
      newObj[key] = deepCopy(tempObj, oldObj[key]);
    } else if (oldObj[key] instanceof Object) {
      let tempObj = {};
      newObj[key] = deepCopy(tempObj, oldObj[key]);
    } else newObj[key] = oldObj[key];
  }
  return newObj;
}
let obj = {
  id: 1,
  name: "ZhangSan",
  msg: {
    age: 18,
    address: "China",
  },
  arr: [
    {
      student: "LiHua",
      code: 0,
    },
    {
      student: "LiLei",
      code: 1,
    },
  ],
};
let newObj = {};
let deepObj = deepCopy(newObj, obj);
obj.msg.age = 20;
console.log(newObj);