# JavaScript | Objects 对象基础(2)

介绍JavaScript中的Objects对象的语法及相关特性。

本篇内容对于新手而言可能相对较为难以理解,建议日后在实践中加深关于本篇内容的理解

# 目录




# 对象方法

我们经常称定义于对象中的函数为“方法”,这是出于这个函数在这个对象中所扮演的角色的的称呼,而“函数”则是它的性质。

比如对于一个“人”(对象),“走路”是它的一种行为,而定义“如何走路”便是“走路”这个行为所定义的方法:双腿交替迈开(函数内的代码)。

创建对象方法的几种方式:

// 1. 直接赋值
user.sayHi = function() {
  alert("Hello!");
};

// 2. 先声明后赋值
function sayHi() {
  alert("Hello!");
};

user.sayHi = sayHi;

// 3. 对象内赋值
let user = {
  sayHi: function() {
    alert("Hello");
  }
};

// 4. 方法简写
let user = {
  sayHi() {
    alert("Hello");
  }
};

一般来说3与4较为正常,而4在绝大部分情况下都与3没有区别。故推荐使用第4种方法。


# “this”

在讲构造函数创建对象之前,我们先简单介绍一下this

如果有学过其他编程语言,你可能会察觉到 JavaScript 中的“this”关键字与其他大多数编程语言不太相同。

比如,JavaScript 的this可以用于任何函数而不会报语法错误。

function sayHi() {
  alert( this.name );
}

这是因为,JavaScript 的this在运行时才求值,其值由调用该函数的“.”点之前的对象决定。

let user = { name: "John" };
let admin = { name: "Admin" };

function sayHi() {
  alert( this.name );
}

user.f = sayHi;
admin.f = sayHi;
// 函数在两个对象之间共用

user.f();     // John  (this == user)
admin.f();    // Admin  (this == admin)

admin['f'](); // Admin(使用方括号亦可)

但如果在没有任何对象调用该函数,严格模式下的this等于undefined,这时候如果访问属性则会出错;非严格模式下的this将会是全局对象,即window

我们不应该在没有对象的情况下调用this,这一般是错误的。


# 构造函数

JavaScript 早期时候虽然没有“类”的概念,但我们可以借助构造函数来达到我们相似的效果,实现对象结构的复用。

在现代 JavaScript 中已经有 class 语法了,其本质仍然是一种特殊的函数。

# 定义规则

构造函数在技术上与一般的函数没有区别,但我们约定:

  1. 大写字母开头
  2. 只能使用new操作符执行

例子:

function User(name) {
    // this = {};(隐式创建)
    this.name = name;
    this.isAdmin = false;
    this.sayHi = function() {
        alert("Hello");
    };
    // return this;(隐式返回)
}
let Haze = new User("Haze");

// 结构等价于
let user = {
    name: "Haze",
    isAdmin: false,
    sayHi: function() {
        alert("hello");
    }
};

# 构造函数的 return 规则

一般来说都让它隐式返回this,如果显式return,则有以下规则:

  • 如果返回一个对象,则返回该对象,不返回this
  • 如果返回其他东西,则忽略,仍然返回this

# new 操作符

上面的例子中,隐式创建空对象与隐式返回this都是new的作用。

使用new操作符调用函数,会自动执行以下步骤:

  1. 创建一个全新的对象
  2. 这个对象会被执行[[Prototype]]链接,将原型实例化(不懂可先忽略该行)
  3. 生成的新对象会绑定到函数调用的this
  4. 如果函数没有返回对象类型,那么new表达式中的函数调用会自动返回这个新的对象

