Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript 中的原型和原型链 #72

Open
lxfriday opened this issue Dec 14, 2019 · 4 comments
Open

JavaScript 中的原型和原型链 #72

lxfriday opened this issue Dec 14, 2019 · 4 comments
Labels
javascript javascript

Comments

@lxfriday
Copy link
Owner

lxfriday commented Dec 14, 2019

形成文章

image

ref

看下面的问题

Object.toString === Function.toString
// true
Object.toString === Object.prototype.toString
// false
Object.prototype.toString === ({}).toString
// true
@lxfriday lxfriday added the javascript javascript label Dec 14, 2019
@lxfriday
Copy link
Owner Author

lxfriday commented Dec 19, 2019

前端面试必备 | 使用原型和构造函数创建对象(原型篇:上)

本文翻译自 https://medium.com/better-programming/prototypes-in-javascript-5bba2990e04b,作者 Rupesh Mishra,翻译时有删改,标题有改动。

在这篇文章中,我们将会讨论 JavaScript 中的原型是什么,以及在 JavaScript 中如何实现面向对象编程。

使用构造函数 constructor 创建对象的问题

看看下面的代码:

function Human(firstName, lastName) {
  this.firstName = firstName,
  this.lastName = lastName,
  this.fullName = function() {
    return this.firstName + " " + this.lastName;
  }
}
var person1 = new Human("Virat", "Kohli");
console.log(person1)

我们使用 Human 构造函数创建 person1person2 两个对象:

var person1 = new Human("Virat", "Kohli");
var person2 = new Human("Sachin", "Tendulkar");

当执行上面的代码之后,JavaScript 引擎将会创建两个该构造函数的拷贝,一个是 person1 另一个是 person2

构造函数创建对象

从图中可以看出,每个对象都拥有 firstName lastName fullName 这三个属性。每个使用构造函数创建的对象都会拥有自己的属性和方法,但是我们没有必要声明多个 fullName 函数,这会浪费内存。接下来,我们看看如何解决这个问题。

原型

在 JavaScript 中创建一个函数的时候,JavaScript 引擎会添加一个 prototype 属性给这个函数,这个 prototype 属性指向一个对象,这就是我们所说的原型对象。原型对象默认会有一个 constructor 属性,这个 constructor 指向的就是有 prototype 属性的函数。看下面的图示:

如上图,Human 构造函数有一个 prototype 属性指向原型对象。而原型对象有一个 constructor 属性又指回了这个构造函数。让我们看下面的代码:

function Human(firstName, lastName) {
  this.firstName = firstName,
  this.lastName = lastName,
  this.fullName = function() {
    return this.firstName + " " + this.lastName;
  }
}
var person1 = new Human("Virat", "Kohli");
console.log(person1)

输出的结果

通过下面的代码访问 Human 构造函数的原型属性:

console.log(Human.prototype)

从上面的图片可以看出,函数的原型属性是一个有两个属性的对象(原型对象),两个属性如下:

  1. constructor 属性,这个属性指向构造函数 Human
  2. __proto__ 属性,后面我们讨论继承的文章中将会讨论这个属性;

使用构造函数创建对象

当一个对象被创建的时候,JavaScript 引擎添加了一个 __proto__ 属性给新创建的对象。__proto__ 属性会指向构造函数的原型对象(构造函数的 prototype 属性指向的对象)。

如上图所示, 使用 Human 构造函数创建的 person1 对象有一个 __proto__ 属性,这个属性指向的就是构造函数的原型对象。

var person1 = new Human("Virat", "Kohli");

从上面图片可以看出,person1__proto__ 属性所指向的对象和 Human.prototype 属性所指向的对象相同。我们使用 === 检验一下:

// true
console.log(Human.prototype === person1.__proto__ )

上面全等符号得到的结果是 true,所以我们可以进一步确定新对象的 __proto__ 属性指向的对象就是构造函数的原型对象。

接下来我们是使用 Human 构造函数创建 person2 对象:

var person2 = new Human("Sachin", "Tendulkar");
console.log(person2);

上面图中可以看出, person2__proto__ 也是指向的 Human.prototype(构造函数的原型对象)。

所以有下面的结果

Human.prototype === person2.__proto__ //true
person1.__proto__ === person2.__proto__ //true

所以,person1person2__proto__ 属性都指向 Human 构造函数的原型对象。

构造函数的原型对象会在所有使用该构造函数创建的对象中共享。

原型对象

因为原型对象是对象,所以我们可以添加属性和方法给原型对象。因此允许使用构造函数创建的所有对象共享那些属性和方法。

可以使用点表示法或方括号表示法将新属性添加到构造函数的原型对象中,如下所示:

//Dot notation
Human.prototype.name = "Ashwin";
console.log(Human.prototype.name)//Output: Ashwin

//Square bracket notation
Human.prototype["age"] = 26;
console.log(Human.prototype["age"]); //Output: 26

console.log(Human.prototype);

nameage 属性已添加到 Human原型对象中。

例子

//Create an empty constructor function
function Person(){
}
//Add property name, age to the prototype property of the Person constructor function
Person.prototype.name = "Ashwin" ;
Person.prototype.age = 26;
Person.prototype.sayName = function(){
  console.log(this.name);
}

//Create an object using the Person constructor function
var person1 = new Person();

//Access the name property using the person object
console.log(person1.name)// Output" Ashwin

我们分析一下 console.log(person.name) 执行的时候发生了什么。让我们看看 person1 是否有 name 属性。

console.log(person1);

正如我们所见, person1 对象是空的,并且它只有一个 __proto__ 属性。那么 console.log(person.name) 的结果是 Ashwin 是如何发生的呢?

当我们尝试访问对象的属性时,JavaScript 引擎首先尝试在对象上查找属性,如果该属性存在于对象上,则它会输出其值。但是,如果该属性不存在于对象上,则它将尝试在 __proto__ 上找到该属性。如果找到了属性,则返回值,否则 JavaScript 引擎将尝试在对象的 __proto__ 上找到该属性。这条查找链一直持续到 __proto__ 属性为 null,在这种情况下,输出将为 undefined

