let a = '123'
let b = [1,2]
const c = {
a:'123', b:'456'}
function A(){
}
A.prototype.name = 'A'
const a1 = new A
// 基础类型判断
typeof a // string
a instanceof String // true
// 对象/数组类型判断
b instanceof Array // true
Array.isArray(b) // true
// NaN值判断
Number.isNaN(NaN) // true
Object.is(NaN, NaN) // true
// 属性可枚举
c.propertyIsEnumerable('a') // true
Object.propertyIsEnumerable.call(c, 'a') // true
// 是否是自有属性
c.hasOwnProperty('a') // true
Object.hasOwnProperty.call(c, 'a') // true
a1.hasOwnProperty('name') // false (name属性在原型对象而非实例上)
A.prototype.hasOwnProperty('name') // true
// 判断对象或对象的原型链上是否有该属性
'name' in a1 // true
'name' in A.prototype // true
// 对象原型上是否有该属性
hasPrototypeProperty(a1, 'name') // true
a1.name = 'cc'
hasPrototypeProperty(a1, 'name') // false (被覆盖)
// 实例与原型对象关系确定
a1.__proto__.constructor = A // true
A.prototype.isPrototypeOf(a1) // true
a1 instanceof A // true
// 获取实例的原型对象
a1.__proto__ == A.prototype
Object.getPrototypeOf(a1) == A.prototype // true
// 创建新对象并指定原型
let a2 = Object.create(A.prototype)
// 获取属性
for...in // 拿到实例和原型上的属性
Object.keys // 仅拿到实例上的属性(不包括不可枚举属性)
Object.getOwnPropertyNames // 仅拿到实例属性(包括不可枚举属性)
Object.getOwnPropertySymbols // 拿到实例属性中的符号属性(Symbol)
对象属性分两种:数据属性和访问器属性
数据属性:
[[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;
function A(){
}
let a = new A
let b = new A
// 下面等式结果均为 true
A.prototype.constructor === A
A.prototype.__proto__ === Object.prototype
// 正常的原型链都会终止于Object的原型对象
A.prototype.__proto__.constructor === Object
// Object原型的原型是null
A.prototype.__proto__.__proto__ === null
// 实例通过__proto__链接到原型对象
a.__proto__ === A.prototype
// 构造函数通过prototype属性链接到原型对象
a.__proto__.constructor === A
// 同一个构造函数创建的两个实例,共享同一个原型对象:
a.__proto__ === b.__proto__
2,3行可以看出原型 A.prototype
的构造函数是 A,再往上找是 Object的原型
6,8行 原型 A.prototype
的原型链上的构造函数是 Object, 原型Object的原型链往上就是 null
11,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()
在数组中间插入元素
DateDate.parse
getTime()
字符串charAt()
方法返回给定索引位置的字符concat()
,将一个或多个字符串拼接成一个新字符串slice()
、 substr()
和 substring()
提取字符串
substr
第二个参数是提取数量, slice
和 substring
两个参数都是开始位置和结束位置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
代理行为。
代理的应用场景: 跟踪属性访问、隐藏属性、阻止修改或删除属性、函数参数验证、构造函数参数验证、数据绑定,以及可观察对象。
代理的缺点:
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