在函数内部,我们可以使用new.target属性来检查该函数被调用时是否使用了new操作符:

  • new,返回该函数(即 function Func { ... }
  • 不带 new,则返回 undefined

这样做使得构造函数的调用变得安全,不会被误调用为普通函数。

# 其他

  • new.target的特性,我们还可以延伸出一个小 trick。

    当调用时没有使用new操作符,自动帮忙补上:

    function User(name) {
      if (!new.target) {          // 如果没有运行 new
        return new User(name);    // 自动补上 new
      }
      this.name = name;
    }
    
    let john = User("John");
    let john = new User("John");  // 相同
    

    但该方法非常不推荐使用,因为不利于阅读。

  • 对于以下对象,我们可以使用其他方法代替使用 new 创建对象:

    符号 替代
    {} new Object()
    "" new String()
    0 new Number()
    false new Boolean()
    [] new Array()
    /()/ new RegExp()
    function (){} new Function()
  • JavaScript 为我们内置了许多常用的对象的构造函数,如日期Date、集合Set以及数组Array

  • 当构造函数没有参数时,语法上是支持省略括号的:

    let user = new User;  // 等价于 new User();
    

    但从代码风格角度上并不推荐这么做。


# 回调函数中的 this

对于一个对象而言,我们很可能经常需要使用回调函数去改变自身的某个属性。下面看一个例子:

function Num() {
    this.value = 42;
    this.delayOutput = function() {
        setTimeout(function () {      // 回调一个匿名函数
            console.log(this.value);  // 这里有没有错误?
        }, 1000);
    }
}

let num = new Num();

按照直观上的感觉,我们的setTimeout对一个匿名函数进行了1秒定时的回调,回调函数中会输出this所指向的也就是Num对象的value值,所以一秒之后控制台会输出42

但这是不对的。按照这么执行的结果,我们会得到一个undefined

num.delayOutput(); // undefined

为什么呢?这是因为,this在匿名函数/箭头函数中有不同的表现:

  • 在没有进行绑定的情况下,匿名函数 function() {...} 的命名空间处于全局对象上,所以匿名函数的this会等于undefined(严格模式下)或者window(非严格模式下)
  • 箭头函数 () => {} 自身没有this,在箭头函数内访问this会取得来自外部上下文中的this

从这一点出发,上面的例子中的回调函数中的this实际上指向的是window全局对象,因为window上没有value属性,所以输出undefined

所以在回调函数中需要使用this的情况下,请不要使用匿名函数。

TIP

有人可能要说,欸那我之前定义对象的方法也是用的function() {...},里面调用this看起来一切正常呀:

this.num = NaN;
// 一般对象
let obj = {
    num: 42,
    getNum: function () {
        return this.num
    }
}
console.log(obj.getNum()); // 42
// 构造函数创建对象
let Constructor = function() {
    this.num = 65535;
    this.getNum = function() {
        return this.num;
    }
}
console.log(this.num); // NaN
let a = new Constructor();
console.log(a.num); // 65535
console.log(a.getNum()); // 65535

实际上,这些都已经不是匿名函数了,他们绑定在了一个变量或者属性上,有了一个名字,这个时候this的值就被绑定在了这些对象上面。

在回调函数这个领域中,箭头函数相对而言就显得非常合适,因为它正好指向了箭头函数所处上下文的对象。

在上上一个的例子中,匿名函数的外部上下文就是Num中,实例化后就是num对象。所以上例将匿名函数改为箭头函数,即可正常运行:

function Num() {
    this.value = 42;
    this.delayOutput = function() {
        setTimeout(() => console.log(this.value), 1000);  // 箭头函数
    }
}

let num = new Num();
num.delayOutput();

// 一秒后:
// 42

不过,因为箭头函数没有自己的this的问题,我们并不适合使用箭头函数定义需要使用到this的对象方法(这里的对象是指不是使用new操作符创建的对象,而是使用字面量创建的对象),因为这个时候,this不会指向对象本身,而是指向调用者的“上一级”比如window

const num = {
    value: 42,
    output: () => console.log(this.value)
};

num.output();  // undefined
// output()中的this没有指向"."之前的num

如果是使用new操作符创建的对象,因为前面讲到的:new操作符会将新生成的对象绑定到函数的this上,所以在构造函数中使用箭头函数定义对象方法是没有问题的。

总结一下:

  • 匿名函数的thisundefined或者window
  • 箭头函数的this来自箭头函数的外部的上下文
  • 如果要使用this,请使用箭头函数

另附this的完整规则流程图,以便日后查阅:

this 规则.jpg

上次更新: 2020/3/26 22:51:19