因此,当获取 person1.name 的值时,JavaScript 引擎会检查 person1 对象上是否存在该属性。此时,name 属性不在 person1 对象上。所以 JavaScript 引擎会依据 __proto__ 找到上层对象,并且在该对象中查找 name 属性是否存在。此时,name 属性存在于 person1.__proto__ 中, 所以返回查找到的值 Ashwin

让我们使用 Person 构造函数创建一个对象 person2

var person2 = new Person();
//Access the name property using the person2 object
console.log(person2.name)// Output: Ashwin

现在我们在 person1 上定义一个 name 属性:

person1.name = "Anil"
console.log(person1.name)//Output: Anil
console.log(person2.name)//Output: Ashwin

这里 person1.name 输出 Anil。如前所述,JavaScript 引擎首先尝试在对象本身上查找属性。在这种情况下,person1 对象本身存在 name 属性,因此 JavaScript 引擎输出person1name 属性值 Anil

而对于 person2,对象上不存在 name 属性。因此,它输出 person2 原型对象上的 name 属性值 Ashwin

原型存在的问题

由于原型对象在使用构造函数创建的所有对象之间共享,因此其属性和方法也在所有对象之间共享。如果对对象 A 具有原始值的原型属性进行修改,则其他对象将不会受到影响,这将在其对象上创建一个属性,如下所示。

console.log(person1.name);//Output: Ashwin
console.log(person2.name);//Output: Ashwin

person1.name = "Ganguly"

console.log(perosn1.name);//Output: Ganguly
console.log(person2.name);//Output: Ashwin

上面代码前两行,获取 name 属性都是在原型对象上获取的,而 person1.name = "Ganguly" 则直接给 person1 对象添加了 name 属性,所以下面打印的时候,是直接从 person1 对象上获取到的 name 属性的值,而 person2 由于没有这个属性,所以依然会到原型对象上获取 name 属性。

让我们来看另外一个原型的例子,在这个例子中原型对象有一个引用类型的属性。更改这个引用数据将会出现问题。

//Create an empty constructor function
function Person(){
}
//Add property name, age to the prototype property of the Person constructor function
Person.prototype.name = "Ashwin" ;
Person.prototype.age = 26;

Person.prototype.friends = ['Jadeja', 'Vijay'],//Arrays are of reference type in JavaScript
Person.prototype.sayName = function(){
  console.log(this.name);
}

//Create objects using the Person constructor function
var person1= new Person();
var person2 = new Person();

//Add a new element to the friends array
person1.friends.push("Amit");

console.log(person1.friends);// Output: "Jadeja, Vijay, Amit"
console.log(person2.friends);// Output: "Jadeja, Vijay, Amit"

在上面的例子中, person1person2 都指向原型对象中 friends 属性对应的数组。 person1 在这个数组中添加了一个字符串。

由于 friends 数组是存在于 Person.prototype (原型对象)上的,所以它不属于 person1,因此 person1 更改 friends 数组时,更改的是原型对象的数组。所以 person2.friends 访问的同样也是这个被更改过的数组。

上面的操作如果是为了让所有的实例都共享这个被变更过的数组,那这样做是没问题的。但是明显,这里我们并不想要它这样。我们想要 person2 访问到的数组内的数据依然是最开始的那几个,并不想让数组被改变。

组合使用构造函数和原型

我们知道,使用构造函数或者原型创建对象都会存在问题,接下来我们组合使用这两者来解决上面的问题。

  1. 构造函数的问题:每个对象都会声明对应的函数,浪费内存;
  2. 原型的问题:更改引用类型的原型属性的值会影响到其他实例访问该属性;

为了解决上面的问题,我们可以用把所有对象相关的属性定义在构造函数内,把所有共享属性和方法定义在原型上

//Define the object specific properties inside the constructor
function Human(name, age){
  this.name = name,
  this.age = age,
  this.friends = ["Jadeja", "Vijay"]
}
//Define the shared properties and methods using the prototype
Human.prototype.sayName = function(){
  console.log(this.name);
}
//Create two objects using the Human constructor function
var person1 = new Human("Virat", 31);
var person2 = new Human("Sachin", 40);

//Lets check if person1 and person2 have points to the same instance of the sayName function
console.log(person1.sayName === person2.sayName) // true

//Let's modify friends property and check
person1.friends.push("Amit");

console.log(person1.friends)// Output: "Jadeja, Vijay, Amit"
console.log(person2.friends)//Output: "Jadeja, Vijay"

我们想要每个实例对象都拥有 name agefriends 属性,所以我们使用 this 把这些属性定义在构造函数内。另外,由于 sayName 是定义在原型对象上的,所以这个函数会在所有实例间共享。

在上面的例子中,person1 对象更改 friends 属性时, person2 对象的 friends 属性没有更改。这是因为 person1 对象更改的是自己的 friends 属性,不会影响到 person2 内的。

最后

往期精彩:

关注公众号可以看更多哦。

感谢阅读,欢迎关注我的公众号 云影 sky,带你解读前端技术,掌握最本质的技能。关注公众号可以拉你进讨论群,有任何问题都会回复。

公众号

交流群

@lxfriday
Copy link
Owner Author

前端面试必备 | 古怪的原型(鸡生蛋还是蛋生鸡)(原型篇:中)

本文翻译自 https://medium.com/free-code-camp/prototype-in-js-busted-5547ec68872,作者 Pranav Jindal
,翻译时有删改,标题有改动。

以下四行足以使大多数 JavaScript 开发人员感到困惑:

Object instanceof Function 
// true 
Object instanceof Object 
// true 
Function instanceof Object 
// true 
Function instanceof Function 
// true

JavaScript 中的原型是极其难以理解的概念之一,但是你不能逃避它。不管你怎么忽略,你终究会在开发过程中碰到原型难题。

所以,让我们直面它吧。

从基础开始,JavaScript 中包含以下数据类型:

  1. boolean
  2. number
  3. string
  4. undefined
  5. null
  6. symbol
  7. bigint(new)
  8. object

上面的数据类型中除了对象,其他的都是原始数据类型,他们存储对应类型的数据。

