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(上)》第二部分 this和对象原型 #11

Open
qunzi0214 opened this issue Sep 29, 2020 · 0 comments
Labels
read book 读书笔记

Comments

@qunzi0214
Copy link
Owner

qunzi0214 commented Sep 29, 2020

关于this

定义

this 实际上是在函数被调用时动态发生的绑定,它指向什么完全取决于函数在哪里被调用

this指向优先级

  1. new 操作符(指向新创建对象)
  2. 显式绑定(call、apply、bind)
  3. 隐式绑定(foo.bar.methods() 指向bar)
  4. 默认绑定(指向window或undefined)

柯里化的问题

当显式绑定传入第一个参数为null或者undefined的时候,this会被绑定至全局

function foo() {
  console.log(this.a);
}

var a = 2;

foo.call(null); // 2

当使用硬绑定来实现柯里化的时候,被绑定函数内部如果使用this,会导致操作全局对象

function foo(a, b) {
  this.a = 1;
  console.log(a + b);
}

var bar = foo.bind(null, 2);

bar(3); //5

console.log(a); // 1

一个好的处理方式是:需要在显式绑定忽略this时,传入 DMZ

function foo(a, b) {
  this.a = 1;
  console.log(a + b);
}

var ø = Object.create(null); // 不会创建Object.prototype委托,比{}更空

var bar = foo.bind(ø, 2);

bar(3); //5

console.log(a); // ReferenceError

软绑定

  1. 避免函数引用时this会被绑定至全局或undefined
  2. 保留隐式和显式修改this的能力
if (!Function.prototype.softBind) {
  Function.prototype.softBind = function (obj) {
    var fn = this;
    var curried = [].slice.call(arguments, 1);
    var bound = function () {
      return fn.apply(
        (!this || this === (window || global)) ? obj : this,
        curried.concat.apply(curried, arguments)
      );
    };

    bound.prototype = Object.create(fn.prototype);

    return bound;
  }
}

function foo() {
  console.log(this.name);
}

var obj = { name: 'obj' },
    obj2 = { name: 'obj2' },
    obj3 = { name: 'obj3' };

var bar = foo.softBind(obj);

bar(); // obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // obj2
bar.call(obj3); // obj3
setTimeout(obj2.foo, 10); // obj

箭头函数

根据当前的词法作用域来决定this,继承外层函数调用的this指向
优先级高于new

function foo() {
  setTimeout(() => {
    console.log(this.a);
  }, 100);
}

foo.call({
  a: 1
}); // 1

对象

万物皆对象的说法是错误的

基本类型和对象子类型(或称内置对象,比如String)是不等同的,javascript会在操作基本类型的时候自动将之转化为内置对象

var str = 'abc';
var strObj = new String('abc');

console.log(str === strObj); // false
console.log(typeof str); // string
console.log(typeof strObj); // object
console.log(str instanceof String); // false
console.log(strObj instanceof String); // true

基本类型中:boolean、string、number字面量与对应的内置对象不等同,null和undefined没有构造形式,而Object、Array、Function和RegExp无论使用字面量还是构造形式来声明,他们都是对象。

对象内容的存储

对象内容并不是存储在对象内部,而是通过属性名(键)来指向内存某一位置的值

可计算属性名

最常用的场景是ES6的Symbol,因为Symbol不透明且无法预测

const key = Symbol('key');
const obj = {
  [key]: 1,
};

console.log(obj[key]);

Object.assign

Object.assign内部就是使用=操作符来赋值,以此实现浅拷贝。因此原对象属性的一些特性(writable等)不会被复制到目标对象

Object.defineProperty

var myObj = {};

Object.defineProperty(myObj, 'a', {
  value: 2,
  // 是否可写,如果改为false,修改值不生效,严格模式下报错 TypeError
  writable: true, 
  // 是否可配置,如果改为false,不可再通过Object.defineProperty来配置descriptor,也不能删除此属性。否则 TypeError
  configurable: true, 
  // 是否可枚举,如果改为false,不会出现在对象遍历中(如for...in)
  enumerable: true, 
});

