Js 见怪不怪的那些基础知识

378 阅读20分钟

数组去重

原始方法--双重循环

在函数内新开辟一个空间来保存结果,遍历原数组并在新数组中查找是否存在原数组的值,如果存在则break出循环,若没有则将该值push到新数组中保存起来,并进行下一次的遍历。

var a = [1,2,2,4,4,5,5,5,9];
function unique(array) {
    var result = [];
    var arrayLen = array.length;
    for (var i = 0; i< arrayLen ; i++) {
        for (var j=0, resLen = result.length; j < resLen; j++) {
            if(array[i] === result[j]) break;
        }
        if (j === resLen) {
            result.push(array[i])
        }
    }
    return result;
}
console.log(unique(a))  //[1, 2, 4, 5, 9]

使用indexOf方法简化内层循环

indexOf方法详情

var a = [1,2,2,4,4,5,5,5,9];
function unique(array) {
    var result = [];
    var arrayLen = array.length;
    for (var i = 0; i< arrayLen ; i++) {
        var current = array[i];
        if(result.indexOf(current) === -1) {
            result.push(current)
        }
    }
    return result;
}
console.log(unique(a))  //[1, 2, 4, 5, 9]

利用对象的属性不能相同的特点进行去重

Array.prototype.filter方法详情

var a = [1,2,2,4,4,5,5,5,9];
function unique(array) {
    var obj = {};
    return array.filter(function(item, index, array){
        return obj.hasOwnProperty(item) ? false : (obj[item] = true)
    })
}

console.log(unique(a)); // [1, 2, 4, 5, 9]

ES6中的去重方法

  • ES6 提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。我们可以利用他的特性来完成数组去重
var a = [1,2,2,4,4,5,5,5,9];
function unique(array) {
   return Array.from(new Set(array));
}
console.log(unique(a)); // [1, 2, 4, 5, 9]

运用扩展运算符来简化

function unique(array) {
    return [...new Set(array)];
}

使用箭头函数

var unique = (array) => [...new Set(array)]
  • 利用Map数据结构进行数组去重,这个方法类似于利用对象特性的那个方法。
function unique (array) {
    const map = new Map()
    return array.filter((item) => !map.has(item) && map.set(item, 1))
}

类型判断

typeof一元运算符

typeof 1 // 'number'
typeof 'yff' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'

当我们检验基本类型的值时,除了null,typeof都能返回正确的结果,我们再来试试用typeof 检测引用类型的值。

function add(x,y) {
    return x+y;
}
typeof [];  // 'object'
typeof {};  // 'object'
typeof add; // 'function'

可以看出用typeof 运算符来检测引用数据类型是不合适的。

instanceof运算符

function add(x,y) {
    return x+y;
}
add instanceof Object   //true

instanceof的原理是根据原型链查找,它会在add函数的原型链式去查找是否有Object,如果有则返回true。

用instanceof进行基本类型的检测

Symbol.hasInstance用于判断某对象是否为某构造器的实例。

可以用其来重定义instanceof方法。将其行为重定义为用typeof进行基本类型检测,便可用instanceof进行基本类型的检测。

class PrimitiveString {
  static [Symbol.hasInstance](x) {
    return typeof x === 'string'
  }
}
console.log('yff' instanceof PrimitiveString)   //true

static定义了类的静态方法,使用instanceof时会默认调用。

手动实现instanceof

