阅读 68

InterviewMap —— Javascript (三)

一、深入理解 javascript 的 this 绑定

this问题的由来

var people = {
    Name: "海洋饼干",
    getName : function(){
        console.log(this.Name);
    }
};
var bar = people.getName;

bar();    // undefined
复制代码
var people = {
    Name: "海洋饼干",
    getName : function(){
        console.log(this.Name);
    }
};
var bar = people.getName;
var Name = "zjj";
bar(); // zjj
复制代码

上面两个问题,分别得到的记过是 undefinedzjj。是不是觉得很别扭,下面我们就去温习一下this的绑定规则吧。

1、this出现的意义

内存的数据结构

var obj = {foo: 5}
复制代码

原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo属性,实际上是以下面的形式保存的。

函数

由于函数是一个单独的值,所以可是在不同的环境中调用它。

var f = function () {};
var obj = { f: f };

// 单独执行,全局下
f()

// obj 环境下执行
obj.f()
复制代码

由于函数可以在不同的环境中执行,所以需要一种机制可以,在函数内部访问到当前的运行环境。所以this就出现了。

如下面的栗子。函数可以使用不同的运行环境,导致内部使用的x的值是变化的。

var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 单独执行
f() // 1

// obj 环境执行
obj.f() // 2
复制代码

所以这就是this存在的意义了。

2、this绑定规则

牢记准则: this指向什么,完全取决于什么地方以什么方式调用,而不是创建时。

4种绑定的规则如下:

(1)、默认绑定

function foo(){
    var a = 1 ;
    console.log(this.a);    // 10
    // window.a
}
var a = 10;
foo(); // 默认是window
复制代码

(2)、隐性绑定

function foo(){
    console.log(this.a);
}

var obj = {
    a : 10,
    foo : foo  // 必须有这条
}

foo();                // undefined

obj.foo();            // 10
复制代码

这里的obj.foo()就是隐性绑定了。如果是链性的关系,比如 xx.yy.obj.foo();, 上下文取函数的直接上级,即紧挨着的那个,或者说对象链的最后一个(也即是obj了)。

(3)、显性绑定

隐性质绑定的限制: 隐性绑定中有一个致命的限制,就是上下文必须包含我们的函数 ,例:var obj = { foo : foo },如果上下文不包含我们的函数用隐性绑定明显是要出错的。

显性绑定,js 给我们提供的函数 callapply,它们的作用都是改变函数的this指向,第一个参数都是 设置this对象。

使用call和apply

function foo(a,b){
    console.log(a+b);
}
foo.call(null,'海洋','饼干');        // 海洋饼干  这里this指向不重要就写null了
foo.apply(null, ['海洋','饼干'] );     // 海洋饼干
复制代码

使用bind

var obj = {
    a: 10
}

function foo () {
    console.log(this.a);
}

foo = foo.bind(obj);

foo();  // 10
复制代码

修改上面的那个栗子:

function f (){
    cosnole,log(this.a);
};

var obj = {
    a: 11
}

f.call(obj);

f(); // 11
复制代码

这样显然没有了上面所说的隐性的弊端了。

(4)、new 绑定

function foo(){
    this.a = 10;
    console.log(this);
}
foo();                    // window对象
console.log(window.a);    // 10   默认绑定

var obj = new foo();      // foo{ a : 10 }  创建的新对象的默认名为函数名
                          // 然后等价于 foo { a : 10 };  var obj = foo;
console.log(obj.a);       // 10    new绑定
复制代码

函数与new一块使用即构造函数,this指向新创建的对象

总结

  • 如果函数被new 修饰
this绑定的是新创建的对象,例:var bar = new foo(); 函数 foo 中的 this 就是一个叫foo的新创建的对象 , 然后将这个对象赋给bar , 这样的绑定方式叫 new绑定 .
复制代码
  • 如果函数是使用call,apply,bind来调用的
this绑定的是 call,apply,bind 的第一个参数.例: foo.call(obj); , foo 中的 this 就是 obj , 这样的绑定方式叫 显性绑定 .
复制代码
  • 如果函数是在某个 上下文对象 下被调用