如果目标对象引用了其他对象,这些属性特性的声明不会对子对象的内容产生作用

Object.preventExtensions()

var obj = {};

Object.preventExtensions(obj);

obj.a = 1; // 严格模式下报错
obj.a // undefined

Object.seal()

等于对现有对象调用Object.preventExtensions(),同时把所有现有属性设置为 configurable: false,即只能修改已有属性的值

Object.freeze()

等于对现有对象调用Object.seal(),同时把所有现有属性设置为 writable: false

getter 和 setter

当给一个属性定义getter,或setter或两者都有时,这个属性会被定义为”访问描述符“(区别于”数据描述符“),javascript会忽略它的value和writable特性,只关心set、get、configurable和enumerable(?实测会报错,不能同时设置get和value、writable)

var obj = {
  get a() {
    return 2;
  },
  _b_: 3,
};

Object.defineProperty(obj, 'b', {
  get() {
    return this._b_;
  },
  set(val) {
    this._b_ *= val;
  },
  enumerable: true,
});

console.log(obj.a); // 2
console.log(obj.b); // 3
obj.b = 3;
console.log(obj.b); // 9

in 和 Object.hasOwnProperty

in 操作符会检查属性名是否存在于对象或原型链中,而 Object.hasOwnProperty 只会检查属性是否存在对象中

propertyIsEnumerable

会检查属性名是否直接存在于对象上且可枚举

Object.keys

返回所有可枚举属性名的数组

Object.getOwnPropertyNames

返回一个数组,包含所有在对象上的属性,无论是否可枚举

iterator

var array = [1, 2, 3];
var it = array[Symbol.iterator]();

it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { value: undefined, done: true }

Symbol.iterator 会指向一个'返回迭代器对象'的函数

for...of

for...of 会找到对象的 iterator 来处理遍历(每次调用next方法)
数组天生自带 iterator ,所以可以被for...of遍历,但是普通对象不行,可以手动给想遍历的对象定义 iterator

var obj = {
  a: 1,
  b: 2,
  c: 3,
};

Object.defineProperty(obj, Symbol.iterator, {
  enumerable: false,
  writable: false,
  configurable: true,
  value: function () {
    var o = this;
    var index = 0;
    var keys = Object.keys(o);
    return {
      next: function () {
        return {
          value: o[keys[index++]],
          done: index > keys.length
        }
      }
    }
  }
});

for (var item of obj) {
  console.log(item); // 1, 2, 3
}

类和原型

类是一种设计模式

javascript的机制其实和类完全不同,虽然可以用一般意义上的类来理解(比如实例化,继承等),但是javascript所做的一切本质上只是在操作对象

属性设置和屏蔽

obj.foo = 'bar';
  1. 如果 obj 对象中包含名为 foo 的普通数据访问属性名,则这条赋值语句会修改已有属性值
  2. 如果 foo 存在原型链的上层,并且没有被标记为只读(writable: true),会在 obj 上新增一条屏蔽属性
  3. 如果 foo 存在原型链的上层,并且被设置为只读(writable: false),这条赋值语句会被忽略,严格模式下报错
  4. 如果 foo 存在原型链的上层,并且它是一个 setter ,则会调用这个 setter ,不会在 obj 上产生屏蔽属性,也不会重新定义 setter

如果希望在3、4也屏蔽上层foo,就不能使用=操作符来赋值,可以通过Object.defineProperty()来添加 foo 属性

javascript”实例化“本质

在面向类的语言中,类可以被复制多次(类似用模具做东西)。但是在javascript中,并没有类似的复制方式,不能创建一个类的多个实例,只是创建了多个对象。它们的 prototype 关联的是同一个对象(引用),在默认情况下并不会复制。因此,在javascript中,”实例化的本质“是 —— 让两个对象相关联

new 操作符实际上并没有直接创建关联,这个关联只是个副作用(?!)

prototype.constructor

鉴于上述javascript实例化的本质,因此不能信任这条属性,不应该把它理解为”由...构造“。

function Foo() {

}

Foo.prototype = {};

var obj = new Foo();

console.log(obj.constructor === Object); // true

