对象属性分两种:数据属性和访问器属性数据属性:
[[Configurable]] 是否可删除/特性是否可修改[[Enumberable]] 是否可迭代[[Writable]] 值是否可修改[[Value]] 属性实际值,默认为 undeifined访问器属性:
[[Configurable]] 是否可删除/特性是否可修改[[Enumberable]] 是否可迭代[[Get]][[Set]]= 两张图,一个是工厂模式,在函数内部显示创建对象另一个是构造函数,通过 new 操作符来创建对象按照红宝书里的解释, new 操作符实现了下面步骤:(1) 在内存中创建一个新对象。(2) 这个新对象内部的 [[Prototype]] 特性被赋值为构造函数的 prototype 属性。(3) 构造函数内部的 this 被赋值为这个新对象(即this指向新对象)。(4) 执行构造函数内部的代码(给新对象添加属性)。(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。按照步骤实现,即:
let o = new Object() o.__proto__ = A.prototype // 3,4 步并做一步 A.call(o) a = o;2,3行可以看出原型 A.prototype 的构造函数是 A,再往上找是 Object的原型6,8行 原型 A.prototype 的原型链上的构造函数是 Object, 原型Object的原型链往上就是 null11,13行,再看实例和原型的关系 实例a的原型链指向 A的原型,实例a原型链上的构造函数指向 A通过这么一番梳理,我自己是理解了,关键是搞清楚实例,构造函数,原型对象分别是什么,这样才能梳理逻辑。
(1) 在对象实例中找一个属性,找不到则会去对象原型上去找(2) 实例和对象原型拥有同一个属性时,实例属性会覆盖对象原型,且实例属性的修改和原型属性无关;覆盖后,设置实例上的同名属性为null,也不会从对象原型去找,可以通过 delete 该实例属性以使属性访问与对象原型建立联系(3) 枚举顺序,大部分方法根据浏览器而定,但有些方法是固定的Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和Object.assign()
升序枚举数值键以插入顺序枚举字符串和符号键(4) 实例生成之后去向原型对象添加属性和方法,这些属性和方法在实例上也会体现。但是直接替换原型对象则不会反映到实例上(即使指定 constructor 为原构造函数也不行)。简单来说就是不要直接给原型对象赋值。(5) 原型对象的属性在实例间共享,如果属性值为引用类型(比如 Array ),在实例1中push一个值,在实例2中该属性值也会改变
这个真的是绕不过去的坎,每本书都有,还总记不住:
工厂模式:优点是接口封装,可以批量产生不同的对象缺点是总是要在函数内显示的创建对象且不知道创建的对象类型
function a(name){ let o = new Object() o.name = name return o } let b = a('bob') // {name: 'bob'} 构造函数缺点是内部定义的方法在实例化时总被定义一遍,实际上是同一个方法,若将方法定义到函数外会影响全局对象。
function A(name){ this.name = name; this.say(){ return 'hi' } } let a = new A('bob') let b = new A('join') a.say() // hi b.say() // hi 原型模式通过原型方法共享解决了2的缺点,但如果属性有引用类型,会修改原型
function A(){} A.prototype.name = '123' A.prototype.f = [1,2,3] let a = new A let b = new A a.f.push(4) b.f // [1,2,3,4] 继承原型链继承也有3中的缺点,而且b实例初始化时不能向A.prototype传参
function A(name){ this.name = name; } function B(){} B.prototype = new A let b = new B 经典继承/对象伪装解决了3中修改原型的问题
function A(name){ this.name = name; } function B(){ A.call(this, 'Bob') } B.prototype = new A let b = new B b.name // Bob 组合继承解决了3,4的问题, 所以它是目前使用最多的继承模式。缺点是每次实例化父原型会被调用两次
function A(name){ this.name = name; this.colors = [1,2,3] } function B(name){ A.call(this, name) } B.prototype = new A // 第一次 let a = new B let b = new B('Bob') // 第二次 a.colors.push(4) a.colors // [1,2,3,4] b.colors // [1,2,3] b.name // Bob 原型式继承存在3中引用类型共享的问题,适用场景是需要对象间共享信息且不需要单独创建构造函数的情况,跟 Object.create 只有一个参数时的行为一样。
function object(o){ function F(){} F.prototype = o; return new F } let o = {name: '11', friend:[1,2,3]} let a1 = object(o) a1.name = 'a1' a1.friend.push(4) let a2 = object(o) a2.name = 'a2' a2.friend.push(5) o.friend // [1,2,3,4,5] 寄生式继承适用场景是对象信息共享,类似工厂模式+原型式继承,给传来的对象属性增强,同样存在原型式继承的缺点,还有构造函数中的函数重用问题。
function a(o){ // 调用7中的方法 let clone = object(o) clone.say = function(){ console.log('hi') } } let a3 = a(o) a3.name = 'a3' a3.say() // hi 寄生式组合继承解决组合继承中每次实例化父类会被调用两次的问题,目前是引用类型继承最佳实践。
function compose(sub, sup){ let pro = object(sup) // 创建对象 pro.constructor = sub // 增强对象 sub.prototype = pro // 赋值对象 }第2行代码创建了父类的副本,然后3、4两行代码将子类的原型指向父类副本。最后再看看寄生式组合继承如何使用
//声明了构造函数 function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"]; } //为构造函数添加原型方法,以使实例能共享方法 SuperType.prototype.sayName = function() { console.log(this.name); }; // 组合继承,解决引用类型属性共享问题,且能传参 function SubType(name, age) { SuperType.call(this, name); this.age = age; } // 寄生式组合继承,解决了父类两次调用问题 compose(SubType, SuperType); SubType.prototype.sayAge = function() { console.log(this.age); }; let b1 = new SubType('Bob', 12) let b2 = new SubType('John', 13) b1.colors.push('pink') b1.colors // ["red", "blue", "green", "pink"] b2.colors // ["red", "blue", "green"](1) 类定义不能提升,函数声明可以(2) 类受块作用域限制,函数受函数作用域限制(3) 类表达式名称可选,当类名与变量名同时存在时,外部访问只能通过赋值的变量名访问。(4) 使用new操作符实例化类和实例化函数的步骤相同。当类中声明 constructor 函数时,实例化使用的是内部 constructor 函数(5) 不用new操作符调用类会报错(6) 类中构造函数返回值非this时,使用 instanceof 不会检测出实例与类有关联,因为对象的原型指针没有修改(7) 类是JS中的一等公民,可以被当作参数传递(8) 类的继承中,super方法得到的是父类实例,只能在constructor或静态方法中使用。在constructor中使用时,必须在this添加属性之前,否则获取不到 this 会抛出错误。
代码地址->利用Babel解析工具来看看类的本质,我们先创建个简单的类 A,拥有如下方法解析后的代码有些长,把它分成三块来分析:首先是一个工具方法· _createClass
'use strict'; //严格模式下操作 var _createClass = function () { // 这个方法等同于 ES6中的Object.defineProperties,给对象添加多个数据属性 function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } /** * @param Constructor 构造函数 * @param protoProps 原型属性,最终添加到 Constructor.prototype,即原型上 * @param staticProps 静态属性,最终添加到构造函数上,即 A.xx * return Constructor 返回增强后的构造函数 */ return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();看完了属性处理方法,再来看看什么情况下会抛错
// 接收参数为实例和构造函数, 判断实例的原型构造函数是否是Constructor // 上面类的行为第6点,类中构造函数返回值非this时,使用 instanceof 不会检测出实例与类有关联, // 这个时候就会抛错 function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }声明的工具函数都说完了,接下来到调用
var A = function () { function A(name) { // 检测原型 _classCallCheck(this, A); this.name_ = name; return this; } // 给类添加方法 // myName,生成器方法和访问属性被添加到原型对象上,即A.prototype, 方法可以被实例共享 // 最后一个 sayName静态方法,被添加到A上,不能被共享,只能通过A.sayName调用 _createClass(A, [{ key: 'myName', value: function myName() { console.log(this.name_[0].toUpperCase() + this.name_.slice(1)); } }, { key: 'NicknameIterator', value: /*#__PURE__*/regeneratorRuntime.mark(function NicknameIterator() { return regeneratorRuntime.wrap(function NicknameIterator$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return 'Jack'; case 2: _context.next = 4; return 'Jake'; case 4: _context.next = 6; return 'J-Dog'; case 6: case 'end': return _context.stop(); } } }, NicknameIterator, this); }) }, { key: 'n', get: function get() { return this.name_; }, set: function set(val) { this.name_ = val; } }], [{ key: 'sayName', value: function sayName() { console.log('my name is ' + this.name); } }]); return A; }();写一个继承类,同样查看其原型实现
class A{} class B extends A {}Babel转换后
var A = function A() { _classCallCheck(this, A); }; // B继承A var B = function (_A) { _inherits(B, _A); function B() { _classCallCheck(this, B); return _possibleConstructorReturn(this, (B.__proto__ || Object.getPrototypeOf(B)).apply(this, arguments)); } return B; }(A);重点放在 _inherits 和 _possibleConstructorReturn 方法做了什么,大概能猜出来
_inherits 将类B的__proto__指向类A,复制A类的方法到B,以使AB建立联系_possibleConstructorReturn返回继承到的类 function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } // 此例中call即B.__proto__,也就是A 存在且等式成立,返回 A return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } // 拷贝原型,并赋值给子类的原型 subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }数组flat 数组平铺from 类数组对象转为数组,空值占位:
let b=Array.from([,,,]) b // [undefined,undefined,undefined,undefined]of 将一组参数转为数组some 数组有一项为true则为trueevery 数组每项全true则返回trueincludes 数组包含某值fill() 批量复制方法copyWithin() 填充数组方法slice()创建一个包含原有数组中一个或多个元素的新数组, 两个参数为开始到结束,不包括结束。splice()在数组中间插入元素
删除, 第一个参数为起始位置,第二个参数为删除数量插入, 第二个参数设置为0,后面任意参数为插入元素值替换, 第二个参数非0DateDate.parse getTime() 字符串charAt() 方法返回给定索引位置的字符concat() ,将一个或多个字符串拼接成一个新字符串slice() 、 substr() 和 substring() 提取字符串
参数区别: substr 第二个参数是提取数量, slice 和 substring 两个参数都是开始位置和结束位置负数区别:substr 第二个参数为负数时转为0, substring 将负数参数转为0,slice 为长度+负数indexOf() 和 lastIndexOf() 。这两个方法从字符串中搜索传入的字符串,并返回位置(如果没找到,则返回-1)startsWith() 、 endsWith() 和 includes() 包含方法trim 去两边空格repeat 重复值 对象方法
Object.defineProperties 可以同时定义对象上的多个属性Object.getOwnPropertyDescriptor() 可以取得指定属性的属性描述符Object.getOwnPropertyDescriptors() ES2017新增 相当于对传入对象的每个属性调用Object.getOwnPropertyDescriptor(),返回结果Object.assign() 浅复制、对象合并, 原理是调用待拷贝对象的 get 方法获取到值,再调用结果对象的set方法赋值,最终将待拷贝对象的可枚举的自有属性拷贝下来。Object.is() 接收两个参数判断是否相等,类似于 ===,但对边界值的判断做了标准化处理,不会因浏览器产生差异 console.log(Object.is(true, 1)); // false console.log(Object.is({}, {})); // false console.log(Object.is("2", 2)); // false // 正确的0、-0、+0相等/不等判定 console.log(Object.is(+0, -0)); // false console.log(Object.is(+0, 0)); // true console.log(Object.is(-0, 0)); // false // 正确的NaN相等判定 console.log(Object.is(NaN, NaN)); // true能够控制暂停和执行并实现 iterator 接口的函数就是生成器,除箭头函数外,表现形式为函数名前加 * 。迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。可以被迭代的对象为可迭代对象,可迭代对象中都实现了 iterator 接口(可迭代协议),该接口满足两个特点:
实现了 next 方法,返回 iteratorResult 对象,包含 done 和 value 属性暴露 Symbol,iterator 属性作为默认迭代器代理的作用:可以拦截对目标对象的属性操作代理的使用: new Proxy(target, handler) , Proxy 是构造函数,接受两个参数, target 目标对象,和 handler 代理行为。代理的应用场景: 跟踪属性访问、隐藏属性、阻止修改或删除属性、函数参数验证、构造函数参数验证、数据绑定,以及可观察对象。代理的缺点:
目标对象方法中有this值时可能会得到预料外的结果部分内置类型的方法无法调用,如 Date const target = new Date(); const proxy = new Proxy(target, {}); console.log(proxy instanceof Date); // true proxy.getDate(); // TypeError: 'this' is not a Date object反射API与 Object 非常相似,主要是在代理的过程中使用,来实现对象的原始操作。
const target = { foo: 'bar' }; const handler = { get: Reflect.get }; const proxy = new Proxy(target, handler); console.log(proxy.foo); // bar console.log(target.foo); // bar