this绑定的是那个上下文对象,例 : var obj = { foo : foo }; obj.foo(); foo 中的 this 就是 obj . 这样的绑定方式叫 隐性绑定 .
复制代码
  • 如果都不是,即使用默认绑定
   例:function foo(){...} foo() ,foo 中的 this 就是 window.(严格模式下默认绑定到undefined).
   这样的绑定方式叫 默认绑定 .
复制代码

3、箭头函数中的this

function a() {
    return () => {
        return () => {
        	console.log(this)
        }
    }
}
console.log(a()()())
复制代码

箭头函数其实是没有 this 的,这个函数中的 this 只取决于他外面的第一个不是箭头函数的函数的 this。在这个例子中,因为调用 a 符合前面代码中的第一个情况,所以 this 是 window。并且 this 一旦绑定了上下文,就不会被任何代码改变。

二、闭包

1、认识闭包

闭包的定义: 闭包是指那些可以访问自由变量的函数。

自由变量: 指可以在函数中访问,但并不是函数参数也不是局部变量的变量

闭包 = 函数 + 自由变量

var a = 1;

function foo() {
    console.log(a);
}

foo();
复制代码

上诉栗子也满足上面的要求,所以他也是一个闭包。可是你会发现,他怎么和我们平时所见到的不一样呢?

在《JavaScript权威指南》中就讲到:从技术的角度讲,所有的JavaScript函数都是闭包。

其实这是理论上的闭包,并不是实践中的闭包,我们平时遇见的都是实践中的闭包。

ECMAScript中,闭包指的是:

- 理论上: 所有的函数都是闭包。因为每一个函数创建的时候都会保存上层上下文中的变量对象保存起来。存放到作用域链中,即便是全局变量也是。

- 实践上: 只有满足下列条件的才是:
    即便创建他的执行上下文已经被销毁了,但是他仍然存在
    在代码中引用了自由变量
复制代码

我们先从栗子入手吧:

var scope = "global scope";
function checkscope(){
    var scope = "local scope"; // 内部变量
    
    function f(){
        // 引用自由变量 
        return scope;
    }
    // 返回出去,表示仍然存在
    return f;
}

var foo = checkscope();
foo();
复制代码

首先我们要分析一下这段代码中执行上下文栈和执行上下文的变化情况。

第一步 创建全局执行上下文并压入执行上下文栈

ECStack = [
    globalContext
];
复制代码

第二步 全局执行上下文对象

globalContext.VO = {
    scope: undefined,
    checkscope: function(){},
    foo: undefined
}
复制代码

第三步 全局执行,创建checkscope函数

globalContext.VO = {
    scope: global scope,
    checkscope: function(){},
    foo: undefined
}

checkscope.[[scope]] = {
    globalContext.VO
}
复制代码

第三步 创建函数上下文,并压入执行上下文栈

// 函数执行上下文创建好后,压入执行上下文栈中
ECStack = [
    checkscopeContext,
    globalContext
];
复制代码

第四步 函数并不会立马执行,首先是创建函数的scope

checkscopeContext.scope = checkscope.[[scope]]
复制代码

第五步 创建活动对象AO

checkscopeContext.AO = {
    arguments: {
        length: 0
    },
    scope: undefined
}
复制代码

第六步 将AO插入到作用域链中

checkscopeContext = {
    AO = {
        arguments: {
            length: 0
        },
        scope: undefined,
        f: function(){}
    },
    scope: [AO, checkscope.[[scope]]]
}
复制代码

第七步 开始执行checkscope函数

checkscopeContext = {
    AO = {
        arguments: {
            length: 0
        },
        scope: 'local scope',
        f: function(){}
    },
    scope: [AO, checkscope.[[scope]]]
}


复制代码

第八步 checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出

ECStack = [
    globalContext
];
复制代码

第九步 继续执行全局上下文的代码,开始创建和执行f函数,首先创建函数上下文,插入到执行上下文栈中。

f.[[scope]] = checkscopeContext.AO

ECStack = [
    fContext,
    globalContext
];


复制代码

