Skip to content

2 对象与函数

约 1145 个字 194 行代码 预计阅读时间 6 分钟

1 对象

  • JS 中的「变量」被保存在 Stack 中,基本数据类型的值直接在栈内存中进行存储
  • JS 中的「对象」被保存在 Heap 中,通过保存内存地址空间建立对象名与数据的联系

    可以通过设置 obj = null 断开对象名与数据的联系

    比较两个对象时,比较的是两个对象的「内存地址」(与数据是否一致无关)

  • 对象的分类

    1. 内建对象(Math, Function , Number, ...)

      由 ES 标准定义的对象,在任何 ES 实现中均可以使用

    2. 宿主对象(DOM, BOM

      由 JS 运行环境(主要是浏览器)提供的对象

    3. 自定义对象

      开发人员自行创建的对象

1.1 基本操作

属性名
  • 对象的属性名「不强制要求」遵守标识符命名规范

    比如属性 obj.var 就可以正常读写

  • 若命名特殊属性名(比如纯数字),就需要用以下方式:

    obj["keyName"] = keyValue;
    // 读取时同理
    console.log(obj["keyName"]);
    
    // 中括号也可以根据变量值获取对应属性
    var key = "hello";
    obj[key] = "你好";
    

  • 可以使用 in 运算符检验对象是否含有指定属性

    console.log("keyName" in obj); // 返回 Boolean
    

对象中的属性值可以是任意类型,包括 Object。

  • 创建对象

    // 1. 使用 new 调用构造函数
    var obj = new Object(); 
    // 2. 使用对象字面量创建(可以顺便初始化)
    var obj1 = {
        name: "Tom",
        age: 18,
        // 属性名加不加引号都行,比较诡异的时候一定要加
        "$%^&*(*": "nothing",
        // 也可以套娃
        children: {
            name: "hello",
            age: "world"
        }
    };
    

  • 为对象添加属性

    obj.keyName = keyValue;
    

  • 读取指定属性

    console.log(obj.keyName);
    // 读取未定义的属性将返回 undefined
    

  • 修改指定属性

    obj.keyName = newKeyValue;
    

  • 删除指定属性 js delete obj.keyName;

  • 枚举对象中的属性

    for (var k in obj) { // k 拿到的是属性名,按初始化书写顺序排列
        console.log("key = " + k);
        console.log("value = ", obj[k]) // 变量需要使用中括号访问
    }
    

1.2 方法

使用 Function 作为属性值

var obj = {
    sayName = function() {
        document.write("My name is")
    };
};
// 调用
obj.sayName();

1.3 使用「工厂方法」创建对象

返回的都是 Object(没有类型)

// 批量创建结构相似的 object
function createPerson(name, age) {
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    return obj;
}
var p1 = createPerson("Tom", 17);
var p2 = createPerson("Tim", 22);

1.4 构造函数

  • 首字母大写(不强制)
  • 只能通过 new 关键字调用(不加会被当成普通函数)

    => 立刻创建一个新的 Object,并设为函数的 this,最后返回

  • 使用同一个构造函数创建的对象被认为属于”同一类“ => 该类的实例

    可以使用 obj instanceof className 判断实例是否属于某一类(返回 Boolean)

    => obj instanceof Object === true(所有类都是 Object 的后代)

function Person(name, age) { // 可以认为定义了一个 class
    // this 是新建的对象
    this.name = name;
    this.age = age;
    // 每执行一次就会创建一个「新的·唯一的」sayName 方法 => 最好封装一下
    this.sayName = function() {
        console.log("Hello, I'm " + this.name);
    }
    // 改良后的表达 => 所有该类实例共用一个函数
    this.sayName = fun;
}
// 在全局作用域中定义方法 => 污染了全局作用域的命名空间
function fun() {
    console.log("Hello, I'm " + this.name);
}

var p = new Person("Tom", 19);

1.5 原型对象

同一类的实例均能访问原型对象,可以设置一些共有属性和方法

解析器会为每一个函数添加属性 prototype,保存原型对象地址:

  • 作为普通函数调用时,无意义
  • 作为「构造函数」调用时,所创建实例中均存在指向该原型对象的隐含属性 __proto__
// 向原型对象添加共有属性 a
MyClass.prototype.a = 1;

var mc = new MyClass(); // mc 自身没有 a 属性
console.log(mc.a);      // -> 去原型对象中找到了 a(mc有a时被实例属性覆盖)
console.log("a" in mc); // -> true 原型中有也会返回 true
console.log(ms.hasOwnProperty("a")); // -> false 对象自身不含 a 属性

// 向原型对象添加共有方法 sayHello
Myclass.prototypr.sayHello = function() {alert("hello")};
mc.sayHello(); // 调用原型中的 sayHello 方法(只有1个且不会污染全局作用域)

寻找属性/方法的顺序:

  1. 自身
  2. 原型对象
  3. 原型的原型对象
  4. 持续性套娃至 Object 的原型
  5. 返回 undefined

返回 null 是因为当前属性本身值为 null (不是不存在)

1.6 自定义 toString

当我们尝试打印对象时,实际上显示的是对象 toString 方法的返回结果

默认调用的是 Object 原型上的 toString ,但是很难看捏 => 自定义一下吧!

className.prototype.toString = function() {
    // 一定要 return 一个 String 类型的结果捏
    return "Person[name="+this.name+",age="+this.age+"]";
}
// 似乎 console.log 会打出 Object 结构
// 但 document.write 会显示 toString 结果

2 函数

函数也是一个对象

2.1 创建函数

  1. 调用构造函数

    var f = new Function();
    
    // 可以通过 String 形式初始化代码
    var f2 = new Function("console.log('Hello World!')");
    // => 封装的函数不会立即执行(需要手动调用)
    f2(); // 执行 f2 中封装的代码
    

  2. 使用函数声明

    // function 函数名([参数列表]) {函数体}
    function myFunc() {
        console.log("hello!");
    }
    // 手动调用一下:
    myFunc();
    

  3. 使用函数表达式(匿名函数)

    // var 函数名 = function([参数列表]) {}
    var func = function() {
        console.log("这是个匿名函数")
    }
    // 也调用一下:
    func();
    

立即执行函数

仅执行一次的匿名函数(小括号包裹,并在末尾加()

(function(){alert("only once")})();

// 也可以带参数
(function(a, b) {
    console.log(a+b);
})(1, 2);

2.2 参数与返回值

  • 形参
    • 多个形参之间使用 , 隔开
    • 形参将在调用时被赋值(传少了就是 undefined
    • 解析器「不会」检查实参类型与数量(多的被丢弃)
  • 实参
    • 可以是任意数据类型
      • 实参为 Object
        // 参数过多时通过 Object 传递参数
        var person = {
            name: "Tom",
            age: 18
        }
        function sayHello(p) {
            // 通过 p.keyName 获取对应属性值
            console.log("Hello, " + p.name);
        }
        sayHello(person);
        
      • 实参为 Function
        function sayHello(info) {
            console.log("You say: " + info);
        }
        function action(func) {
            func("hello");
        }
        // 开始套娃 - 其实传的是函数指针(?)
        action(sayHello);
        // 一个反面例子 - 加括号传的是返回值...
        action(sayHello());
        
        // 也可以传匿名函数
        action(function(){alert("你被骗哩!")});
        
  • 返回值
    • 需要手动 return 返回值(可以丢个表达式返回执行结果),缺省时返回 undefined
    • return 之后的语句会被跳过
    • 返回值可以为任意类型(可以是函数或对象捏)
      // 返回函数的抽象例子
      function f1() { console.log("执行 f1"); }
      function f2() { return f1; }
      
      var a = f2(); // 现在 a 拿到了 f1 的函数指针
      a(); // 调用一下,输出 “执行 f1”
      
      // 两部并一步就是: f2()();
      

2.3 this 指针

解析器在每次调用函数时都会传入隐含参数 this,指向函数执行的上下文对象

视「调用方式」不同,this 将会指向不同的对象:

  1. 直接通过函数名调用:返回 window
  2. 以「方法」形式调用:返回对应的 Object

    function f() { console.log(this); }
    f(); // 输出 window
    
    var obj = {
        func: f
    };
    obj.func(); // 输出 obj
    
    // 不太妙的例子
    var name = "global";
    function sayName() {
        console.log(name);      // 此处一直用全局的 name
        console.log(this.name); // 正常了捏
    }
    var obj = {
        sayName: sayName
    };
    sayName();     // => global
    obj.sayName(); // => global
    
  3. 通过「函数对象调用 call / reply」:返回指定对象

    • call(neoThisObj, param1, param2, ...) 可以在对象后依次传递实参
    • apply(neoThisObj, [param1, param2, ...]) 需要将实参放进数组统一传递
    function fun() {
        console.log(this);
    }
    // 不指定参数,this = window
    fun.call(), fun.apply();
    // 指定参数,this = 传入对象
    var obj = {};
    fun.call(obj), fun.apply(obj);
    
    // 一个 NTR 的例子
    var p1 = {
        name: 'p1',
        sayName: function(a, b) {
            console.log(this.name + a + b);
        }
    }
    var p2 = { name: 'p2' }
    p1.sayName("h1", "h2");            // => p1,h1,h2
    // call & apply 的不同实参传递方式
    p1.sayName.call(p2, "h1", "h2");   // => p2,h1,h2
    p1.sayName.apply(p2,["h1", "h2"]); // => p2,h1,h2
    
  4. 以「构造函数」形式调用:返回新建对象

  5. 以「事件响应函数」形式调用:返回被绑定对象(触发对象)
  6. 在「事件绑定」中
    • addEventListener 的回调:返回绑定对象
    • attachEvent 的回调:返回 window

2.4 实参 arguments

在调用函数时,浏览器会传入两个隐含参数:

  1. 上下文对象 this
  2. 实参封装的类数组对象 arguments

    • 可以通过下标操作数据,也可以获取长度(但不是数组实例)
    • 可以通过 callee 属性返回当前正在执行的函数对象
function fun(a,b,c) { // 三个「形参」
    console.log(arguments.length);
}
// arguments.length 和定义「形参」的数量无关
fun(1,2);       // => 输出 2
fun(1,2,3,4,5); // => 输出 5

// 通过下标获取实参 arguments[idx] -> 不定义形参也能用
function f() {
    console.log(arguments[0])
}
f(1); // 输出 1

// 检查 callee 是否为当前函数对象
function check() {
    console.log(arguments.callee == check);
}
check(); // => 输出 true