而对象 object 是引用类型,我们可以将其描述为键-值对的集合(事实上不仅如此)。

在 JavaScript 中,可以使用构造函数 (constructor) 或者对象字面量({})创建对象。

JavaScript 中的函数是可以 “调用” 的特殊对象。我们使用 Function 构造函数或者函数声明来创建函数。这些构造函数既是对象又是函数,这个问题始终让我困惑,就像鸡生蛋还是蛋生鸡一样困惑着每个人。

在开始了解原型之前,我想澄清一下 JavaScript 中有两个原型:

  1. prototype:这是一个特殊的对象,它是所有你创建的函数都会有的一个属性。更准确点讲,你创建的任何函数都已经存在该属性,但是这个属性对于 JavaScript 引擎自带的函数或者 bind 产生的新函数却是不一定会有的。这个 prototype 属性所指向的对象与你用该构造函数创建的对象的 [[Prototype]] 属性所指向的对象是同一个;
  2. [[Prototype]]:这是每个对象都有的隐藏属性,如果在对象上无法读取到某个属性,则 JavaScript 引擎会尝试从对象的 [[Prototype]] 属性指向的对象上继续查找。实例的 这个属性所指向的对象和构造函数的 prototype 属性指向的对象是同一个。[[Prototype]] 是给引擎内部使用的,在我们编写的 JS 脚本中可以使用 __proto__ 属性访问原型对象。还有其他访问此原型的新方法,但是为了简洁起见,我将用 __proto__ 代替 [[Prototype]] 来做讲解;
var obj = {} // 对象字面量
var obj1 = new Object() // 构造函数创建对象

上面两个语句对于创建一个新的对象来讲是一样的,事实上当我们执行上面任何一条语句的时候都发生了很多事情。

当我创建一个新对象的时,创建的是一个空对象。事实上,它并不是空的,因为它是对象构造函数 Object 的一个实例,因此它本身会有一个属性指向 Object.prototype ,而这个属性就是 __proto__

如果我们查看 Object 构造函数的 prototype 属性,你会发现它和 obj.__proto__ 一模一样。事实上他们是两个不同的指针指向了相同的对象。

obj.__proto__ === Object.prototype
// true

每个函数的 prototype 属性都会有一个 constructor 属性,这个属性都是指向的函数自己。对于 Object 函数,prototype 有一个 constructor 属性指回了 Object 函数。

Object.prototype.constructor === Object
//true

在上面的图片中,左边是 Object 构造函数展开后的。你可能会感到疑惑,里面怎么有这么多函数。函数其实也是对象,因此它也可以像对象一样拥有各种属性

如果你仔细看,你会发现 Object (左边的)有一个 __proto__ 属性,这意味着 Object 肯定也是由其他有 prototype 的构造函数创建的。由于 Object 是一个函数对象,所以它肯定是由 Function 构造函数创建的。

Object.__proto__ 看起来和 Function.prototype 一样。当我检查两者是否全等时,发现它们确实是指向的同一个对象。

Object.__proto__ === Function.prototype
//true

如果你仔细的看上面的图,你也会发现 Function 本身也有一个 __proto__ 属性,这意味着 Function 构造函数也一定由其他有 prototype 的构造函数创建而来。由于 Function 本身是一个函数,它肯定是通过 Function 构造函数创建而来,也就是说,它自己创建了自己。这看起来比较荒谬,但是当你检查的时候,它确实是自己创建了自己。

Function__proto__prototype 实际上指向了相同的对象,也就是 Function 的原型对象。

Function.prototype === Function.__proto__
// true

文章前面也说过,函数的 prototype.constructor 属性必然会指向这个函数。

Function.prototype.constructor === Function
// true

上面这张图非常有趣!!

我们再来捋一遍,Function.prototype 也有一个 __proto__ 属性。好吧,这也没什么让人惊讶的,毕竟 prototype 是一个对象,它肯定可以有一个这个属性。但是注意,这个属性也是指向 Object.prototype 的。

Function.prototype.__proto__ == Object.prototype
// true

所以有了下面这张图:

// instanceof 操作符
a instanceof b

instanceof 操作符会查找 a 的原型链上的任何 constructor 属性。只要找到了 b 就会返回 true,否则返回 false

现在我们回到文章最开始的四个 instanceof 语句。

Object instanceof Function
Object.__proto__.constructor === Function

Object instanceof Object
Object.__proto__.__proto__.constructor === Object

Function instanceof Function
Function.__proto__.constructor === Function

Function instanceof Object
Function.__proto__.__proto__.constructor === Object

上面的情况太让人纠结了,哈哈!!但是我希望能简单点理解。

这里我有一点没有提出来,那就是 Object.prototype 没有 __proto__ 属性。

事实上,它其实有一个 __proto__ 属性指向 null。原型链查找最终会在找到 null 之后停止查找。

Object.prototype.__proto__
// null

Object, Function, Object.prototypeFunction.prototype 也有一些函数属性。如 Object.assignObject.prototype.hasOwnPropertyFunction.prototype.call,这些都是引擎内部函数,他们没有 prototype 属性,它们是 Function 的实例,它们有指向 Function.prototype__proto__ 属性。

Object.create.__proto__ === Function.prototype
// true

你也可以探索其他的构造函数,如 ArrayDate,或者看看它们的实例的 prototype__proto__。我确定你可以发现这些功能内在的联系。

额外的问题:

这里有几个困扰我一段时间的问题:为什么 Object.prototype 是普通对象而 Function.prototype 是函数对象。

这里 https://stackoverflow.com/a/32929083/1934798 给出了解答。

另一个问题是:原始数据类型是如何调用对应的方法的,如 toString()substr()toFixed()?这里 https://javascript.info/native-prototypes#primitives 给出了解释。(译者注:也叫如何理解包装对象


我把上面两个问题贴到这里。

第一个: 为什么 Function.prototype 是一个函数对象而 Object.prototype 是一个普通对象?

在 ES6 中 Array.prototype Function.prototype 和其他的构造函数的 prototype 不一样:

  1. Function.prototype 是一个 JavaScript 引擎内置的函数对象;
  2. Array.prototype 是一个引擎内置的数组对象,并且内置有针对这种对象的一些方法。