第十步 开始执行f函数,但是并不是立马执行,首先创建AO对象和scope

fContext.AO = {
    arguments: {
        length: 0
    },
    scope: [AO, checkscopeContext.AO, globalContext.VO]
}
复制代码

第十步 开始真正执行f函数,返回一个值,执行完毕之后,将该函数弹出栈

ECStack = [
    globalContext
];
复制代码

了解到这个过程,我们应该思考一个问题,那就是:

当 f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?
复制代码

js是可以的,因为在f的scope中,Scope: [AO, checkscopeContext.AO, globalContext.VO],。维护了一个作用域链。虽然说checkscope函数已经被销毁了,但是由于f维护自己的作用域链,并且使用了上一层的自由变量,即使已经被销毁,但是checkscopeContext.AO对象却会驻留在内存中,所以自然我们的f就能访问上层变量了。

2、面试必刷题

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0](); // 3
data[1](); // 3
data[2](); // 3
复制代码

原因:

全局上下文VO对象为:

VO = {
    data: [...],
    i: 3
}
复制代码

当执行 data[0] 函数的时候,data[0] 函数的作用域链为:

data[0]Context = {
    Scope: [AO, globalContext.VO]
}
复制代码

此时访问i的话,访问的都是全局中的,因此都是输出3.

所以让我们改成闭包看:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
    return function() {
       console.log(i); 
    }
  })(i);
}

data[0](); // 0
data[1](); // 1
data[2](); // 2
复制代码

此时data[0]的作用域就发生了变化了:

data[0]Context = {
    Scope: [AO, 匿名函数Context.AO ,globalContext.VO]
}
复制代码

匿名函数中的AO为:

AO: {
    arguments: {
        0: 0,
        length: 1
    },
    i: 0
}
复制代码

data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数 Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值为3),所以打印的结果就是0。

3、如何创建闭包

创建闭包最常见方式,就是在一个函数内部创建另一个函数,并返回。

function foo (){
    var a = 10;
    
    function bar(){
        return a;
    }
    
    return bar;
}

var result = foo();
result(); // 10
复制代码

4、闭包的好处

1、 累加 【减少全局变量个数】

function add () {
  var a  = 0;
  return function () {
    a++;
console.log(a);
  }
}

var result = add();
result(); // 1
result(); // 2
复制代码

2、封装

var http = function () {
  // 局部变量
  var host = "http://..";

  // 局部变量
  var admin = function(){
    // 局部
    var url = host + "/login"
    return {
      login : function() {
        return 1;
      }
    }
  }();

  return {
    admin
  }
}();

http.admin.login();
复制代码

5、闭包的使用注意

1、 对捕获的变量只是个引用,不是复制

2、每调用一次父函数,就会产生一个新的闭包

function f() {
  var num = 1;
  return function () {
    num++;
    alert(num);
  }
}

var result1 = f();
result1(); // 2
result1(); // 3

var result2 = f();
result2(); // 2
result2(); // 3
复制代码

3、循环

<ul>
    <li id="1">1</li>
    <li id="2">2</li>
    <li id="3">3</li>
  </ul>
  <script>
    for(var i = 1 ; i <= 3; i++) {
      var el = document.getElementById(i);
      el.onclick = function() {
        alert(i);
      }
    }
  </script>

  // 结果是无论点击那个,都是弹出4
复制代码
// 解决办法
<ul>
    <li id="1">1</li>
    <li id="2">2</li>
    <li id="3">3</li>
  </ul>
  <script>
    for(var i = 1 ; i <= 3; i++) {
      var el = document.getElementById(i);
      el.onclick = (function(id) {
        return function () {
          alert(id);
        }
      })(i);;
    }
  </script>
复制代码

高效使用 JavaScript 闭包

三、js参数按值传递

在《JavaScript高级程序设计》第三版 4.1.3,讲到传递参数:

ECMAScript中所有函数的参数都是按值传递的。

按值传递: 把外面的值,复制一份传给函数作为参数使用。就和把值从一个变量复制到另一个变量一样。其实就是建立了一份备份。