function myInstanceof(left, right) {
    //基本数据类型直接返回false
    if(typeof left !== 'object' || left === null) return false;
    //getProtypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
    let proto = Object.getPrototypeOf(left);
    while(true) {
        //查找到尽头,还没找到
        if(proto == null) return false;
        //找到相同的原型对象
        if(proto == right.prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
}

上述代码来着神三元的原生JS灵魂之问上

Object.prototype.toString

调用 Object.prototype.toString 会返回一个由 "[object " 和 class 和 "]" 组成的字符串,而 class 是要判断的对象的内部属性。所以class 值就是识别对象类型的关键!

下面罗列了用该方法可以检测到的数据类型

var number = 1;          // [object Number]
var string = 'yff';      // [object String]
var boolean = true;      // [object Boolean]
var undefine = undefined;     // [object Undefined]
var null = null;          // [object Null]
var obj = {a: 1}         // [object Object]
var array = [1, 2, 3];   // [object Array]
var date = new Date();   // [object Date]
var error = new Error(); // [object Error]
var reg = /a/g;          // [object RegExp]
var func = function a(){}; // [object Function]

function checkType() {
    for (var i = 0; i < arguments.length; i++) {
        console.log(Object.prototype.toString.call(arguments[i]))
    }
}

checkType(number, string, boolean, undefine, null, obj, array, date, error, reg, func)
console.log(Object.prototype.toString.call(Math)); // [object Math]
console.log(Object.prototype.toString.call(JSON)); // [object JSON]
function a() {
    console.log(Object.prototype.toString.call(arguments)); // [object Arguments]
}
a();

上述代码来自于冴羽大神的JavaScript专题之类型判断(上)

Array.isArray() 检测数组

console.log(Array.isArray(['1',2,3]))   //true

方法详情

深浅克隆

浅克隆

只能拷贝一层对象。如果有对象的嵌套,那么浅拷贝将无能为力。

手动实现

const shallowClone = (source) => {
    if (typeof source === 'object' && source !== null) {
        const target  = Array.isArray(source) ? [] : {}
        for (let prop in source) {
            if (source.hasOwnProperty(prop)) {
                target[prop] = source[prop]
            }
        }
        return target
    } else {
        return source
    }
}

Object.assign

Object.assign具体用法

let obj = { name: 'sy', age: 18 };
const obj2 = Object.assign({}, obj, {name: 'sss'});
console.log(obj2);//{ name: 'sss', age: 18 }

slice浅拷贝数组

slice具体用法

let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
console.log(newArr); //[1, 2, {val: 4}]
newArr[2].val = 1000;
console.log(arr);//[ 1, 2, { val: 1000 } ]

concat浅拷贝数组

concat具体用法

let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);//[ 1, 2, 3 ]

扩展运算符(...)

let obj = {name:"jack", age:18, hair:{color:'black'}};
let obj1 = {...obj};
obj1.hair.color='yellow';
console.log(obj); // {name:"jack", age:18, hair:{color:'yellow'}}

深克隆

JSON.parse(JSON.stringify());

存在的问题:

  1. 无法解决循环引用问题
const a = {val:2};
a.target = a;

如上代码,拷贝a会出现系统栈溢出,因为出现了无限递归的情况。

  1. 无法拷贝一写特殊的对象,诸如 RegExp, Date, Set, Map等。
  2. 无法拷贝函数(划重点)。

手写深度克隆(简易版)

const isObject = (source) => (typeof source === 'object' || typeof source === 'function') && source !== null;
// 用Map解决循环引用问题,记录下已经拷贝过的对象,如果说已经拷贝过,那直接返回它行了。
const deepClone = (source, map=new WeakMap()) => {
    if(map.get(source))
        return source;

    if (isObject(source)) {
        map.set(source, true)
        const target = Array.isArray(source) ? [] : {};
        // 解决无法对Symbol类型的拷贝 
        // 可以查找一个给定对象的符号属性时返回一个 ?symbol 类型的数组。注意,每个初始化的对象都是没有自己的 symbol 属性的,因此这个数组可能为空,除非你已经在对象上设置了 symbol 属性。
        let symKeys = Object.getOwnPropertySymbols(source); // 查找
        if (symKeys.length) { // 查找成功	
            symKeys.forEach(symKey => {
                if (isObject(source[symKey])) {
                    target[symKey] = deepClone(source[symKey], map); 
                } else {
                    target[symKey] = source[symKey];
                }    
            });
        }
        for (let prop in source) {
            if (source.hasOwnProperty(prop)) {
                target [prop] = deepClone(source[prop], map);
            }
        }
        return target;
    } else {
        return source
    }
}

参考文章:神三元的 原生JS灵魂之问, 请问你能接得住几个?(中)

数组扁平化

数组扁平化通俗的来说就是将多维数组变为一维数组

递归

var a = [1,[2,[3,4]]];
function flatten(array) {
    let result = [];
    let length = array.length
    for (let i=0;i<length;i++) {
        if(Array.isArray(array[i])){
            result = result.concat(flatten(array[i]))
        } else {
            result.push(array[i])
        }
    }
    return result;
}
console.log(flatten(a));

toString + split

思路就是将多维数组先转化为字符串,再用split将字符串转化为字符数组,再将数组中的元素转化为数字。

function flatten(array) {
    return array.toString().split(',').map(function(item){
        return +item
    })
}

reducer函数迭代器

function flatten(array) {
    return array.reduce((prev, cur) => {
        return prev.concat(Array.isArray(cur) ? flatten(cur) : cur);
    }, []);
}

ES6

  • 扩展运算符
function flatten(array) {
    while (array.some(item => Array.isArray(item))) {
        array = [].concat(...array);
    }
    return array;
}
  • flat方法
var a = [1,[2,[3,4]]];
array = a.flat(Infinity);

flat()方法详情

Js 参数传递

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

了解基本类型和引用类型特征

  • 基本类型:储存在栈内存中。值本身是不可改变的,比如对String操作,都只是将操作后的值重新赋值给一个新变量,原始值是不变的。
  • 引用类型:实际的值储存在堆内存中,栈内存储存了一个固定长度的地址,该地址指向储存在堆内存中的值。值本身是可变的,比如对Array操作,可以直接改变原数组的值。

值传递

借助下面的例子进行讲解:

let num = 5 
function add(num) {
    num += 10
}
add(num)
console.log(num)    //5

如果是按引用传递的,那么num的值应该是15。但是我们可以看到最终num是没有改变,所以得出基本类型的参数是按值传递的。大家可以将函数的传参当作函数内部的局部变量,当函数执行完成后,该变量会被自动销毁的。那再让我们来看下一个示例:

let person ={ name: "Jerry" }    
function setName(obj) {        
    obj.name = "Tom"    
}    
setName(person)    
console.log(person.name)    //Tom

这时候可能有些小伙伴们就迷惑了,最后的值不是已经改变了吗?其实我们同样将参数复制了一个副本到局部变量,只不过这个副本是指向堆内存的地址罢了。当在函数内部改变或是添加obj的name属性后,函数外部的person也有反映,是因为person指向的对象在堆内存中只有一个,而且是全局的。我们再来看一个例子,大家可能会更明白些:

let person ={ name: "Jerry" }    
function setName(obj) {        
    obj.name = "Tom"        
    obj = new Object()    // 注意这里        
    obj.name = "Spike"    
}    
setName(person)    
console.log(person.name)    //Tom

我们这是又在函数内部添加了两行代码,最终得到的值依然为Tom。可见,函数参数传递的并不是变量的引用,而是变量拷贝的副本,当变量是原始类型时,这个副本就是值本身,当变量是引用类型时,这个副本是指向堆内存的地址。

参考资料:

juejin.cn/post/684490…

JavaScript高级程序设计

七种模式创建对象

Object构造函数和对象字面量创建对象的缺点:

使用同一个接口创建很多对象,会产生大量的重复代码。

由于这个缺点所以出现了一下几种创建对象的模式

工厂模式

工厂模式代码:

function createPerson(name, age, job) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function() {
        alert(this.name);
    };
    return o;
}