函数原型对象是为了兼容 ES6 之前的版本的 JS,这也不会让 Function.prototype 成为一个特别的函数。只有构造函数才会有 prototype 属性。

能作为构造函数的函数必须有一个 prototype 属性。

下面有一些非构造函数的例子。

  1. Math 对象的方法
typeof Math.pow; // "function
'prototype' in Math.pow; // false
  1. 一些宿主对象(host objects)
typeof document.createElement('object'); // "function
'prototype' in document.createElement('object'); // false
  1. ES6 中的箭头函数(正是因为没有 prototype 属性,所以箭头函数不能作为构造函数使用
typeof (x => x * x); // "function
'prototype' in (x => x * x); // false

第二个:如何理解包装对象
也可以参考这篇文章 https://blog.csdn.net/lhjuejiang/article/details/79623505

在文章的最开始我们列出了 JS 中的数据类型,其中(这里不考虑 symbolbigintbooleannumberstringnullundefined 都是非引用类型,也就是说变量直接指向的是原始值。

我们平常也会看到下面的操作:

var str = 'hello'; //string 基本类型
var s2 = str.charAt(0);
alert(s2); // h

上面的 string 是一个基本类型,但是它却能召唤出一个 charAt() 的方法,这是什么原因呢?

主要是因为:字符串去调方法的时候,基本类型会找到对应的包装对象类型,然后包装对象把所有的属性和方法给了基本类型,然后包装类型消失。

其过程大概是下面这样:

var str = 'hello'; //string 基本类型
var s2 = str.charAt(0); //在执行到这一句的时候 后台会自动完成以下动作 :
 
 var str = new String('hello'); // 1 找到对应的包装对象类型,然后通过包装对象创建出一个和基本类型值相同的对象
 var s2 = str.chaAt(0); // 2 然后这个对象就可以调用包装对象下的方法,并且返回结给s2.
 str = null;  //    3 之后这个临时创建的对象就被销毁了, str =null; 
  
alert(s2);// h 
alert(str);// hello 注意这是一瞬间的动作 实际上我们没有改变字符串本身的值。

也就是说,当原始值需要用到包装对象的属性或者方法的时候,会构造一个临时的包装对象出来,使用了之后就销毁了。所以即使给这个原始值赋值,由于赋值之后对象会被销毁,之后从这个原始值上并不能获取到对应的属性。

看下面的面试题:把原始值当做一个对象用的时候,所使用的的方法会对隐式产生的包装对象起作用,但不对原始值起作用。

var str="hello";
str.number = 10; // 包装对象消失
alert(str.number); // undefined

最后

往期精彩:

关注公众号可以看更多哦。

感谢阅读,欢迎关注我的公众号 云影 sky,带你解读前端技术,掌握最本质的技能。关注公众号可以拉你进讨论群,有任何问题都会回复。

公众号

交流群

@lxfriday
Copy link
Owner Author

lxfriday commented Dec 23, 2019

前端面试必备 | 千万不能错过的原型操作方法及其模拟实现(原型篇:下)

这篇文章主要讲解原型的查找、变更、判断和删除,附带着对原型的作用方式做一下回顾。

instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

即通过下面的操作来判断:

object.__proto__ === Constructor.prototype ?

object.__proto__.__proto__ === Constructor.prototype ?

object.__proto__.__proto__....__proto__ === Constructor.prototype

当左边的值是 null 时,会停止查找,返回 false

实际是检测 Constructor.prototype 是否存在于参数 object 的原型链上。

用法:

object instanceof Constructor

看看下面的例子:

// 定义构造函数
function C(){} 
function D(){} 

var o = new C();

o instanceof C; // true,因为 Object.getPrototypeOf(o) === C.prototype

o instanceof D; // false,因为 D.prototype 不在 o 的原型链上

o instanceof Object; // true,因为 Object.prototype.isPrototypeOf(o) 返回 true
C.prototype instanceof Object // true,同上

C.prototype = {};
var o2 = new C();

o2 instanceof C; // true

o instanceof C; // false,C.prototype 指向了一个空对象,这个空对象不在 o 的原型链上.

D.prototype = new C(); // 继承
var o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true 因为 C.prototype 现在在 o3 的原型链上

需要注意的是 Constructor.prototype 可能会由于人为的改动,导致在改动之前实例化的对象在改动之后的判断返回 falseC.prototype = {}; 直接更改了构造函数的原型对象的指向,所以后面再次执行 o instanceof C; 会返回 false

再看看下面一组例子,演示 String Date 对象都属于 Object 类型。

var simpleStr = "This is a simple string"; 
var myString  = new String();
var newStr    = new String("String created with constructor");
var myDate    = new Date();
var myObj     = {};
var myNonObj  = Object.create(null);

simpleStr instanceof String; // 返回 false, 检查原型链会找到 undefined
myString  instanceof String; // 返回 true
newStr    instanceof String; // 返回 true
myString  instanceof Object; // 返回 true

myObj instanceof Object;    // 返回 true, 尽管原型没有定义
({})  instanceof Object;    // 返回 true, 同上
myNonObj instanceof Object; // 返回 false, 一种创建非 Object 实例的对象的方法

myString instanceof Date; //返回 false

myDate instanceof Date;     // 返回 true
myDate instanceof Object;   // 返回 true
myDate instanceof String;   // 返回 false

instanceof 模拟实现

function simulateInstanceOf(left, right) {
  if (right === null || right === undefined) {
    throw new TypeError(`Right-hand side of ' instanceof ' is not an object`)
  }
  const rightPrototype = right.prototype
  left = Object.getPrototypeOf(left)

  while (left !== null) {
    if (left === rightPrototype) return true
    left = Object.getPrototypeOf(left)
  }

  return false
}

Symbol.hasInstance

Symbol.hasInstance 用于判断某对象是否为某构造器的实例。因此你可以用它自定义 instanceof 操作符在某个类上的行为。

class MyArray {  
  static [Symbol.hasInstance](instance) {
    // instance 是左边的参数
    return Array.isArray(instance);
  }
}
console.log([] instanceof MyArray); // true

Object.prototype.isPrototypeOf()

prototypeObj.isPrototypeOf(object)

isPrototypeOf() 方法用于测试一个对象是否存在于另一个对象的原型链上。

function Foo() {}
function Bar() {}
function Baz() {}

Bar.prototype = Object.create(Foo.prototype);
Baz.prototype = Object.create(Bar.prototype);

var baz = new Baz();

console.log(Baz.prototype.isPrototypeOf(baz)); // true
console.log(Bar.prototype.isPrototypeOf(baz)); // true
console.log(Foo.prototype.isPrototypeOf(baz)); // true
console.log(Object.prototype.isPrototypeOf(baz)); // true

Object.getPrototypeOf

Object.getPrototypeOf(object)

Object.getPrototypeOf() 方法返回指定对象的原型(内部 [[Prototype]] 属性的值)。如果没有继承属性,则返回 null

var proto = {};
var obj = Object.create(proto);
Object.getPrototypeOf(obj) === proto; // true

var reg = /a/;
Object.getPrototypeOf(reg) === RegExp.prototype; // true

注意:Object.getPrototypeOf(Object) 不是 Object.prototype

ObjectFunction 都属于函数对象,所以它们都是 Function 构造函数的实例,也就是说,会有下面的结果,具体原因请看我的上一篇文章

Object instanceof Function
// true

Object.getPrototypeOf( Object ) 是把 Object 这一构造函数看作对象,返回的当然是函数对象的原型,也就是 Function.prototype

正确的方法是,Object.prototype 是构造出来的对象的原型。

var obj = new Object();
Object.prototype === Object.getPrototypeOf( obj );              // true
Object.prototype === Object.getPrototypeOf( {} );               // true

在 ES5 中,如果参数不是一个对象类型,将抛出一个 TypeError 异常。在 ES6 中,参数会被强制转换为一个 Object(使用包装对象来获取原型)。

Object.getPrototypeOf('foo');
// TypeError: "foo" is not an object (ES5)
Object.getPrototypeOf('foo');
// String.prototype                  (ES6)

该方法的模拟实现:

Object.getPrototypeOf = function(obj) {
  if (obj === null || obj === undefined) {
    throw new Error('Cannot convert undefined or null to object')
  }
  if (typeof obj === 'boolean' || typeof obj === 'number' || typeof obj === 'string') return Object(obj).__proto__
  return obj.__proto__
}

Object.setPrototypeOf

Object.setPrototypeOf(obj, prototype)

Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即, 内部 [[Prototype]] 属性)到另一个对象或 null

如果 prototype 参数不是一个对象或者 null (例如,数字,字符串,boolean,或者 undefined),则会报错。该方法将 obj[[Prototype]] 修改为新的值。

对于 Object.prototype.__proto__ ,它被认为是修改对象原型更合适的方法。

该方法的模拟实现:

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj; 
}