寻找路径:
obj不存在 constructor -> Foo.prototype不存在 constructor -> Object.prototype的 constructor 指向 Object
修正这种行为,必须显式的定义Foo.prototype.constructor

关于继承

重新指定 prototype.constructor 时

Bar.prototype = Foo.prototype; 
// 直接引用而不是创建新对象,会导致修改父类原型
Bar.prototype = new Foo();
// 基本能满足要求,但是有一些副作用:丢弃原本原型,同时调用constructor,导致原型属性变成对象属性
Bar.prototype = Object.create(Foo.prototype); 
// 正确做法(非原型属性用 Foo.call(this) 来继承),唯一缺点是会丢弃原本的原型,需要重新指定constructor

// es6
Object.setPrototypeOf(Bar.prototype, Foo.prototype); // 不需要抛弃原本的原型对象

内省

a instanceof Foo // 在a的整条链中是否存在Foo.prototype,只能处理对象和函数的关系

Foo.prototype.isPrototypeOf(c); // 不用类的角度来解释,而是直接关注对象之间的关系
b.isPrototypeOf(c)

Object.getPrototypeOf(a); // 直接获取
function Foo() { }

function Bar() { }

function Baz() { }

Object.setPrototypeOf(Bar.prototype, Foo.prototype);
Object.setPrototypeOf(Baz.prototype, Bar.prototype);

const a = new Baz();

console.log(a instanceof Baz); // true
console.log(Baz.prototype.isPrototypeOf(a)); // true
console.log(Object.getPrototypeOf(a) === Baz.prototype); // true 
console.log(Object.getPrototypeOf(Object.getPrototypeOf(a)) === Bar.prototype); // true
console.log(a.__proto__.__proto__.__proto__ === Foo.prototype); // true es6之前非标准

__proto__实际上更像一个getter/setter,并不存在当前对象中,而是存在 Object.prototype 中。

// 模拟一个 .__proto__
Object.defineProperty(Object.prototype, '__proto__', {
  get: function () {
    return Object.getPrototypeOf(this);
  },
  set: function (o) {
    Object.setPrototypeOf(this, o);
    return o;
  }
});

委托

面向委托的设计模式

在javascript中,用委托代替继承来解释原型链更为恰当,因为本质上prototype的机制就是对象之间的关联关系

委托理论

var task = {
  setId: function (id) {
    this.id = id;
  },
  outputId: function () {
    console.log(this.id);
  }
};

var subTask = Object.create(task); // 让subTask委托task

console.log(Object.getPrototypeOf(subTask)); // task

subTask.prepareTask = function (id, label) {
  this.setId(id);
  this.label = label;
};

subTask.outputTask = function () {
  this.outputId();
  console.log(this.label);
};

subTask.prepareTask(1, 2);
subTask.outputTask(); // 1, 2

Object.create 会创建一个新对象,并把它的__proto__委托到我们指定的对象

原型风格和对象关联风格

原型风格

function Foo(who) {
  this.me = who;
}

Foo.prototype.identify = function () {
  return `i am ${this.me}`;
}

function Bar(who) {
  Foo.call(this, who);
}

Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.speak = function () {
  alert(`hello, ${this.identify()}`);
}

var b1 = new Bar('b1');
var b2 = new Bar('b2');

b1.speak(); // hello, i am b1
b2.speak(); // hello, i am b2

对象关联风格

const Foo = {
  init: function (who) {
    this.me = who;
  },
  identify: function () {
    return `i am ${this.me}`
  }
};

const Bar = Object.create(Foo);

Bar.speak = function () {
  alert(`hello, ${this.identify()}`);
}

var b1 = Object.create(Bar);
b1.init('b1');
var b2 = Object.create(Bar);
b2.init('b2');

b1.speak(); // hello, i am b1
b2.speak(); // hello, i am b2

关于对象关联风格的思考:乍一看 b1 的初始化变为了两个步骤,需要更多代码。但其实这也是一个优点,把构造和初始化分离,在某些情况下更灵活

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
read book 读书笔记
Projects
None yet
Development

No branches or pull requests

1 participant