工厂模式的不足

没有解决对象识别的问题(即怎样知道一个对象的类型)

构造函数模式

构造函数模式代码

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        alert(this.name);
    };
}

创建Person的新实例,需借用new操作符。

聊一聊new

new一个构造函数都发生了什么?

  1. 创建了一个新对象
  2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象

构造函数尽量不要返回值。因为返回原始值不会生效,返回对象会导致 new 操作符没有作用,看下面代码。

function Test(name) {
  this.name = name
  console.log(this) // Test { name: 'yq' }
  return { age: 26 }
}
const t = new Test('yq')
console.log(t) // { age: 26 }
console.log(t.name) // 'undefined'

当我们new一个新对象时,这个对象可以访问到构造函数原型上的属性,所以这个对象和原型对象连接的,下来手动实现一个new的过程

// 第一个参数为一个构造函数,剩余参数被构造函数所使用
function create(Con, ...args) {
  let obj = {}
  // 相当于obj.__proto__ = Con.prototype
  Object.setPrototypeOf(obj, Con.prototype)
  // 将 obj 绑定到构造函数上,并且传入剩余的参数
  let result = Con.apply(obj, args)
  // 判断构造函数返回值是否为对象,如果为对象就使用构造函数返回的值,否则使用 obj,这样就实现了忽略构造函数返回的原始值
  return result instanceof Object ? result : obj
}