js中有基本类型和引用类型。基本类型是存在与栈中的,引用类型是存在于堆中的,但是它的引用地址却是存在栈中的。

1、按值传递

var value = 1;
function foo(v) {
    v = 2;
    console.log(v); //2
}
foo(value);
console.log(value) // 1
复制代码

很好理解,当传递 value 到函数 foo 中,相当于拷贝了一份 value,假设拷贝的这份叫 _value,函数中修改的都是 _value 的值,而不会影响原来的 value 值。

参照上图来分析:

_value 是 value 的一个备份,或者说是副本,就相当于 _value = value 。此时 _value = 2; 改变了,但是 value 并没有变。

2、引用传递

var obj = {
    value: 1
};
function foo(o) {
    o.value = 2;
    console.log(o.value); //2
}
foo(obj);
console.log(obj.value) // 2
复制代码

所谓按引用传递,就是传递对象的引用,函数内部对参数的任何改变都会影响该对象的值,因为两者引用的是同一个对象。

针对官方的解释我们来分析:

ECMAScript中所有函数的参数都是按值传递的。

也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。基本类型的传递如同基本类型的复制一样,而引用类型值的传递,如同引用类型变量的复制一样。
复制代码
  • 总结

很简单,javascript函数参数都是按值传递(都是栈内数据的拷贝)。 基本类型传的是值本身(因为直接把值存在栈内),引用类型传的是对象在内存里面的地址 (因为复杂对象存在堆内,所以在栈里存对象所在的堆地址)。 这种引用类型的传递其实也是传递的值,就是堆地址的引用。

四、call和apply的模拟实现

1、call和apply的区别

callapply都是为了解决改变 this 的指向。作用都是相同的,只是传参的方式不同。

除了第一个参数外,call 可以接收一个参数列表,apply 只接受一个参数数组。

let a = {
    value: 1
}
function getValue(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value)
}

getValue.call(a, 'zjj', 12);
getValue.apply(a, ['zjj', 12]);
复制代码

2、模拟实现 call 和 apply

【1】模拟实现 call

第一步

var foo = {
    value: 1,
    bar: function() {
        console.log(this.value)
    }
};

foo.bar(); // 1
复制代码

如果我们设计成这样子,就可以实现了。但是我们为foo对象又添加了一个属性有点不好呢,没关系,我们可以delete嘛。

所以我们模拟的步骤可以分为:

将函数设为对象的属性
执行该函数
删除该函数
复制代码

测试案例:

var obj = {
    a: 1
}

function foo(){
    console.log(this.a);
}

foo.call_new(obj);
复制代码

实现:

// 第一版
Function.prototype.call_new =  function(context){
    // 首先要获取调用call的函数,用this可以获取
    context.fn = this;
    context.fn();
    delete context.fn;
}
复制代码

第二步

call 函数还能给定参数执行函数。举个例子:

var obj = {
    a: 1
}

function foo(name){
    console.log(name);  // zjj
    console.log(this.a) // 1
};

foo.call(obj, 'zjj');
复制代码
Function.prototype.call_new = function (context) {
      // 首先要获取调用call的函数,用this可以获取
      context.fn = this;
      var args = [];
      for (var i = 1; i < arguments.length; i++) {
        args.push('arguments[' + i + ']');
      }
      eval('context.fn(' + args + ')');
      delete context.fn;
    }

    var obj = {
      a: 1
    }

    function foo(name) {
      console.log(name);
      console.log(this.a);
    }

    foo.call_new(obj, 'zjj');
复制代码

第三步

还有小问题需要解决,this为null的时候,默认是window。而且函数可能会有返回值的。

Function.prototype.call_new = function (context) {
      var context = context || window;
      // 首先要获取调用call的函数,用this可以获取
      context.fn = this;
      var args = [];
      for (var i = 1; i < arguments.length; i++) {
        args.push('arguments[' + i + ']');
      }
      var result = eval('context.fn(' + args + ')');
      delete context.fn;
      return result;
    }

    var obj = {
      a: 1
    }

    function foo(name) {
      console.log(name);
      console.log(this.a);
      return {
        name: name,
        value: this.a
      }
    }

    console.log(foo.call_new(obj, 'zjj'));