Object.create

Object.create(proto[, propertiesObject])

propertiesObject 对应 Object.defineProperties() 的第二个参数,表示给新创建的对象的属性设置描述符。

如果 propertiesObject 参数是 null 或非原始包装对象,则抛出一个 TypeError 异常。

Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__

看下面的例子:

const person = {
  isHuman: false,
  printIntroduction: function () {
    console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
  }
};

const me = Object.create(person);

me.name = "Matthew"; // "name" is a property set on "me", but not on "person"
me.isHuman = true; // inherited properties can be overwritten

me.printIntroduction();
// expected output: "My name is Matthew. Am I human? true"

上面的操作和我们实例化一个新对象很类似。

下面我们使用 Object.create() 实现继承,Object.create() 用来构建原型链,使用构造函数给实例附加自己的属性:

// Shape - 父类(superclass)
function Shape() {
  this.x = 0;
  this.y = 0;
}

// 父类添加原型方法
Shape.prototype.move = function(x, y) {
  this.x += x;
  this.y += y;
  console.info('Shape moved.');
};

// Rectangle - 子类(subclass)
function Rectangle() {
  // 让子类的实例也拥有父类的构造函数中的附加的属性
  Shape.call(this); // call super constructor.
}

// 子类继承父类
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

var rect = new Rectangle();