apply()方法说明

测试代码:

function Test(name, age) {
  this.name = name
  this.age = age
}
Test.prototype.sayName = function () {
    console.log(this.name)
}
const a = create(Test, 'yck', 26)
console.log(a.name) // 'yck'
console.log(a.age) // 26
a.sayName() // 'yck'

构造函数模式的缺点:

每个方法都要在每个实例上重新创建一遍

function Person() {
    this.name = name;
    this.sayName = function() {
        alert(this.name);
    }
}
let person1 = new Person();
let person2 = new Person();

这里person1和person2实例对象里都有一个sayName()方法,这两个方法只是同名但是不等,且功能相同。这完全是没必要的。

alert(person1.sayName == person2.sayName) // false

原型模式

我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。这就解决了构造函数模式所创建对象的缺点了

原型模式代码

function Person() {}
Person.prototype.name = "yff";
Person.prototype.age = 19;
Person.prototype.sayName = function() {
    alert(this.name);
};
let person1 = new Person();
let person2 = new Person();
person1.sayName(); // "yff"
person1.sayName == person2.sayName //true

理解原型对象

无论什么时候当我们创建一个函数,该函数会创建一个prototype属性,该属性执行函数的原型对象。而这个原型对象会默认自动获得一个constructor属性,该属性指向prototype所在的函数。

Person.prototype.constructor == Person

当构造函数创建一个实例对象,该实例内部将包含一个[[Prototype]]指针,这个指针指向构造函数原型。我们没有标准方式访问这个指针,但是大多数浏览器的每个对象都支持一个__proto__属性,用这个属性便可访问构造函数的原型对象。

person1.__proto__ == Person.prototype

  • isPrototypeOf()方法:确定两个对象是否存在上述关系

alert(Person.prototype.isPrototypeOf(person1)) // true

  • Object.getPrototypeOf()方法:返回[[Prototype]]的值

alert(Object.getPrototypeOf(person1) == Person.prototype) // true

  • hasOwnProperty()方法: 检测一个属性是存在于实例中,还是存在于原型中,只有给定属性存在于对象的实例中时,才会返回true

alert(person1.hasOwnProperty("name")) // false

我们可以通过对象实例访问保存在原型中的值,但不能通过对象实例重写原型的值。如果我们在对象实例添加了一个与实例原型同名的属性,该属性便会屏蔽实例原型的那个属性,我们可以通过delete操作符来删除对象实例的属性,从而能够重新访问原型中的属性。

原型与in操作符

  1. 单独使用:in 操作符会通过对象能够访问给定的属性时返回true,无论该属性存在于实例中还是原型中。
  2. for in 循环:返回的是能够通过对象访问的,可枚举的(enumerated)属性,其中既包括存在于实例中的,也包括存在于原型中的属性。
  • Object.keys()方法:取得对象上所有可枚举的实例属性

  • Object.getOwnPropertyNames()方法: 取得所有实例属性,无论它是否可枚举。

原型对象的缺点

  1. 因为它省略了为构造函数传递初始化参数这一环节,所以所有实例在默认情况下都将取得相同的属性值。
  2. 原型中所有属性是被很多实例共享的,这种共享对函数非常合适。但对于包含引用类型值的属性来说,问题就比较突出
function Person() {}
Person.prototype = {
    constructor: Person,
    name: "yff",
    friends: ['lst','ghc']
};
var person1 = new Person()
var person2 = new Person()
person1.friends.push("xsh");
alert(person1.friends) // "lst, ghc, xsh"
alert(person2.friends) // "lst, ghc, xsh"
alert(person1.friends === person2.friends) // true