复制代码
// ES6
Function.prototype.call_new = function (context) {
      var context = context || window;
      // 首先要获取调用call的函数,用this可以获取
      context.fn = this;
      var args = [];
      args = [...arguments].slice(1);
      var result = context.fn(...args);
      delete context.fn;
      return result;
    }
复制代码

【2】模拟实现 apply

Function.prototype.call_new = function (context, arr) {
      var context = Object(context) || window;
      // 首先要获取调用call的函数,用this可以获取
      context.fn = this;
      let result;
      if (!arr) {
        result = context.fn();
      } else {
        var args = [];
        for (var i = 0; i < arr.length; i++) {
          args.push('arr['+i+']') ;
        }
        // 这里会自动调用toString方法
        result = eval('context.fn(' + args + ')');
      }

      delete context.fn;
      return result;
    }

    var obj = {
      a: 1
    }

    function foo(name) {
      console.log(name);
      console.log(this.a);
      return {
        name: name,
        value: this.a
      }
    }

    console.log(foo.call_new(obj, ['zjj']));
复制代码
// 使用 ES6
Function.prototype.call_new = function (context, arr) {
      var context = Object(context) || window;
      // 首先要获取调用call的函数,用this可以获取
      context.fn = this;
      let result;
      if (!arr) {
        result = context.fn();
      } else {
        result = context.fn(...arr);
      }

      delete context.fn;
      return result;
    }
复制代码

【2】模拟实现 bind

第一步

Function.prototype.bind1 = function (context) {
      var self = this;
      return function () {
        return self.apply(context)
      }
    }

    var obj = {
      a: 1
    }

    function foo() {
      console.log(this.a);
    }
    var result = foo.bind(obj);
    result(); // 1
复制代码

第二步

处理参数的传递

Function.prototype.bind1 = function (context) {
        var self = this;
        var args = Array.prototype.slice.call(arguments, 1); // 得到参数
        return function () {
          var args1 = Array.prototype.slice.call(arguments);

          return self.apply(context, args.concat(args1));
        }
      }

      var obj = {
        a: 1
      }

      function foo(name, age) {
        console.log(name);
        console.log(age);
        console.log(this.a);
      }
      var result = foo.bind(obj, 'zjj');
      result(18);
复制代码

五、数组对象和arguments

1、认识类数组对象

var array = [1,2,3,4,5];

var arrayLike = {
    0: '1',
    1: 4,
    2: 'zjj',
    length: 3
}
复制代码

拥有一个 length 属性和若干索引属性的对象

那让我们从读写、获取长度、遍历三个方面看看这两个对象。

读写

  console.log(array[0]); // 1
  console.log(arrayLike[0]); // 1

  array[0] = "lll";
  arrayLike[0] = "mmm";

  console.log(array[0]); // lll
  console.log(arrayLike[0]); // mmm
复制代码

获取长度

  console.log(array.length); // 5
  console.log(arrayLike.length); // 3
复制代码

遍历:

  for (var i = 0, len = array.length; i < len; i++) {
    console.log(array[i]);
  }
  for (var i = 0, len = arrayLike.length; i < len; i++) {
    console.log(arrayLike[i]);
  }
复制代码

就上面的情况,两者是非常的像。

2、类数组对象调用数组的方法

无法直接调用,只能是Function.call间接调用。

var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }

Array.prototype.join.call(arrayLike, '&'); // name&age&sex

Array.prototype.slice.call(arrayLike, 0); // ["name", "age", "sex"] 
// slice可以做到类数组转数组

Array.prototype.map.call(arrayLike, function(item){
    return item.toUpperCase();
}); 
// ["NAME", "AGE", "SEX"]
复制代码

3、类数组转数组

var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }
// 1. slice
Array.prototype.slice.call(arrayLike); // ["name", "age", "sex"] 
// 2. splice
Array.prototype.splice.call(arrayLike, 0); // ["name", "age", "sex"] 
// 3. ES6 Array.from
Array.from(arrayLike); // ["name", "age", "sex"] 
// 4. apply
Array.prototype.concat.apply([], arrayLike)
复制代码