console.log('Is rect an instance of Rectangle?', rect instanceof Rectangle); // true
console.log('Is rect an instance of Shape?', rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'

关于 Object.createpropertyObject 参数

如果不指定对应的属性描述符,则默认都是 false。描述符有以下几个:

  1. enumerable 可枚举,默认 false
  2. configurable 可删除,默认 false
  3. writable 可赋值,默认 false
  4. value 属性的值

看下面的例子:

var 0;
o = Object.create(Object.prototype, {
  name: {
    value: 'lxfriday', // 其他属性描述符都是 false
  },
  age: {
    value: 100,
    enumerable: true, // 除了可枚举,其他描述符都是 false
  }
})

从上面的结果可以看出,描述符默认都是 false,不可枚举的属性也无法通过 ES6 的对象扩展进行浅复制。

Object.create 的模拟实现:

Object.create = function(proto, propertiesObject) {
  const res = {}
  // proto 只能为 null 或者 type 为 object 的数据类型
  if (!(proto === null || typeof proto === 'object')) {
    throw new TypeError('Object prototype may only be an Object or null')
  }
  Object.setPrototypeOf(res, proto)

  if (propertiesObject === null) {
    throw new TypeError('Cannot convert undefined or null to object')
  }
  if (propertiesObject) {
    Object.defineProperties(res, propertiesObject)
  }

  return res
}

Object.assign

Object.assign(target, ...sources)

方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。它属于浅拷贝,只会复制引用。

如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性。

Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。该方法使用源对象的 [[Get]] 和目标对象的 [[Set]],所以它会调用相关 gettersetter。如果合并源包含 getter,这可能使其不适合将新属性合并到原型中。

String 类型和 Symbol 类型的属性都会被拷贝。

当拷贝的中途出错时,已经拷贝的值无法 rollback,也就是说可能存在只拷贝部分值的情况。

Object.assign 不会在那些 source 对象值为 nullundefined 的时候抛出错误。

const o1 = { a: 1, b: 1, c: 1 };
const o2 = { b: 2, c: 2 };
const o3 = { c: 3 };

const obj = Object.assign({}, o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }

拷贝 symbol 类型的属性

const o1 = { a: 1 };
const o2 = { [Symbol('foo')]: 2 };

const obj = Object.assign({}, o1, o2);
console.log(obj); // { a : 1, [Symbol("foo")]: 2 }
Object.getOwnPropertySymbols(obj); // [Symbol(foo)]

继承属性和不可枚举属性是不能拷贝的

const obj = Object.create({foo: 1}, { // foo 是个继承属性。
  bar: {
    value: 2  // bar 是个不可枚举属性。
  },
  baz: {
    value: 3,
    enumerable: true  // baz 是个自身可枚举属性。
  }
});

const copy = Object.assign({}, obj);
console.log(copy); // { baz: 3 }

原始类型会被包装为对象

const v1 = "abc";
const v2 = true;
const v3 = 10;
const v4 = Symbol("foo")

const obj = Object.assign({}, v1, null, v2, undefined, v3, v4); 
// 原始类型会被包装,null 和 undefined 会被忽略。
// 注意,只有字符串的包装对象才可能有自身可枚举属性。
console.log(obj); // { "0": "a", "1": "b", "2": "c" }

异常会打断后续拷贝任务

const target = Object.defineProperty({}, "foo", {
  value: 1,
  writable: false
}); // target 的 foo 属性是个只读属性。

Object.assign(target, {bar: 2}, {foo2: 3, foo: 3, foo3: 3}, {baz: 4});
// TypeError: "foo" is read-only
// 注意这个异常是在拷贝第二个源对象的第二个属性时发生的。

console.log(target.bar);  // 2,说明第一个源对象拷贝成功了。
console.log(target.foo2); // 3,说明第二个源对象的第一个属性也拷贝成功了。
console.log(target.foo);  // 1,只读属性不能被覆盖,所以第二个源对象的第二个属性拷贝失败了。
console.log(target.foo3); // undefined,异常之后 assign 方法就退出了,第三个属性是不会被拷贝到的。
console.log(target.baz);  // undefined,第三个源对象更是不会被拷贝到的。

拷贝访问器

访问器是一个函数, Object.assign 拷贝的时候会直接调用 getter 函数。

const obj = {
  foo: 1,
  get bar() {
    return 2;
  }
};

let copy = Object.assign({}, obj); 
console.log(copy); // { foo: 1, bar: 2 } copy.bar的值来自obj.bar的getter函数的返回值

// 下面这个函数会拷贝所有自有属性的属性描述符
function completeAssign(target, ...sources) {
  sources.forEach(source => {
    let descriptors = Object.keys(source).reduce((descriptors, key) => {
      descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
      return descriptors;
    }, {});

    // Object.assign 默认也会拷贝可枚举的Symbols
    Object.getOwnPropertySymbols(source).forEach(sym => {
      let descriptor = Object.getOwnPropertyDescriptor(source, sym);
      if (descriptor.enumerable) {
        descriptors[sym] = descriptor;
      }
    });
    Object.defineProperties(target, descriptors);
  });
  return target;
}

copy = completeAssign({}, obj);
console.log(copy);
// { foo:1, get bar() { return 2 } }

Object.assign 的模拟实现:

function assign(target, sources) {
  if (target === null || target === undefined) {
    throw new TypeError('Cannot convert undefined or null to object')
  }

  const targetType = typeof target
  const to = targetType === 'object' ? target : Object(target)

  for (let i = 1; i < arguments.length; i++) {
    const source = arguments[i]
    const sourceType = typeof source
    if (sourceType === 'object' || sourceType === 'string') {
      for (const key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
          to[key] = source[key]
        }
      }
    }
  }
  return to
}

Object.defineProperty(Object, 'assign', {
  value: assign,
  writable: true,
  configurable: true,
  enumerable: false,
})

new Constructor()

new constructor[([arguments])]

我们使用 new 可以创造一个指向构造函数原型的对象,并且让该对象拥有构造函数中指定的属性。

new 操作符的行为有以下三点需要特别注意,当代码 new Foo(...) 执行时,会发生以下事情:

  1. 一个继承自 Foo.prototype 的新对象被创建;
  2. 使用指定的参数调用构造函数 Foo,并将 this 绑定到新创建的对象。new Foo 等同于 new Foo(),也就是没有指定参数列表,Foo 不带任何参数调用的情况。
  3. 由构造函数返回的对象就是 new 表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。(一般情况下,构造函数不返回值,但是用户可以选择主动返回对象,来覆盖正常的对象创建步骤)

上面的第三步,返回 null 时,虽然 typeofobject,但是仍然会返回步骤一中创建的对象。

new 的模拟实现:

function monitorNew(constructor, args) {
  // 提取构造函数和参数,arguments 被处理之后不包含构造函数
  const Constructor = Array.prototype.shift.call(arguments)
  // 创建新对象,并把新对象的原型指向 Constructor.prototype 
  const target = Object.create(Constructor.prototype)
  // 把新对象作为上下文,执行 Constructor
  const ret = Constructor.apply(target, arguments)
  // 构造函数返回 null,则返回创建的新对象
  if (ret === null) return target
  // 如果是对象则返回指定的对象,否则返回创建的对象
  return typeof ret === 'object' ? ret : target
}

参考

最后

往期精彩:

关注公众号可以看更多哦。

感谢阅读,欢迎关注我的公众号 云影 sky,带你解读前端技术,掌握最本质的技能。关注公众号可以拉你进讨论群,有任何问题都会回复。

公众号

交流群

@lxfriday lxfriday reopened this Jan 1, 2020
@lxfriday
Copy link
Owner Author

lxfriday commented Jan 1, 2020

JavaScript 中创建对象的那些事儿

本文原载自 http://js-professional.lxfriday.xyz/blog/2019/12/31/JavaScript%E4%B8%AD%E5%88%9B%E5%BB%BA%E5%AF%B9%E8%B1%A1%E7%9A%84%E9%82%A3%E4%BA%9B%E4%BA%8B%E5%84%BF,作为学习笔记总结呈现。