由于friends数组存在于Person.prototype而非person1中,所以刚刚对person对friends属性的修改也会通过person2.friends(与person1.friends指向同一个数组)属性所反映出来。

组合使用构造函数模式和原型模式

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。这样每个实例都会有一份实例属性的副本,但同时又共享着对方法的引用。且这种混合模式还支持向构造函数传递参数。

构造函数模式和原型模式的代码

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.friends = ["lst","ghc"];
}
Person.prototype = {
    constructor: Person,
    sayName: function() {
        alert(this.name)
    }
}

构造函数模式和原型模式的缺点

代码的封装性不好

动态原型模式

通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型

动态原型模式的代码

function Person(name, age) {
    this.name = name;
    this.age = age;
    if(typeof this.sayName != "function") {
        Person.prototype.sayName = function() {
            alert(this.name)
        };
    }
}

这里只有在sayName()方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需再做什么修改了。这里对原型的修改会立刻反映到实例上。对于采用这种模式创建的对象,还可以使用instanceof操作符确定它的类型。

使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。

寄生构造函树模式

寄生构造函树模式代码

function Person(name, age, job) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function() {
        alert(this.name);
    };
    return o;
}

可看出寄生构造函树模式除了使用new操作符并把使用包装函数叫做构造函数外,这个模式和工厂模式其实是一模一样的。

用法

这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组,由于不能直接修改Array构造函数,因此可使用此模式。

function SpecialArray() {
    var values = new Array();
    // 添加属性
    values.push.apply(values, arguments);
    // 添加方法
    values.toPipedString = function() {
        return this.join("|")
    }
    // 返回数组
    return values;
}
var colors = new SpecialArray("red", "blue", "yellow");
alert(colors.toPipedString()); //"red|blue|green"

寄生构造函树模式缺点

  1. 返回的对象与构造函数或者与构造函数的原型属性之间没有关系
  2. 不能依赖instanceof操作符来确定对象类型。

稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this对象,稳妥对象适合在安全的环境中(这些环境会禁用this和new),或者防止数据被其他应用程序改动时使用。

稳妥构造函数遵循寄生构造函数类似的模式,但有两点不同:

  1. 创建对象的实例方法不引用this
  2. 不使用new操作符调用构造函数

稳妥构造函数模式的代码

function Person(name, age) {
    var o = new Object();
    // 可以在这里定义私有变量和函数
    
    // 添加方法
    o.sayName = function() {
        alert(name)
    };
    return o;
}
let friend = Person("yff", 18);
friend.sayName(); // "yff"

变量frined保存了一个稳妥对象,而除了调用sayName()方法外,没有别的方式可以访问其数据成员。

参考文章: yck的 重学 JS 系列:聊聊 new 操作符

参考书籍: JavaScript高级程序设计

六种模式实现继承

ECMAScript只支持实现继承,而且实现继承主要依靠原型链来实现的。

原型链继承

基本思路

原型链继承的本质便是重写原型对象。将一个类型的实例作为另一个类型的原型,如此层层递进便构成了原型链。

代码展示

function SuperType() {
    this.property = true
}
SuperType.prototype.getSuperValue = function() {
    return this.property;
};
function SubType() {
    this.subproperty = false;
}
// 重写SubType类型原型对象,让SuperType类型的实例作为SubType类型的原型对象
SubType.prototype = new SuperType();
let o = new SubType();
alert(o.getSuperValue());    // true

确定原型和实例的关系

  • instanceof操作符:

如果SuperType存在于o的原型链上则返回true

alert(o instanceof SuperType); // true

使用instanceof操作符还可以确定变量类型

alert(o instanceof Object); //true

  • isPrototypeOf()方法 只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此该方法也会返回true alert(SuperType.prototype.isPrototypeOf(o)); //true

注意点

  • 给原型添加方法的代码一定要放在替换原型的语句之后
  • 通过原型链实现继承时,不能使用对象字面量创建原型方法

