深入JavaScript(5)手写new的实现、类数组对象与arguments对象

76 阅读3分钟

1.手写模拟new

new 运算符允许开发人员创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

先看看new实现了哪些功能

function Person(name, age) {
    this.name = name
    this.age = age
    this.habit = 'myhabit'
}
Person.prototype.sayHi = () => {
    console.log('hi');
}

const person = new Person('myName', 18)
console.log(person.name); // myName
console.log(person.age); // 18
console.log(person.habit); // myhabit
person.sayHi() // hi

可以看到,实例person可以:

  • 访问到构造函数里的属性
  • 访问到构造函数prototype中属性

因为new是关键字,所以我们使用函数的形式来模拟。

1.1 第一版

1.我们可以看到new后的结果会返回一个新对象,所以我们要建立一个新对象,假设新对象叫obj,obj 会具有 Person 构造函数⾥的属性,我们可以使⽤ Person.apply(obj, Array.prototype.slice.call(arguments, 1)) 来给 obj 添加新的属性。

2.实例的原型(__ proto __)属性会指向构造函数的prototype。

基于上述写出第一版代码

function myNew(fn) {
    const obj = new Object()
    fn.apply(obj, Array.prototype.slice.call(arguments, 1))
    obj.__proto__ = fn.prototype
    return obj
}

function Person(name, age) {
    this.name = name
    this.age = age
    this.habit = 'myhabit'
}
Person.prototype.sayHi = () => {
    console.log('hi');
}

const person = myNew(Person, 'myName', 18)
console.log(person.name); // myName
console.log(person.age); // 18
console.log(person.habit); // myhabit
person.sayHi() // hi

1.2 第二版

可是假如构造函数有返回值的话

/*
* 返回值为对象时
*/
function Person(name, age) {
    this.name = name
    this.age = age
    this.habit = 'myhabit'
    return {
        age
    }
}
Person.prototype.sayHi = () => {
    console.log('hi');
}

const person = new Person('myName', 18)
console.log(person.name); // undefined
console.log(person.age); // 18
console.log(person.habit); // undefined
person.sayHi() // 报错 Uncaught TypeError: person.sayHi is not a function




/*
* 返回值为基本类型时
*/
function Person(name, age) {
    this.name = name
    this.age = age
    this.habit = 'myhabit'
    return '123'
}
Person.prototype.sayHi = () => {
    console.log('hi');
}

const person = new Person('myName', 18)
console.log(person.name); // myName
console.log(person.age); // 18
console.log(person.habit); // myhabit
person.sayHi() // hi

可见

  • 构造函数返回一个对象时,实例只能访问返回的对象中的属性
  • 构造函数返回不是一个对象时,本来该返回什么就返回什么

基于次写出最终版的代码

function myNew1(fn) {
    if (typeof fn !== "function") throw new TypeError(`${fn} is not a constructor`)
    const obj = new Object()
    obj.__proto__ = fn.prototype
    const result = fn.apply(obj, Array.prototype.slice.call(arguments, 1))
    return typeof result === 'object' ? result : obj
}

function Per(name, age) {
    this.str = 60;
    this.age = age
    return {
        name
    }
}
Per.prototype.sayHi = () => {
    console.log('hi');
}

const son = myNew1(Per, 11, 22)
console.log(son.name); // 11
console.log(son.age); // undefined
son.sayHi() // 报错 不是函数

2 类数组对象与arguments

2.1 类数组对象

所谓的类数组对象:

拥有⼀个 length 属性和若⼲索引属性的对象

const arr = ['name', 'age']
const arrLike = {
    0: 'name',
    1: 'age',
    length: 2
}

console.log(arr[0]); // name
console.log(arrLike[0]); // name

arr[0] = 'newName'
arrLike[0] = 'newName'

console.log(arr[0]); // newName
console.log(arrLike[0]); // newName

console.log(arr.length); // 2
console.log(arrLike.length); // 2

for (let index = 0; index < arr.length; index++) {
    console.log(arr[index]);    
}
// newName
// age
for (let index = 0; index < arrLike.length; index++) {
    console.log(arrLike[index]);    
}
// newName
// age

但是调用原生的数组方法就会报错

arrLike.push('newPush')

只能通过Function.call间接调用

const arrLike = {
    0: 'name',
    1: 'age',
    length: 2
}
const newArrLike = Array.prototype.join.call(arrLike, "&")
const sliceArrLike = Array.prototype.slice.call(arrLike, 1)
console.log(newArrLike); // name&age
console.log(sliceArrLike); // ['age']

类数组转数组

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)

2.2 Arguments对象

Arguments 对象只定义在函数体中,包括了函数的参数和其他属性。在函数体中,arguments 指代该函数的 Arguments 对象。

这个比较需要注意的点就是

  • Arguments 对象的 callee 属性,通过它可以调⽤函数⾃身。
  • 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 值不会共享