要说到类数组对象,Arguments 对象就是一个类数组对象。在客户端 JavaScript中,一些DOM方法(document.getElementsByTagName()等)也返回类数组对象。

4、arguments对象

arguments对象只存在函数体中,包括了参数和其他的属性。箭头函数并没有arguments对象。


function foo(name, age, sex) {
    console.log(arguments);
}

foo('name', 'age', 'sex')
复制代码

arguments 和对应参数的绑定

function foo(name, age, sex, hobbit) {

    console.log(name, arguments[0]); // name name

    // 改变形参
    name = 'new name';

    console.log(name, arguments[0]); // new name new name

    // 改变arguments
    arguments[1] = 'new age';

    console.log(age, arguments[1]); // new age new age

    // 测试未传入的是否会绑定
    console.log(sex); // undefined

    sex = 'new sex';

    console.log(sex, arguments[2]); // new sex undefined

    arguments[3] = 'new hobbit';

    console.log(hobbit, arguments[3]); // undefined new hobbit

}

foo('name', 'age')
复制代码

总之:传入的参数,实参和 arguments 的值会共享,当没有传入时,实参与 arguments 值不会共享

除此之外,以上是在非严格模式下,如果是在严格模式下,实参和 arguments 是不会共享的。

arguments传递参数给别的函数

// 使用 apply 将 foo 的参数传递给 bar
function foo() {
    bar.apply(this, arguments);
}
function bar(a, b, c) {
   console.log(a, b, c);
}

foo(1, 2, 3)
复制代码

强大的ES6

function func(...arguments) {
    console.log(arguments); // [1, 2, 3]
}

func(1, 2, 3);
复制代码

使用ES6的 ... 运算符,我们可以轻松转成数组。

累数组对象的应用

如果要总结这些场景的话,暂时能想到的包括:

参数不定长
函数柯里化
递归调用
函数重载
复制代码

六、创建对象的多种方式以及优缺点

创建对象的不同方式:

1、工厂模式

function createObj (name, age){
        var o = {};
        o.name = name;
        o.age = age;
        o.getName = function(){
          return this.name;
        }
        return 0;
      }

      createObj('zjj');
复制代码

缺点:对象无法识别,因为所有的实例都指向一个原型

2、构造函数模式

function Person(name) {
    this.name = name;
    this.getName = function () {
        console.log(this.name);
    };
}

var person1 = new Person('kevin');
复制代码

缺点:每次创建实例时,每个方法都要被创建一次

3、原型模式

function Person(name) {

}

Person.prototype.name = 'keivn';
Person.prototype.getName = function () {
    console.log(this.name);
};

var person1 = new Person();
复制代码

优点:解决了每个实例的创建都要重新创建一遍方法的缺点 缺点:但是1. 所有的属性和方法都共享 2. 不能初始化参数

4、上面原型模式的改进

function Person(name) {

}

Person.prototype = {
    constructor: Person,
    name: 'kevin',
    getName: function () {
        console.log(this.name);
    }
};

var person1 = new Person();
复制代码

优点: constructor有了 缺点: 属性都共享,不好初始化

5、组合模式【推荐使用】

function Person(name) {
    this.name = name;
}

Person.prototype = {
    constructor: Person,
    getName: function () {
        console.log(this.name);
    }
};

var person1 = new Person();
复制代码

6、 寄生构造函数模式【特殊使用】

function Person(name) {

    var o = new Object();
    o.name = name;
    o.getName = function () {
        console.log(this.name);
    };

    return o;

}

var person1 = new Person('kevin');
console.log(person1 instanceof Person) // false
console.log(person1 instanceof Object)  // true
复制代码

这样方法可以在特殊情况下使用。比如我们想创建一个具有额外方法的特殊数组,但是又不想直接修改Array构造函数,我们可以这样写:

function SpecialArray() {
    var values = new Array();

    for (var i = 0, len = arguments.length; i < len; i++) {
        values.push(arguments[i]);
    }

    values.toPipedString = function () {
        return this.join("|");
    };
    return values;
}