原型链继承的缺点

  • 当一个原型包含了引用类型的值,引用类型值的原型属性会被所有实例共享。
  • 在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

借用构造函数实现继承

基本思路

该技术的思想便是,在子类型构造函数的内部调用超类型构造函数。

代码展示

function SuperType(name) {
    this.colors = ["red", "blue", "green"];
}
function SubType() {
    // 改变了SuperType类型的this指向,指向了SubType中隐式创建的this对象
    SuperType.call(this, "yff");
    // 实例属性
    this.age = 29;
}
let o1 = new SubType();
o1.colors.push("black");
alert(o1.colors);   // "red, blue, green, black"
var o2 = new SubType();
alert(o2.colors);   // "red, blue, green"
alert(o1.name);  // "yff"

借用构造函数实现继承的缺点

  • 方法都在构造函数中定义,无法实现的函数复用。
  • 在超类型的原型中定义的方法,对于子类型是不可见的,结果所有类型都只能使用构造函数模式。

组合继承(伪经典继承)

基本思路

将原型链和借用构造函数的技术组合到一起,发挥二者之长的一种继承模式。 该技术的思路便是,使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样既通过在原型上定义方法实现函数复用,又能保证每个实例都有它自己的属性。

代码展示

function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
    alert(this.name);
};
function SubType(name, age) {
    // 继承属性
    SuperType.call(this, name);
    this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
// 让SubType.prototype.constructor不再指向SuperType,重新指回SubType
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
    alert(this.age)
}
let o1 = new SubType("yff", 18);
let o2 = new SubType("lst", 20);
o1.colors.push("black");
alert(o1.colors);   // "red,blue,green,black"
alert(o2.colors);   // "red,blue,green"
o1.sayAge();    // 18
o1.sayName();   // "yff"

组合继承的缺点

无论在什么情况下,都会调用两次超类型构造函数:

  1. 创建子类型原型的时候 SubType.prototype = new SuperType();
  2. 在子类型构造函数的内部 SuperType.call(this, name);

原型式继承

基本思路

这种方法并没有使用严格意义上的构造函数。该方法的思路便是,借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。

代码展示

function object(o) {
    function F() {};
    F.prototype = o;
    return new F();
}

本质上讲object()对传入其中的对象(o)执行了一次浅复制。object()将传入其中的对象(o)作为基础,在函数内部重新定义了一个构造函数,并将该构造函数的原型对象改为了(o),最后返回该构造函数的实例。

Object.create()方法

方法详情

ECMAScript5新增了Object.create()方法规范了原型式继承,该方法的参数:

  1. 一个用作新对象原型的对象
  2. 一个为新对象定义额外属性的对象(可选)

原型式继承缺点

当传入的原型对象包含了引用类型的值,引用类型值的原型属性会被所创建的实例共享。

寄生式继承

基本思路

寄生式继承是与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似详细说明,见我上篇博客。该方法的思路便是,创建一个仅用于封装继承过程的函数,该函数内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

代码展示

function createAnother(original) {
    // 该方法见原型式继承,创建一个新对象
    var clone = object(original);
    // 以某种方式增强这个对象
    clone.sayHi = function() {
        alert("hi");
    }
    // 返回这个对象
    return clone;
}

寄生式继承的缺点

使用寄生式继承为对象添加函数,会由于不能做到函数复用而降低效率,与构造函数模式类似。

寄生组合式继承

基本思路

由于组合继承的缺点(见上文),所以提出来寄生组合式继承来解决这个问题。 所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混合形式来继承方法。该继承方法的思路便是,不必为了指定子类型的原型而调用超类型,我们所需的仅仅是超类型原型的一个副本。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

代码展示

function inheritPrototype(subType, superType) {
    // 创建对象(object()方法见原型式)
    var prototype = object(super.prototype)
    // 增强对象, 为创建的副本添加constructor属性,从而弥补因重写原型而失去默认的constructor属性
    prototype.constructor = subType;
    // 指定对象, 将创建的对象(副本)赋值给子类型的原型
    subType.prototype = prototype;
}

参考书籍:JavaScript高级程序设计