本文原载自 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
构造函数和工厂的区别:函数
this
return
使用构造函数建立对象将会有如下几个步骤:布局
[[Prototype]]
指针指向构造函数的 prototype
属性指向的对象;this
指向新建立的对象;return
非 null
的对象,那返回的就是这个对象,不然返回新建立的这个对象。没有 return
时,隐式返回新建立的对象,return null
会返回新建立的对象;缺点:每次实例化一个新对象,都会在内部建立一个 sayName
对应的匿名函数,而这个函数对全部实例来说是没有必要每次都建立的,他们只须要指向同一个函数便可。学习
因此上面的代码通过改造以后,变成下面这样:this
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
上述的作法虽然解决了重复建立匿名函数的问题,可是又引入了新的问题。spa
外面的 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
关于原型的工做原理,能够查看下面三篇文章,看完以后相信你对原型的认识比大多数人都要深入!
对象中属性的查找机制:
当从对象中访问一个属性的时候,JS 引擎将会按属性名进行查找。JS 引擎会先查找对象自身。若是找到了这个属性,就会中止查找并返回属性对应的值,若是在对象自身没有找到,则会经过原型链到原型对象中继续查找这个属性,若是找到了这个属性,就会中止查找并返回属性对应的值,不然会继续到上层原型链中查找,直到碰到 null
。
当一个属性添加到实例中时,这个属性会覆盖原型上的同名属性,这个覆盖指的是查找的时候不会到原型中查找同名属性。即便属性的值被赋值为 null
或 undefined
,它依然会阻止到原型链上访问。因此若是想要访问,就须要删除这个属性,使用 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
能够经过组合使用 hasOwnProperty
和 in
来实现判断一个属性是否存在于原型链上:
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()
是有肯定的枚举顺序的。
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
访问的时候也会打印三个字符串。
因为这个问题,原型模式并不会单独使用,咱们常常会结合构造函数和原型来建立对象。
咱们知道,使用构造函数或者原型建立对象都会存在问题,接下来咱们组合使用这二者来解决上面的问题。
为了解决上面的问题,咱们能够把全部对象相关的属性定义在构造函数内,把全部共享属性和方法定义在原型上。
// 把对象相关的属性定义在构造函数中 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
age
和 friends
属性,因此咱们使用 this
把这些属性定义在构造函数内。另外,因为 sayName
是定义在原型对象上的,因此这个函数会在全部实例间共享。
在上面的例子中,person1
对象更改 friends
属性时, person2
对象的 friends
属性没有更改。这是由于 person1
对象更改的是本身的 friends
属性,不会影响到 person2
内的。
往期精彩:
关注公众号能够看更多哦。
感谢阅读,欢迎关注个人公众号 云影 sky,带你解读前端技术,掌握最本质的技能。关注公众号能够拉你进讨论群,有任何问题都会回复。