В Javascript почти все операции, такие как чтение, присваивание, вызов методов и т. д., выполняются вокруг «объектов», и долгое время вопрос о том, как лучше понимать и контролировать эти операции, стал важным вопросом при разработке языка.
I. Контроль доступа к объектам JS
[1.1] Знакомые геттеры/сеттеры
Так называемый геттер/сеттер обычно определяется как:
- Метод получения не принимает аргументов и всегда возвращает значение.
- Сеттер всегда принимает параметр и не возвращает значение
Некоторый здравый смысл геттера/сеттера:
- Два наиболее часто используемых метода доступа, также известные как методы доступа.
- Используется для инкапсуляции закрытых методов-членов, чтобы изолировать прямой доступ к ним из внешнего мира.
- Вы также можете добавить другую логику в процесс доступа, чтобы обеспечить простоту внешних вызовов.
- Достигается гибкость во внутренней логике объекта или класса, сохраняя возможность изменения
- Может автоматически генерироваться во многих IDE
Сначала посмотрите на общую реализацию на других языках:
Один из них — традиционный явныйgetXXX()/setXXX(v)
вызов метода
//JAVA
public class People {
private Integer _age;
public Integer getAge() {
return this._age;
}
public void setAge(Integer age) {
this._age = age;
}
public static void main(String[] args) {
People p = new People();
p.setAge(18);
System.out.println(p.getAge().toString()); //18
}
}
Несомненно, явное именование вызовов произвольно и может быть реализовано на разных языках.
Другой - неявный геттер/сеттер
//AS2
class Login2 {
private var _username:String;
public function get userName():String {
return this._username;
}
public function set userName(value:String):Void {
this._username = value;
}
}
var lg = new Login2;
lg.userName = "tom";
trace(lg.userName); //"tom"
//C#
class People
{
private string _name;
public string name
{
get {
return _name;
}
set {
_name = value;
}
}
}
People p = new People();
p.name = "tom";
Console.WriteLine(p.name)
//PHP
class MyClass {
private $firstField;
private $secondField;
public function __get($property) {
if (property_exists($this, $property)) {
return $this->$property;
}
}
public function __set($property, $value) {
if (property_exists($this, $property)) {
$this->$property = $value." world";
}
}
}
$mc = new MyClass;
$mc->firstField = "hello";
echo $mc->firstField; //"hello world"
Неявные методы доступа требуют языковой поддержки и похожи на чтение свойств (var x = obj.x
) или присвоение значения свойству (obj.x = "foo"
)
[1.2] Геттеры и сеттеры в ES5
Начиная со спецификации ECMAScript 5.1 (ECMA-262) в 2011 году, JavaScript также начал поддерживать геттеры/сеттеры; по форме он, естественно, такой же, как AS2/AS3, который также реализован для ECMAScript.
Синтаксис геттера:
// prop 指的是要绑定到给定函数的属性名
{get prop() { ... } }
// 还可以使用一个计算属性名的 expression 绑定到给定的函数, 注意浏览器兼容性
{get [expression]() { ... } }
🌰 Пример:
var obj = {
log: ['example','test'],
get latest() {
if (this.log.length == 0) return undefined;
return this.log[this.log.length - 1];
}
}
console.log(obj.latest); // "test"
var expr = 'foo';
var obj2 = {
get [expr]() { return 'bar'; }
};
console.log(obj2.foo); // "bar"
При использовании синтаксиса get следует учитывать следующие проблемы:
- Вы можете использовать числовые или строковые идентификаторы
- не должен иметь параметров
- не может отображаться в литерале объекта с другим получением или записью данных с теми же свойствами
Удалите геттер с помощью оператора удаления:
delete obj.latest;
Ниже показано расширенное использование, то есть значение получается только при первом вызове (ленивый геттер), и геттер преобразуется в обычное свойство данных:
get notifier() {
delete this.notifier;
return this.notifier = document.getElementById('myId');
},
Синтаксис сеттера:
//prop 指的是要绑定到给定函数的属性名
//val 指的是分配给prop的值
{set prop(val) { . . . }}
// 还可以使用一个计算属性名的 expression 绑定到给定的函数, 注意浏览器兼容性
{set [expression](val) { . . . }}
При использовании синтаксиса set следует учитывать следующие проблемы:
- Идентификаторы могут быть числами или строками
- должен иметь явный параметр
- Вы не можете использовать набор для переменной, которая уже имеет реальное значение, и вы не можете установить несколько наборов для свойства в одном и том же объекте.
🌰 Пример:
var language = {
set current(name) {
this.log.push(name);
},
log: []
}
language.current = 'EN';
console.log(language.log); // ['EN']
language.current = 'FA';
console.log(language.log); // ['EN', 'FA']
var expr = "foo";
var obj = {
baz: "bar",
set [expr](v) { this.baz = v; }
};
console.log(obj.baz); // "bar"
obj.foo = "baz"; // run the setter
console.log(obj.baz); // "baz"
Сеттеры можно удалить с помощью операции удаления:
delete o.current;
[1.4] Точное определение членов объекта с помощью Object.defineProperty()
Вспоминая то, что упоминалось ранее, существуют две основные формы дескрипторов свойств, которые существуют в объектах: свойства данных и методы доступа. Дескрипторы должны быть в одной из двух форм, а не оба.
И вообще, свойства, добавляемые к объектам присваиванием, могут быть пройдены и перечислены методами for...in или Object.keys, а значения добавленных таким образом свойств могут быть изменены или удалены.
var obj = {
_c: 99,
get c() {
return this._c;
}
};
obj.a = 'foo';
obj.b = function() {
alert("hello world!");
};
console.log( Object.keys(obj) ); //["_c", "c", "a", "b"]
for (var k in obj) console.log(k); //"_c", "c", "a", "b"
delete obj.b;
delete obj.c;
console.log(obj.b, obj.c); //undefined, undefined
Для свойства данных или метода доступа, определенного таким образом, нет контроля над тем, можно ли его удалить, и нельзя ли ограничить его перечисление.
И использование Object.defineProperty() позволяет изменить эти настройки по умолчанию.
Также, начиная со спецификации ECMAScript 5.1, определен метод Object.defineProperty(). Используется для определения нового свойства непосредственно в объекте или изменения существующего свойства объекта и возврата объекта.
Его синтаксис:
//obj 需要被操作的目标对象
//prop 目标对象需要定义或修改的属性的名称
//descriptor 将被定义或修改的属性的描述符
Object.defineProperty(obj, prop, descriptor)
вdescriptor
Свойства, которые можно установить:
Атрибуты | описывать | применительно к |
---|---|---|
configurable | Можно изменить и удалить | Атрибуты данных, методы доступа |
enumerable | можно перечислить | Атрибуты данных, методы доступа |
value | значение атрибута | атрибут данных |
writable | Может быть изменен оператором присваивания | атрибут данных |
get | геттерные методы | метод доступа |
set | метод установки | метод доступа |
Важно понимать, что этот метод имеет ограниченную поддержку, начиная с IE8 (недоступно для объектов, отличных от DOM).
🌰 Пример:
var o = {};
o.a = 1;
// 等同于 :
Object.defineProperty(o, "a", {
value : 1,
writable : true,
configurable : true,
enumerable : true
});
var o = {};
var bValue;
Object.defineProperty(o, "b", {
get : function(){ //添加存取方法
return bValue;
},
set : function(newValue){
bValue = newValue;
},
enumerable : true,
configurable : true
});
var o = {};
Object.defineProperty(o, "a", {
value : 37,
writable : false //定义了一个“只读”的属性
});
console.log(o.a); // 37
o.a = 25; // 在严格模式下会抛出错误,非严格模式只是不起作用
console.log(o.a); // 37
var o = {};
Object.defineProperty(o, "a", {
get : function(){return 1;},
configurable : false //不可编辑、不可删除
});
// throws a TypeError
Object.defineProperty(o, "a", {configurable : true});
// throws a TypeError
Object.defineProperty(o, "a", {enumerable : true});
// throws a TypeError
Object.defineProperty(o, "a", {set : function(){}});
// throws a TypeError
Object.defineProperty(o, "a", {get : function(){return 1;}});
// throws a TypeError
Object.defineProperty(o, "a", {value : 12});
console.log(o.a); //1
delete o.a; // 在严格模式下会抛出TypeError,非严格模式只是不起作用
console.log(o.a); //1
Object.defineProperty(o, "conflict", {
value: 0x9f91102,
get: function() {
return 0xdeadbeef;
}
}); //抛出 TypeError,数据属性和存取方法不能混合设置
Связанный метод: Object.getOwnPropertyDescriptor()
Возвращает предыдущий из указанного объектасобственностьСоответствующий дескриптор свойства. (Собственные свойства относятся к свойствам, непосредственно назначенным объекту, а не к свойствам, которые ищутся в цепочке прототипов)
грамматика:
//其中 prop 对应于 Object.defineProperty() 中第三个参数 descriptor
Object.getOwnPropertyDescriptor(obj, prop)
🌰 Пример:
var o = {
get foo() {
return 17;
}
};
Object.getOwnPropertyDescriptor(o, "foo");
// {
// configurable: true,
// enumerable: true,
// get: /*the getter function*/,
// set: undefined
// }
Связанные методы: Object.defineProperties()
определить непосредственно на объектенесколькоНовые свойства или изменение существующих свойств
грамматика:
//prop 和 descriptor 的定义对应于 Object.defineProperty()
Object.defineProperties(obj, {
prop1: descriptor1,
prop2: descriptor2,
...
})
🌰 Пример:
var obj = {};
Object.defineProperties(obj, {
'property1': {
value: true,
writable: true
},
'property2': {
value: 'Hello',
writable: false
}
});
Связанные методы: Object.create()
Создайте новый объект, используя указанный объект-прототип и его свойства.
грамматика:
//proto 为新创建对象的原型对象
//props 对应于 Object.defineProperties() 中的第二个参数
Object.create(proto[, props])
🌰 Пример:
// 创建一个原型为null的空对象
var o = Object.create(null);
var o2 = {};
// 以字面量方式创建的空对象就相当于:
var o2 = Object.create(Object.prototype);
var foo = {a:1, b:2};
var o = Object.create(foo, {
// foo会成为所创建对象的数据属性
foo: {
writable:true,
configurable:true,
value: "hello"
},
// bar会成为所创建对象的访问器属性
bar: {
configurable: false,
get: function() { return 10 },
set: function(value) {
console.log("Setting `o.bar` to", value);
}
}
});
[1.5] __define[G,S]etter__()
так какнестандартныйа такжеУстаревшийМетоды,defineGetter() а такжеdefineSetter() иногда появляется в каком-то историческом коде и до сих пор работает в Firefox/Safari/Chrome и т. д.
🌰 Просто посмотрите на пример:
var o = {
word: null
};
o.__defineGetter__('gimmeFive', function() {
return 5;
});
console.log(o.gimmeFive); // 5
o.__defineSetter__('say', function(vlu) {
this.word = vlu;
});
o.say = "hello";
console.log(o.word); //"hello"
[1.6] __lookup[G,S]etter__()
Так же естьlookupGetter() а такжеlookupSetter() дванестандартныйа такжеУстаревшийМетоды
- lookupGetter() возвращает функцию получения для свойства объекта
🌰 Пример:
var obj = {
get foo() {
return Math.random() > 0.5 ? "foo" : "bar";
}
};
obj.__lookupGetter__("foo")
// (function (){return Math.random() > 0.5 ? "foo" : "bar"})
Если заменить стандартным методом, то это:
Object.getOwnPropertyDescriptor(obj, "foo").get
// (function (){return Math.random() > 0.5 ? "foo" : "bar"})
И если это свойство доступа унаследовано:
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(obj), "foo").get
// function __proto__() {[native code]}
- lookupSetter() Функция установки, которая возвращает свойство объекта
🌰 Пример:
var obj = {
set foo(value) {
this.bar = value;
}
};
obj.__lookupSetter__('foo')
// (function(value) { this.bar = value; })
// 标准且推荐使用的方式。
Object.getOwnPropertyDescriptor(obj, 'foo').set;
// (function(value) { this.bar = value; })
[1.7] с совместимым с onpropertychange древним утренним браузером
В некоторых крайних случаях, когда требуется совместимость с браузерами, такими как IE6/IE7, используйте поддерживаемый IE
onpropertychange
События также могут имитировать геттеры/сеттеры.
Обратите внимание, что этот метод ограничен объектами DOM, которые были загружены в документ.
function addProperty(obj, name, onGet, onSet) {
var
oldValue = obj[name],
getter = function () {
return onGet.apply(obj, [oldValue]);
},
setter = function (newValue) {
return oldValue = onSet.apply(obj, [newValue]);
},
onPropertyChange = function (event) {
if (event.propertyName == name) {
// 暂时移除事件监听以免循环调用
obj.detachEvent("onpropertychange", onPropertyChange);
// 把改变后的值传递给 setter
var newValue = setter(obj[name]);
// 设置 getter
obj[name] = getter;
obj[name].toString = getter;
// 恢复事件监听
obj.attachEvent("onpropertychange", onPropertyChange);
}
};
// 设置 getter
obj[name] = getter;
obj[name].toString = getter;
obj.attachEvent("onpropertychange", onPropertyChange);
}
2. Прокси и отражение в JS
На самом объекте определение контроля доступа к атрибутам один за другим иногда приводит к раздуванию кода и даже сложности в обслуживании; понимание концепции и использования прокси и отражения может эффективно улучшить эти ситуации.
[2.1] Традиционный режим прокси
В классическом шаблоне проектирования (Design Pattern) широко используется шаблон прокси (Proxy Pattern), он определяется как:
В прокси-режиме прокси-объект (Proxy) действует как интерфейс к другому целевому объекту (Real Subject). Прокси-объект находится между пользователем (Клиентом) целевого объекта и самим целевым объектом и отвечает за защиту доступа к целевому объекту.
Типичные сценарии применения:
- Контроль доступа и кэширование целевых объектов
- Задержка инициализации целевого объекта
- получить доступ к удаленному объекту
🌰 Например:
function Book(id, name) {
this.id = id;
this.name = name;
}
function BookShop() {
this.books = {};
}
BookShop.prototype = {
addBook: function(book) {
this.books[book.id] = book;
},
findBook: function(id) {
return this.books[id];
}
}
function BookShopProxy() {
}
BookShopProxy.prototype = {
_init: function() {
if (this.bookshop)
return;
else
this.bookshop = new BookShop;
},
addBook: function(book) {
this._init();
if (book.id in this.bookshop.books) {
console.log('existed book!', book.id);
return;
} else {
this.bookshop.addBook(book);
}
},
findBook: function(id) {
this._init();
if (id in this.bookshop.books)
return this.bookshop.findBook(id);
else
return null;
}
}
var proxy = new BookShopProxy;
proxy.addBook({id:1, name:"head first design pattern"});
proxy.addBook({id:2, name:"thinking in java"});
proxy.addBook({id:3, name:"lua programming"});
proxy.addBook({id:2, name:"thinking in java"}); //existed book! 2
console.log(proxy.findBook(1)); //{ id: 1, name: 'head first design pattern' }
console.log(proxy.findBook(3)); //{ id: 3, name: 'lua programming' }
Очевидно, что приведенный выше пример кода показывает использование прокси для достиженияленивая инициализацияа такжеКонтроль доступа.
Стоит упомянуть, что другой вид шаблона прокси и шаблона проектированияШаблон декоратораИх легко спутать.Разница между ними в том, что они являются упаковкой исходного целевого объекта; разница в том, что первый фокусируется наПредоставляет тот же API, что и исходный объект, с защищенным управлением доступом к нему., в то время как последний фокусируется на добавлении новых функций в исходный API.
[2.2] Прокси в ES6
В стандарте ECMAScript 2015 (6-е издание, ECMA-262) предлагается собственный прокси-объект. Пользовательское поведение для определения основных операций (таких как поиск свойств, присвоение, перечисление, вызовы функций и т. д.)
грамматика:
let p = new Proxy(target, handler);
целевой объект прокси-объектаtarget
, который может быть объектом любого типа, например, объектом, массивом, функцией или даже другим прокси-объектом;let proxy=new Proxy(target,handle)
После операции прокси и целевые объекты будут влиять друг на друга. который:
let target = {
_prop: 'foo',
prop: 'foo'
};
let proxy = new Proxy(target, handler);
proxy._prop = 'bar';
target._attr = 'new'
console.log(target._prop) //'bar'
console.log(proxy._attr) //'new'
а такжеhandler
Он также является объектом, и его несколько заданных свойств определяются одно за другим функцией, которая представляет собой операцию, выполняемую при выполнении соответствующего доступа к целевому объекту; наиболее распространенной операцией является определение свойств get и set геттера/ сеттер:
let handler = {
get (target, key){
return key in target
? target[key]
: -1; //默认值
},
set (target, key, value) {
if (key === 'age') { //校验
target[key] = value > 0 && value < 100 ? value : 0
}
return true;
}
};
let target = {};
let proxy = new Proxy(target, handler);
proxy.age = 22 //22
Можно заметить, что, в отличие от сеттера самого объекта в ES5, сеттер в прокси должен иметь возвращаемое значение;
И это должно быть легко понять, не только имя такое же, но объект Proxy действительно соответствует классическому режиму прокси - объект прокси инкапсулирует и защищает API целевого объекта, скрывает целевой объект и контролирует его доступ поведение.
В дополнение к определению геттеров/сеттеров более полными свойствами обработчика являются следующие:
- "get": function (oTarget, sKey)
- "set": function (oTarget, sKey, vValue)
- "enumerate": function (oTarget, sKey)
- "ownKeys": function (oTarget, sKey)
- "has": function (oTarget, sKey)
- "defineProperty": function (oTarget, sKey, oDesc)
- "deleteProperty": function (oTarget, sKey)
- "getOwnPropertyDescriptor": function (oTarget, sKey)
- "getPrototypeOf(oTarget)"
- "setPrototypeOf(oTarget, oPrototype)"
- "apply(oTarget, thisArg, argumentsList)":
- "construct(oTarget, argumentsList, newTarget)"
- "isExtensible(oTarget)"
- "preventExtensions(oTarget)"
[2.3] Отражение
Отражение объектов — это возможность языка исследовать свойства объектов и манипулировать ими во время выполнения.
В таких языках, как JAVA/AS3, отражение обычно используется для получения имени класса и списка атрибутов объекта во время выполнения, а затем его динамического построения; например, динамическое создание объекта с помощью значения в файле конфигурации XML, или извлеките swf по названию MovieClip и т.п. в файле.
JS также имеет связанные API-интерфейсы отражения, такие какObject.getOwnPropertyDescriptor()
,Function.prototype.apply()
,in
,delete
и т. д., но эти API-интерфейсы распределены по разным пространствам имен или даже глобально зарезервированным словам и не могут этого сделать, вызывая исключения. Эти факторы затрудняют написание и сопровождение кода, включающего отражение объектов.
[2.4] Отражение в ES6
В то же время, что и прокси, в ECMAScript 2015 (6-е издание, ECMA-262) был введен объект Reflect, включающий несколько методов отражения объекта.
метод отражения | Аналогичная операция |
---|---|
Reflect.apply() | Function.prototype.apply() |
Reflect.construct() | new target(...args) |
Reflect.defineProperty() | Object.defineProperty() |
Reflect.deleteProperty() | delete target[name] |
Reflect.enumerate() | Свойства для обхода операций for...in |
Reflect.get() | Похоже на цель [имя] |
Reflect.getOwnPropertyDescriptor() | Object.getOwnPropertyDescriptor() |
Reflect.getPrototypeOf() | Object.getPrototypeOf() |
Reflect.has() | в операторе |
Reflect.isExtensible() | Object.isExtensible() |
Reflect.ownKeys() | Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target)) |
Reflect.preventExtensions() | Object.preventExtensions() |
Reflect.set() | target[name] = val |
Reflect.setPrototypeOf() | Object.setPrototypeOf() |
- Reflect чем-то похож на ES5 Object, включая методы в объектном языке, Reflect также имеет несколько методов, которые полностью соответствуют Proxy.
- Proxy эквивалентен изменению поведения свойства заданного объекта, а Reflect должен получить эти поведения объекта (исходная версия). Эти два часто используются вместе.
- В Reflect нет конструкторов, можно вызывать все его статические методы.
var target = {
a: 1
};
var proxy = new Proxy(target, {
get: function(tgt, key) {
console.log("Get %s", key);
return tgt[key] + 100;
},
set: function(tgt, key, val) {
console.log("Set %s = %s", key, val);
return tgt[key] = "VAL_" + val;
}
});
proxy.a = 2;
//Set a = 2
console.log(proxy.a);
//Get a
//VAL_2100
console.log(Reflect.get(target, "a"));
//VAL_2
Reflect.set(target, "a", 3);
console.log(Reflect.get(target, "a"));
//3
Видно, что если вы напрямую обращаетесь к значению целевого объекта в Proxy, он, скорее всего, вызовет избыточные геттеры/сеттеры; и использование соответствующих методов в Reflect может эффективно избежать этой ситуации.
Также следует отметить, что эти методы не выдают ошибку при неудаче, а возвращают false, это значительно упрощает обработку:
//In ES5
var o = {};
Object.defineProperty(o, 'a', {
get: function() { return 1; },
configurable: false
});
try {
Object.defineProperty(o, 'a', { configurable: true });
} catch(e) {
console.log("Exception");
}
//In ES2015
var o = {};
Reflect.defineProperty(o, 'a', {
get: function() { return 1; },
configurable: false
});
if( !Reflect.defineProperty(o, 'a', { configurable: true }) ) {
console.log("Operation Failed");
}
[2.5] Работа с прокси/рефлексом
🌰 Пример 1: Установить геттер/сеттер для каждого свойства объекта
//in ES5
var obj = {
x: 1,
y: 2,
z: 3
};
function trace1() {
var cache = {};
Object.keys(obj).forEach(function(key) {
cache[key] = obj[key]; //避免循环 setter
Object.defineProperty(obj, key, {
get: function() {
console.log('Get ', key);
return cache[key];
},
set: function(vlu) {
console.log('Set ', key, vlu);
cache[key] = vlu;
}
})
});
}
trace1();
obj.x = 5;
console.log(obj.z);
// Set x 5
// Get z
// 3
//in ES6
var obj2 = {
x: 6,
y: 7,
z: 8
};
function trace2() {
return new Proxy(obj2, {
get(target, key) {
if (Reflect.has(target, key)) {
console.log('Get ', key);
}
return Reflect.get(target, key);
},
set(target, key, vlu) {
if (Reflect.has(target, key)) {
console.log('Set ', key, vlu);
}
return Reflect.set(target, key, vlu);
}
});
}
const proxy2 = trace2();
proxy2.x = 99;
console.log(proxy2.z);
// Set x 99
// Get z
// 8
🌰 Пример 2. Отслеживание вызовов методов
var obj = {
x: 1,
y: 2,
say: function(word) {
console.log("hello ", word)
}
};
var proxy = new Proxy(obj, {
get(target, key) {
const targetValue = Reflect.get(target, key);
if (typeof targetValue === 'function') {
return function (...args) {
console.log('CALL', key, args);
return targetValue.apply(this, args);
}
} else {
console.log('Get ', key);
return targetValue;
}
}
});
proxy.x;
proxy.y;
proxy.say('excel!');
// Get x
// Get y
// CALL say [ 'excel!' ]
// hello excel!
Суммировать
- Геттеры/сеттеры также известны как методы доступа и являются двумя наиболее часто используемыми методами доступа.
- Исходный объект может быть защищен инкапсуляцией с помощью методов доступа, а гибкость логики может быть сохранена.
- ES5 поддерживает неявные методы доступа get и set, которые можно удалить, удалив
- Использование Object.defineProperty() также может устанавливать геттеры/сеттеры и т.д.
- Исторически используется Object.prototype.define[G,S]etter() совместим с onpropertychange для достижения методов доступа
- Традиционный контроль доступа можно улучшить с помощью прокси и отражений.
- Прокси-объект находится между пользователем целевого объекта и самим целевым объектом и отвечает за безопасность доступа к целевому объекту.
- Собственный прокси-объект ES6. Пользовательское поведение для определения основных операций (таких как поиск свойств, присвоение, перечисление, вызовы функций и т. д.)
- Отражение объектов — это возможность языка исследовать свойства объектов и манипулировать ими во время выполнения.
- ES6 представляет объект Reflect, который используется для включения нескольких методов отражения объекта.
- Reflect имеет несколько методов, соответствующих Proxy один к одному, которые часто используются вместе.
Использованная литература:
- иметь дело с JS.IO/голодным 6-функциональным…
- сегмент fault.com/ah/119000000…
- 2 али ненавидит.com/2017/11/pro…
- Ууху. Call.com/question/21…
- блоги.MSDN.Microsoft.com/IE/2010/09/…
- Джон на двоих.Так / родной-разрыв...
- developer.Mozilla.org/this-cn/docs/…
- Блог woohoo.cn на.com/only sterile/ah…
- Woohoo. Письмо Джо: .com/JavaScript/…
- stackoverflow.com/questions/7…
- www.importnew.com/9716.html
- help.Adobe.com/en_US/as2LC…
- blog.CSDN.net/Crossing 555Crossing/Ах…
- Woohoo. Беги О, был убит .Com / arp / changsha ...
- Php.net/manual/en/come...
- antimatter15.com/Боюсь/2010/02/…
- ву ву ву 52 ты не видишь.com/local area/512047…
- pony foo.com/articles/умереть от голода…
- Woohoo. У Кита плохой день. co.UK/meta program…
- 2 Ари Ненависть.com/2014/12/голодание 6…
- Перейдите на nimat.com/starve6-reflect…
Нажмите и удерживайте QR-код или найдите немного жизни, чтобы подписаться на нас