创建对象的几种基本方式

  • {} 对象字面量
  • Object() 或者 new Object()
  • new Constructor()
  • Object.create()
  • Object.assign()

关于 new Constructor() Object.create()Object.assign() 创建对象的过程和模拟实现可以参考这篇文章 前端面试必备 | 5000字长文解释千万不能错过的原型操作方法及其模拟实现(原型篇:下)

工厂模式

function createPerson(name, age, job) {
  const o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function() {
    console.log(this.name);
  };
  return o;
}
const person1 = createPerson("Nicholas", 29, "Software Engineer");
const person2 = createPerson("Greg", 27, "Doctor");

每一次调用上面的 createPerson 工厂函数都可以创建一个对象,这个对象有 name age job 三个属性和一个 sayName 方法,依据传入的参数的不同,返回对象的值也会不同。

缺点:没有解决这个对象是一个什么类型的对象(没有更精确的对象标识,即没有精确的构造函数)。

构造函数模式

将工厂改造成构造函数之后,如下

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function() {
    console.log(this.name);
  };
}
const person1 = new Person("Nicholas", 29, "Software Engineer");
const person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg

构造函数和工厂的区别:

  1. 没有显式创建对象;
  2. 直接把属性和方法赋值给 this
  3. 没有 return

使用构造函数创建对象将会有以下几个步骤:

  1. 在内存中创建一个新对象
  2. 新对象内部的 [[Prototype]] 指针指向构造函数的 prototype 属性指向的对象;
  3. 将构造函数的上下文 this 指向新创建的对象;
  4. 执行构造函数内部的代码(给新对象添加属性);
  5. 如果构造函数 returnnull 的对象,那返回的就是这个对象,否则返回新创建的这个对象。没有 return 时,隐式返回新创建的对象,return null 会返回新创建的对象;

缺点:每次实例化一个新对象,都会在内部创建一个 sayName 对应的匿名函数,而这个函数对所有实例来讲是没有必要每次都创建的,他们只需要指向同一个函数即可。

所以上面的代码经过改造之后,变成下面这样:

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = sayName;
}
function sayName() {
  console.log(this.name);
}
const person1 = new Person("Nicholas", 29, "Software Engineer");
const person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg

上述的做法虽然解决了重复创建匿名函数的问题,但是又引入了新的问题。

外面的 sayName 函数仅仅在构造函数中用到,如果对象需要很多个这样的函数,那么就需要在外部定义很多个这种函数,这无疑会导致代码很难组织。

原型模式

函数创建之后都会有一个 prototype 属性,每个使用该构造函数创建的对象都有一个 [[prototype]] 内部属性指向它。

使用原型的好处在于它所有的属性和方法会在实例间共享,并且这个共享的属性和方法是直接在原型上设置的。

function Person() {}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
  console.log(this.name);
};
const person1 = new Person();
person1.sayName(); // "Nicholas"
const person2 = new Person();
person2.sayName(); // "Nicholas"
console.log(person1.sayName == person2.sayName); // true

关于原型的工作原理,可以查看下面三篇文章,看完之后相信你对原型的认识比大多数人都要深刻!

  1. 前端面试必备 | 5000 字长文解释千万不能错过的原型操作方法及其模拟实现(原型篇:下)
  2. 前端面试必备 | 古怪的原型(鸡生蛋还是蛋生鸡)(原型篇:中)
  3. 前端面试必备 | 使用原型和构造函数创建对象(原型篇:上)

理解原型的层级

对象中属性的查找机制:

当从对象中访问一个属性的时候,JS 引擎将会按属性名进行查找。JS 引擎会先查找对象自身。如果找到了这个属性,就会停止查找并返回属性对应的值,如果在对象自身没有找到,则会通过原型链到原型对象中继续查找这个属性,如果找到了这个属性,就会停止查找并返回属性对应的值,否则会继续到上层原型链中查找,直到碰到 null

当一个属性添加到实例中时,这个属性会覆盖原型上的同名属性,这个覆盖指的是查找的时候不会到原型中查找同名属性。即使属性的值被赋值为 nullundefined,它依然会阻止到原型链上访问。所以如果想要访问,就需要删除这个属性,使用 delete obj.xx

可以使用 hasOwnProperty 判断实例是否拥有某个属性,返回 true 则表示实例本身拥有该属性,否则表示它没有这个属性。当一个属性存在于原型链上时,可以访问到这个属性,但是使用 hasOwnProperty 将返回 false

in 操作符

in 操作符用在两个地方,一个是用在 for ... in 循环中,另一个是单独使用。单独使用时,返回 true 表示属性可以在对象或者其原型链上找到。

function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
  console.log(this.name);
};
const person1 = new Person();
const person2 = new Person();
console.log(person1.hasOwnProperty("name")); // false
console.log("name" in person1); // true
person1.name = "Greg";
console.log(person1.name); // "Greg" - from instance
console.log(person1.hasOwnProperty("name")); // true
console.log("name" in person1); // true
console.log(person2.name); // "Nicholas" - from prototype
console.log(person2.hasOwnProperty("name")); // false
console.log("name" in person2); // true
delete person1.name;
console.log(person1.name); // "Nicholas" - from the prototype
console.log(person1.hasOwnProperty("name")); // false
console.log("name" in person1); // true

可以通过组合使用 hasOwnPropertyin 来实现判断一个属性是否存在于原型链上:

function hasPrototypeProperty(object, name) {
  return !object.hasOwnProperty(name) && name in object;
}
const obj = Object.create({ name: "lxfriday" });
console.log(obj);
console.log(hasPrototypeProperty(obj, "name"));

关于对象属性的枚举顺序

for ... in Object.keys() Object.getOwnPropertyNames/Symbols()Object.assign() 在处理属性枚举顺序的时候会有很大差别。

for ... in Object.keys() 没有确定的枚举顺序,它们的顺序取决于浏览器实现。

Object.getOwnPropertyNames() Object.getOwnPropertySymbols()Object.assign() 是有确定的枚举顺序的。

  1. 数字键会按照升序先枚举出来;
  2. 字符串和 symbol 键按照插入的顺序枚举出来;
  3. 对象字面量中定义的键会按照代码中的逗号分割顺序枚举出来;