var colors = new SpecialArray('red', 'blue', 'green');
var colors2 = SpecialArray('red2', 'blue2', 'green2');


console.log(colors);
console.log(colors.toPipedString()); // red|blue|green

console.log(colors2);
console.log(colors2.toPipedString()); // red2|blue2|green2
复制代码

七、继承的方式

1、原型链继承

function Parent(name) {
    this.name = name || 'kiven';
}

Parent.prototype.getName = function () {
    return this.name;
}

function Child(){

}

// 这里是原型链继承的标志:
Child.prototype = new Parent('zzz');

var c = new Child();
console.log(c.getName());// zzz
复制代码

存在的问题:

1、 不能向父级传值。
2、 引用类型的实例被多个实例共享,如下:

function Parent () {
    this.names = ['kevin', 'daisy'];
}

function Child () {

}

Child.prototype = new Parent();

var child1 = new Child();

child1.names.push('yayu');

console.log(child1.names); // ["kevin", "daisy", "yayu"]

var child2 = new Child();

console.log(child2.names); // ["kevin", "daisy", "yayu"]
复制代码

直接将父类的实例new Parent()赋给了子类的原型,其实子类的实例child1,child2本身是一个完全空的对象,所有的属性和方法都得去原型链上去找,因而找到的属性方法都是同一个。

2、构造函数模式

function Person (name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.say = function(){
    console.log('hello, my name is ' + this.name);
};
function Man(name, age) {
    Person.apply(this, arguments);
}
//Man.prototype = new Person('pursue');
var man1 = new Man('joe');
var man2 = new Man('david');
console.log(man1.name === man2.name);//false
man1.say(); //say is not a function
复制代码

这里子类的在构造函数里利用了apply去调用父类的构造函数,从而达到继承父类属性的效果,比直接利用原型链要好的多,至少每个实例都有自己那一份资源,但是这种办法只能继承父类的实例属性,因而找不到say方法,为了继承父类所有的属性和方法,则就要修改原型链,从而引入了组合继承方式。

3、组合继承模式


function Parent(name, age) {
    this.name = name || 'kiven';
    this.age = age;
}

Parent.prototype.getName = function () {
    return this.name;
}

function Child(name){
    Parent.call(this, name);
    // 父类绑定 this 
}

// 子类原型链继承
Child.prototype = new Parent();
Child.prototype.constructor = Child;

var c = new Child('kkk');
c.age = 10;
console.log(c.name);
console.log(c.age);
var c1 = new Child('llll');
console.log(c1.name);
console.log(c1.age);


// 调用父类的方法
console.log(c1.getName());
复制代码

组合模式是 JavaScript 中最常用的继承模式。

function Person (name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.say = function(){
    console.log('hello, my name is ' + this.name);
};
function Man(name, age) {
    // [1]
    Person.apply(this, arguments);
}
// [2]
Man.prototype = new Person();
var man1 = new Man('joe');
var man2 = new Man('david');
console.log(man1.name === man2.name);//false
console.log(man1.say === man2.say);//true
man1.say(); //hello, my name is joe

复制代码

这时候要小心没有覆盖掉的方法,因为他们是公有的。

3、寄生组合继承【最流行、最经典的继承方式】

function Person(name, age) {
        this.name = name;
        this.age = age;
      }
      Person.prototype.say = function() {
        console.log("hello, my name is " + this.name);
      };


      function Man(name, age) {
        // 属性继承 
        Person.apply(this, arguments);
      }

      Man.prototype = Object.create(Person.prototype);  //a. 
      Man.prototype.constructor = Man;                  //b. 有助于instanceof的判别


      var man1 = new Man("pursue");
      var man2 = new Man("joe");

      console.log(man1.say == man2.say); // true
      console.log(man1.name == man2.name); // false
复制代码

a对子类的原型对象和父类的原型对象做了很好的连接。并不像原来直接对父类原型的暴力继承,(如Man.prototype = new Person();),这样只是对属性进行了暴力的覆盖,还导致共享。

关注下面的标签,发现更多相似文章
评论