const k2 = Symbol("k2");
const k1 = Symbol("k1");
const o = { 1: 1, [k2]: "sym2", second: "second", 0: 0, first: "first" };
o[k1] = "sym1";
o[3] = 3;
o.third = "third";
o[2] = 2;
// [ '0', '1', '2', '3', 'second', 'first', 'third' ]
console.log(Object.getOwnPropertyNames(o));
// [ Symbol(k2), Symbol(k1) ]
console.log(Object.getOwnPropertySymbols(o));

对象的迭代

ES 2017 引入了两个静态方法来将对象的内容转换为可迭代的格式。

Object.values() 返回对象值构成的数组; Object.entries() 返回一个二维数组,数组中的每个小数组由对象的属性和值构成,类似于 [[key, value], ...]

const o = { foo: "bar", baz: 1, qux: {} };
console.log(Object.values(o)); // ["bar", 1, {}]
console.log(Object.entries(o)); // [["foo", "bar"], ["baz", 1], ["qux", {}]]

在输出的数组中,非字符串的属性会转换成字符串,上述的两个方法对引用类型是采取的浅拷贝。

const o = { qux: {} };
console.log(Object.values(o)[0] === o.qux); // true
console.log(Object.entries(o)[0][1] === o.qux); // true

symbol 键名会被忽略掉。

const sym = Symbol();
const o = { [sym]: "foo" };
console.log(Object.values(o)); // []
console.log(Object.entries(o)); // []

原型的另一种写法

上面的例子中,给原型赋值都是一个个赋,比较繁琐,看看下面的赋值方式:

function Person() {}
Person.prototype = {
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  sayName() {
    console.log(this.name);
  }
};

上面的例子中,Person 的原型直接指向一个对象字面量,这种方式最终的结果和前面的单个赋值是一样的,除了原型的 constructor 属性,constructor 不再指向 Person 构造函数。默认情况下,当一个函数创建的时候,会创建一个 prototype 对象,并且这个对象上的 constructor 属性也会自动指向这个函数。所以这种做法覆盖了默认的 prototype 对象,意味着 constructor 属性指向新对象的对应属性。虽然 instanceof 操作符依然会正常工作,但是已经无法用 constructor 来判断实例的类型。看下面的例子:

const friend = new Person();
console.log(friend instanceof Object); // true
console.log(friend instanceof Person); // true
console.log(friend.constructor == Person); // false
console.log(friend.constructor == Object); // true

如果 constructor 属性很重要,那么你可以手动的给它修复这个问题:

function Person() {}
Person.prototype = {
  constructor: Person,
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  sayName() {
    console.log(this.name);
  }
};

不过上面的设置方法有一个问题,constructor 的属性描述如下

{
  value: [Function: Person],
  writable: true,
  enumerable: true,
  configurable: true
}

我们再看看 Object.prototype.constructor

{
  value: [Function: Object],
  writable: true,
  enumerable: false,
  configurable: true
}

我们自己赋值时枚举属性会被默认设置为 true,所以需要通过 Object.defineProperty 来设置不可枚举:

Object.defineProperty(Person.prototype, "constructor", {
  value: Person,
  enumerable: false,
  configurable: true,
  writable: true
});

原型存在的问题

我们知道原型属性对所有实例是共享的,当原型属性是原始值时没有问题,当原型属性是引用类型时将会出现问题。看看下面的例子:

function Person() {}
Person.prototype = {
  constructor: Person,
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  friends: ["Shelby", "Court"],
  sayName() {
    console.log(this.name);
  }
};
const person1 = new Person();
const person2 = new Person();
person1.friends.push("Van");
console.log(person1.friends); // "Shelby,Court,Van"
console.log(person2.friends); // "Shelby,Court,Van"
console.log(person1.friends === person2.friends); // true

上述例子中,原型属性 friends 原本是一个包含两个字符串的数组,但是由于 person1 修改了它的内容,导致了原型上的这个属性被更改了,所以 person2 访问的时候也会打印三个字符串。

由于这个问题,原型模式并不会单独使用,我们经常会结合构造函数和原型来创建对象。

总结

我们知道,使用构造函数或者原型创建对象都会存在问题,接下来我们组合使用这两者来解决上面的问题。

  1. 构造函数的问题:每个对象都会声明对应的函数,浪费内存;
  2. 原型的问题:更改引用类型的原型属性的值会影响到其他实例访问该属性;

为了解决上面的问题,我们可以把所有对象相关的属性定义在构造函数内,把所有共享属性和方法定义在原型上

// 把对象相关的属性定义在构造函数中
function Human(name, age){
  this.name = name,
  this.age = age,
  this.friends = ["Jadeja", "Vijay"]
}
// 把共享属性和方法定义在原型上
Human.prototype.sayName = function(){
  console.log(this.name);
}
// 使用 Human 构造函数创建两个对象
var person1 = new Human("Virat", 31);
var person2 = new Human("Sachin", 40);

// 检查 person1 和 person2 的 sayName 是否指向了相同的函数
console.log(person1.sayName === person2.sayName) // true

// 更改 person1 的 friends 属性
person1.friends.push("Amit");

// 输出: "Jadeja, Vijay, Amit"
console.log(person1.friends)
// 输出: "Jadeja, Vijay"
console.log(person2.friends)

我们想要每个实例对象都拥有 name agefriends 属性,所以我们使用 this 把这些属性定义在构造函数内。另外,由于 sayName 是定义在原型对象上的,所以这个函数会在所有实例间共享。

在上面的例子中,person1 对象更改 friends 属性时, person2 对象的 friends 属性没有更改。这是因为 person1 对象更改的是自己的 friends 属性,不会影响到 person2 内的。

最后

往期精彩:

关注公众号可以看更多哦。

感谢阅读,欢迎关注我的公众号 云影 sky,带你解读前端技术,掌握最本质的技能。关注公众号可以拉你进讨论群,有任何问题都会回复。

公众号

交流群

您的在看和分享将是我继续前进的动力~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
javascript javascript
Projects
None yet
Development

No branches or pull requests

1 participant